Поиск по этому блогу

вторник, 10 апреля 2018 г.

Анимирование кнопка с прогрессом долгого нажатия

Бывают не тривиальные задания на подобии создания простых вьюх которые расширяют функционал приложения, добавляют какую-то изюминку, так вот в нашем случае у нас будет кнопка по нажатию и удержанию на которую у нас будет происходить заполнение кнопки, а по достижению конца кнопки анимация будет обнуляться, и будет отображаться сообщение о том что мы закончили задание. 

image

Просто и красиво. Начнем мы с того что подключим как не странно только одну библиотеку, ButterKnife. Этого нам будет достаточно для написания этой вью. По сути ButterKnife особо нам в этом не поможет, но поможет красиво подключать в активити и вьюхах элементы без не красивого findViewById.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'

    compile 'com.jakewharton:butterknife:8.8.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

Тут у нас только ButterKnife и SupportLibrary. Этого и хватит. Дальше перейдем к написанию вьюхи которая будет у нас как бы задним фоном кнопки, в ней у нас будет рисоваться сама кнопка и логика анимации заполнения.

ButtonBackgroundView.java
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;

import project.dajver.com.longclickbuttonview.R;

public class ButtonBackgroundView extends View {

    private Paint mSolidPaint;
    private RectF mSolidRect;
    private float mSolidMultiplier;
    private float mClipMultiplier;

    private Paint mStrokePaint;
    private RectF mStrokeRect;
    private float mStrokeMultiplier;

    private Paint mWipePaint;

    public ButtonBackgroundView(Context context) {
        super(context);
        init();
    }

    public ButtonBackgroundView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ButtonBackgroundView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mSolidPaint = new Paint();
        mSolidPaint.setColor(getResources().getColor(R.color.colorPrimary));
        mSolidPaint.setAntiAlias(true);

        mStrokePaint = new Paint();
        mStrokePaint.setColor(getResources().getColor(R.color.colorPrimary));
        mStrokePaint.setStyle(Paint.Style.STROKE);
        mStrokePaint.setStrokeWidth(getResources().getDimension(R.dimen.ring_width));
        mStrokePaint.setAntiAlias(true);

        mWipePaint = new Paint();
        mWipePaint.setColor(Color.WHITE);
        mWipePaint.setAlpha(51);
        mWipePaint.setAntiAlias(true);

        mSolidRect = new RectF();
        mStrokeRect = new RectF();

        resetAnimatedValues();
    }

    private void resetAnimatedValues() {
        mSolidMultiplier = 1.0f;
        mStrokeMultiplier = 1.0f;
        mStrokePaint.setAlpha(0);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int width = canvas.getWidth();
        float halfWidth = width / 2.0f;
        float adjustedHalfWidth = halfWidth * 0.78260869565217f;
        float solidHalfWidth = adjustedHalfWidth * mSolidMultiplier;
        float strokeHalfWidth = adjustedHalfWidth * mStrokeMultiplier;
        float strokeDifference = strokeHalfWidth - solidHalfWidth;

        int height = canvas.getHeight();
        float halfHeight = height / 2.0f;
        float solidHalfHeight = halfHeight * mSolidMultiplier * 0.46808510638298f;
        float strokeHeightDifference = solidHalfHeight + strokeDifference;

        canvas.drawARGB(0, 0, 0, 0);

        mStrokeRect.set(halfWidth - strokeHalfWidth, halfHeight - strokeHeightDifference, halfWidth + strokeHalfWidth, halfHeight + strokeHeightDifference);
        canvas.drawRoundRect(mStrokeRect, strokeHeightDifference, strokeHeightDifference, mStrokePaint);

        mSolidRect.set(halfWidth - solidHalfWidth, halfHeight - solidHalfHeight, halfWidth + solidHalfWidth, halfHeight + solidHalfHeight);
        canvas.drawRoundRect(mSolidRect, solidHalfHeight, solidHalfHeight, mSolidPaint);

        float clipStart = halfWidth - solidHalfWidth;
        float clipEnd = clipStart + (solidHalfWidth * 2.0f) * mClipMultiplier;
        canvas.clipRect(clipStart, 0, clipEnd, height);
        canvas.drawRoundRect(mSolidRect, solidHalfHeight, solidHalfHeight, mWipePaint);
    }

    public Animator getWipeAnimator(long animationTimeMs) {
        ValueAnimator wipeAnimator = ValueAnimator.ofFloat(0.0f, 1.0f).setDuration(animationTimeMs);
        wipeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mClipMultiplier = valueAnimator.getAnimatedFraction();
                invalidate();
            }
        });
        wipeAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationCancel(Animator animation) {
                mClipMultiplier = 0f;
                invalidate();
            }
        });
        return wipeAnimator;
    }
}

В самом верху класса мы определили какие переменные нам понадобятся для отрисовки кнопки, дальше в конструкторе проинициализировали метод init(), в котором у нас идет инициализация цвета фона, скругление, и т.д. В этом методе у нас вызывается метод resetAnimatedValues() который обнуляет всю анимацию на вьюхе, когда она у нас первый раз появляется на экране, в дальнейшем она понадобится для возвращения вьюхи в прежний вид когда мы ее закрасим другим цветом. 

В методе onDraw() мы производим определенные манипуляции по рисованию кнопки, задаем форму кнопки, цвет, сразу рисуем поверх всего наш прогресс, но делаем его равным 0, так как в дальнейшем мы будем его увеличивать до определенного состояния.

И в getWipeAnimator() мы меняем состояние нашего слоя прогресса для того что бы отображать продолжительность нажатия кнопки.

Так же нам нужно добавить colors и dimens которые используются в этой вьюхе. У меня они выглядят вот так.

color.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    <color name="white">#FFF</color>
</resources>

dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="ring_width">2dp</dimen>
    <dimen name="button_width">330dp</dimen>
    <dimen name="button_height">94dp</dimen>
</resources>

Теперь нам нужно используя эту вьюху создать еще одну, которая будет комбинировать в себе наш фон, и текст поверх него.

ButtonView.java
import android.animation.Animator;
import android.content.Context;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnTouch;
import project.dajver.com.longclickbuttonview.R;

public class ButtonView extends FrameLayout {

    public static final long HOLD_TIME_MS = 1800L;

    @BindView(R.id.background)
    ButtonBackgroundView mBackground;

    private Animator mLongPressAnimator;
    private Rect mBoundsRectangle;
    private Handler mHandler;
    private OnButtonLongPressReachedEndListener listener;

    public ButtonView(Context context) {
        super(context);
        init();
    }

    public ButtonView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public ButtonView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        LayoutInflater.from(getContext()).inflate(R.layout.view_long_press_button, this);
        ButterKnife.bind(this);

        mBoundsRectangle = new Rect();
        mHandler = new Handler();
    }

    private Runnable mOnLongPressed = new Runnable() {
        public void run() {
            if (mLongPressAnimator != null) {
                mLongPressAnimator.cancel();
                listener.onButtonLongPressReachedEnd();
            }
        }
    };

    @OnTouch(R.id.hold_to_end_button)
    boolean onTouchPulsingButton(View view, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mBoundsRectangle.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());

                mLongPressAnimator = mBackground.getWipeAnimator(HOLD_TIME_MS);
                mLongPressAnimator.start();
                mHandler.postDelayed(mOnLongPressed, HOLD_TIME_MS);
                break;
            case MotionEvent.ACTION_MOVE:
                if (mBoundsRectangle.contains(view.getLeft() + (int) event.getX(), view.getTop() + (int) event.getY())) {
                    break;
                }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_OUTSIDE:
                mHandler.removeCallbacks(mOnLongPressed);
                mLongPressAnimator.cancel();
                break;
        }
        return true;
    }

    public void setOnButtonLongPressReachedEndListener(OnButtonLongPressReachedEndListener listener) {
        this.listener = listener;
    }

    public interface OnButtonLongPressReachedEndListener {
        void onButtonLongPressReachedEnd();
    }
}

Эта вьюха нам нужна для того что бы наша кнопка выглядела полноценно, что бы у нас был не просто закругленный прямоугольник, а еще и текст который бы пояснял что нужно сделать для достижения нужного эфекта. Собственно в самом верху класса мы проинициализировали наш ButtonBackgroundView, задали количество секунд (1800L) которые мы будем использовать для расчета окончания нажатия, и несколько переменных для анимации, таймера и лисенер для создания колбека в активити по окончанию действия.

Проинициализировав метод init() в конструкторах, и подключив наш layout в этом методе мы переходим дальше. В методе mOnLongPressed() проверяется, что если mLongPressAnimator != null, значит удаляем анимацию из вьюхи, и возвращаем в колбек что пользователь достиг конца кнопки, а если же mLongPressAnimator будет равен null, будет значит что не дождался…

onTouchPulsingButton() у нас отслеживает долгое нажатие и создает анимацию появления прогресса на кнопке, в зависимости от статуса. Если это ACTION_DOWN — мы рисуем анимацию по таймеру, заполняя mBoundsRectangle, который нам нужен для просчета размера кнопки, что бы кнопка заполнялась в не зависимости от размеров которые мы ей задали в начале. Если же это ACTION_UP, ACTION_CANCEL или ACTION_OUTSIDE — мы убираем ее с вьюхи и отключаем таймер.

Ну, а в самом низу класса у нас колбек и сеттер для него. Они нам нужны для того что бы получать результат и возвращать его в активити.

Сам же xml вьюхи выглядит вот так:

view_long_press_button.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/hold_to_end_button"
    android:layout_width="@dimen/button_width"
    android:layout_height="@dimen/button_height">

    <project.dajver.com.longclickbuttonview.view.ButtonBackgroundView
        android:id="@+id/background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <TextView
        android:id="@+id/text"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/hold.to.end"
        android:textAllCaps="false"
        android:textColor="@color/white"
        android:textSize="24sp"/>

</FrameLayout>

Подключили наш фон для кнопки, и положили текст поверх нее текст для того что бы было хоть чуть-чуть похоже на кнопку, вроде бы выглядит убедительно.

Ну а теперь осталось только заиспользовать эту кнопку в нашей активити.

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.Toast;

import butterknife.BindView;
import butterknife.ButterKnife;
import project.dajver.com.longclickbuttonview.view.ButtonView;

public class MainActivity extends AppCompatActivity implements ButtonView.OnButtonLongPressReachedEndListener {

    @BindView(R.id.buttonView)
    ButtonView buttonView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        buttonView.setOnButtonLongPressReachedEndListener(this);
    }

    @Override
    public void onButtonLongPressReachedEnd() {
        Toast.makeText(this, getString(R.string.you_ended_this_button), Toast.LENGTH_LONG).show();
    }
}

В onCreate() проинициализировали layout, ButterKnife и назначили колбеку место где он будет возвращать результат. onButtonLongPressReachedEnd() наш метод который возвращает результат. Собственно и все. Наша размета будет выглядеть очень просто.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical|center_horizontal">

    <project.dajver.com.longclickbuttonview.view.ButtonView
        android:id="@+id/buttonView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

Просто добавили кнопку на экран и отцетровали, собственно и все.

Исходники:
GitHub