Доброго времени суток!
Те, кто следил за Google IO/2014, знают о новом Material Design и новых фишках. Одной из них является пульсирующий эффект при нажатии. Вчера я решил его портировать для старых устройств.
В Android L перешли на новый эффект — пульсирование, он используется по умолчанию в ответной реакции на касание. То есть при касании на экран появляется большой исчезающий (fades) овал с размером родительского слоя и вместе с ним растет круг в точке прикосновения. Эта анимация меня вдохвновила использовать в своем проекте и я решил попробовать его сделать.
Примеры анимации на Google Design.
Создадим класс RippleDrawable со вспомогательным классом Circle, который будет помогать нам рисовать круги:
class RippleDrawable extends Drawable{ final static class Circle{ float cx; // x координата центра круга float cy; // y координата центра круга float radius; // радиус круга /** * Рисуем круг * * @param canvas Canvas для рисования * @param paint Paint с описанием как стилизировать наш круг */ public void draw(Canvas canvas, Paint paint){ canvas.drawCircle(cx, cy, radius, paint); } } }
Вспомогательный элемент Circle нам понадобится для сохранения точки касания. Теперь нам понадобится два круга: фоновой круг, который покроет всего родителя и круг поменьше, для отображения точки касания. Ах, да, и еще объявим константы, значение анимации по умолчанию будет 250мс, радиус круга по умолчанию в 150px. Во сколько раз увеличивать фоновой круг, примечания, все цифры взяты на глаз.
class RippleDrawable extends Drawable{ final static int DEFAULT_ANIM_DURATION = 250; final static float END_RIPPLE_TOUCH_RADIUS = 150f; final static float END_SCALE = 1.3f; // Круг для касания Circle mTouchRipple; // Фоновой круг Circle mBackgroundRipple; // Стили для прорисовки "круга для касания" Paint mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG); // Стили для фонового круга Paint mRippleBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
Флаг Paint.ANTI_ALIAS_FLAG предназначен для сглаживания, чтобы круги были кругами, а не фиг пойми мазней какой-то, теперь инициализируем наши переменные в отдельном методе, укажем что стиль окраски «заливка» и создадим круги, далее вызовем его в конструкторе:
void initRippleElements(){ mTouchRipple = new Circle(); mBackgroundRipple = new Circle(); mRipplePaint.setStyle(Paint.Style.FILL); mRippleBackgroundPaint.setStyle(Paint.Style.FILL); }
Готово, перейдем к наверное самому интересному обработке касаний, добавим в наш класс интерфейс OnTouchListener:
class RippleDrawable extends Drawable implements OnTouchListener{ ... @Override public boolean onTouch(View v, MotionEvent event) { // Сохраняем совершенное действие final int action = event.getAction(); // и в зависимости от действия выполняем методы switch (action){ // Пользователь коснулся экрана case MotionEvent.ACTION_DOWN: onFingerDown(v, event.getX(), event.getY()); // Для того что бы события View срабатывали нам нужно его вызывать return v.onTouchEvent(event); // Пользователь двигает пальцем по экрану (это продолжения касания) case MotionEvent.ACTION_MOVE: onFingerMove(event.getX(), event.getY()); break; // Пользователь убал свой пальчик case MotionEvent.ACTION_UP: onFingerUp(); break; } return false; } ...
При касании по экрану сначала мы сохраняем координаты касания по кругам и размер View (для фонового круга), затем стартуем анимашку, если она ранее не стартовала. Кстати говоря, у обоих кругов имеется opacity (прозрачность), я их определил как 100 для фонового круга и от 160 до 40 для маленьго кружочка. Все цифры опять же были взяты из потолка (зоркий глаз) (если кто не понял, цифры от 0 до 255 argb).
int mViewSize = 0; void onFingerDown(View v, float x, float y){ mTouchRipple.cx = mBackgroundRipple.cx = x; mTouchRipple.cy = mBackgroundRipple.cy = y; mTouchRipple.radius = mBackgroundRipple.radius = 0f; mViewSize = Math.max(v.getWidth(), v.getHeight()); // Если прошлая анимация закончилась создадим новую if(mCurrentAnimator == null){ // Укажем состояние по умолчанию для нашего фонового круга // тоесть восстановим его прозрачность на дефолтный mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA); // Создадим анимашку, здесь константа CREATE_TOUCH_RIPPLE это геттеры и сеттеры // для отправки состояния анимации mCurrentAnimator = ObjectAnimator.ofFloat(this, CREATE_TOUCH_RIPPLE, 0f, 1f); mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION); } // Если анимация играет ничего не делаем ждем пока закончится if(!mCurrentAnimator.isRunning()){ mCurrentAnimator.start(); } } // Сохранение состояния, необходимо для ObjectAnimator float mAnimationValue; /** * ObjectAnimator вызывает эту функции * * @param value состояние анимации от 0 до 1 */ void createTouchRipple(float value){ mAnimationValue = value; // step by step увеличиваем круги, минимальный радиус 40px mTouchRipple.radius = 40f + (mAnimationValue * (END_RIPPLE_TOUCH_RADIUS - 40f)); mBackgroundRipple.radius = mAnimationValue * (mViewSize * END_SCALE); // и плавное исчезновние еще не появивщихся кругов, // тоесть при старте анимации их opacity максимальная, // и в конце она падает до минимального значения int min = RIPPLE_TOUCH_MIN_ALPHA; int max = RIPPLE_TOUCH_MAX_ALPHA; int alpha = min + (int) (mAnimationValue * (max - min)); mRipplePaint.setAlpha((max + min) - alpha); // Перерисовываем invalidateSelf(); }
Теперь, если пользователь коснулся, у нас появляются 2 круга, пользовательский и фоновой, но не уходят, и даже не двигаются при движении пальца, пора это исправлять:
void onFingerMove(float x, float y){ mTouchRipple.cx = x; mTouchRipple.cy = y; invalidateSelf(); }
Проверьте, двигается теперь кружочек-то, а?
Логика при снятии пальца с курка, то есть с экрана. Если анимация была запущена, мы должны ее закончить и привести к конечному состоянию, далее запустить анимацию, исчезновения кругов, где пользовательский круг будет увеличиваться и исчезать одновременно, приступим:
void onFingerUp(){ // Заканчиваем анимацию if(mCurrentAnimator != null) { mCurrentAnimator.end(); mCurrentAnimator = null; createTouchRipple(1f); } // Создаем новую, и при завершении очищаем ее mCurrentAnimator = ObjectAnimator.ofFloat(this, DESTROY_TOUCH_RIPPLE, 0f, 1f); mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION); mCurrentAnimator.addListener(new SimpleAnimationListener(){ @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mCurrentAnimator = null; } }); mCurrentAnimator.start(); } void destroyTouchRipple(float value){ // Сохраняем состояние анимации mAnimationValue = value; // Увеличиваем радиус круга до фонового радиуса mTouchRipple.radius = END_RIPPLE_TOUCH_RADIUS + (mAnimationValue * (mViewSize * END_SCALE)); // и одновременно у обоих кругов создаем эффект затухания mRipplePaint.setAlpha((int) (RIPPLE_TOUCH_MIN_ALPHA - (mAnimationValue * RIPPLE_TOUCH_MIN_ALPHA))); mRippleBackgroundPaint.setAlpha ((int) (RIPPLE_BACKGROUND_ALPHA - (mAnimationValue * RIPPLE_BACKGROUND_ALPHA))); // ну и как же без перерисовки? invalidateSelf(); }
Анимация готова, можем смело проверять.
import android.animation.Animator; import android.animation.ObjectAnimator; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; import android.util.Property; import android.view.MotionEvent; import android.view.View; public class RippleDrawable extends Drawable implements View.OnTouchListener{ final static Property<RippleDrawable, Float> CREATE_TOUCH_RIPPLE = new FloatProperty<RippleDrawable>("createTouchRipple") { @Override public void setValue(RippleDrawable object, float value) { object.createTouchRipple(value); } @Override public Float get(RippleDrawable object) { return object.getAnimationState(); } }; final static Property<RippleDrawable, Float> DESTROY_TOUCH_RIPPLE = new FloatProperty<RippleDrawable>("destroyTouchRipple") { @Override public void setValue(RippleDrawable object, float value) { object.destroyTouchRipple(value); } @Override public Float get(RippleDrawable object) { return object.getAnimationState(); } }; final static int DEFAULT_ANIM_DURATION = 250; final static float END_RIPPLE_TOUCH_RADIUS = 150f; final static float END_SCALE = 1.3f; final static int RIPPLE_TOUCH_MIN_ALPHA = 40; final static int RIPPLE_TOUCH_MAX_ALPHA = 120; final static int RIPPLE_BACKGROUND_ALPHA = 100; Paint mRipplePaint = new Paint(Paint.ANTI_ALIAS_FLAG); Paint mRippleBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); Circle mTouchRipple; Circle mBackgroundRipple; ObjectAnimator mCurrentAnimator; Drawable mOriginalBackground; public RippleDrawable() { initRippleElements(); } public static void createRipple(View v, int primaryColor){ RippleDrawable rippleDrawable = new RippleDrawable(); rippleDrawable.setDrawable(v.getBackground()); rippleDrawable.setColor(primaryColor); rippleDrawable.setBounds(v.getPaddingLeft(), v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom()); v.setOnTouchListener(rippleDrawable); if(Build.VERSION.SDK_INT >= 16) { v.setBackground(rippleDrawable); }else{ v.setBackgroundDrawable(rippleDrawable); } } public static void createRipple(int x, int y, View v, int primaryColor){ if(!(v.getBackground() instanceof RippleDrawable)) { createRipple(v, primaryColor); } RippleDrawable drawable = (RippleDrawable) v.getBackground(); drawable.setColor(primaryColor); drawable.onFingerDown(v, x, y); } /** * Set colors of ripples * * @param primaryColor color of ripples */ public void setColor(int primaryColor){ mRippleBackgroundPaint.setColor(primaryColor); mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA); mRipplePaint.setColor(primaryColor); invalidateSelf(); } /** * set first layer you background drawable * * @param drawable original background */ public void setDrawable(Drawable drawable){ mOriginalBackground = drawable; invalidateSelf(); } void initRippleElements(){ mTouchRipple = new Circle(); mBackgroundRipple = new Circle(); mRipplePaint.setStyle(Paint.Style.FILL); mRippleBackgroundPaint.setStyle(Paint.Style.FILL); } @Override public void draw(Canvas canvas) { if(mOriginalBackground != null){ mOriginalBackground.setBounds(getBounds()); mOriginalBackground.draw(canvas); } mBackgroundRipple.draw(canvas, mRippleBackgroundPaint); mTouchRipple.draw(canvas, mRipplePaint); } @Override public void setAlpha(int alpha) {} @Override public void setColorFilter(ColorFilter cf) {} @Override public int getOpacity() { return 0; } @Override public boolean onTouch(View v, MotionEvent event) { // Сохраняем совершенное действие final int action = event.getAction(); // и в зависимости от действия выполняем методы switch (action){ // Пользователь коснулся экрана case MotionEvent.ACTION_DOWN: onFingerDown(v, event.getX(), event.getY()); // Для того что бы события View срабатывали нам нужно его вызывать return v.onTouchEvent(event); // Пользователь двигает пальцем по экрану (это продолжения касания) case MotionEvent.ACTION_MOVE: onFingerMove(event.getX(), event.getY()); break; // Пользователь убал свой пальчик case MotionEvent.ACTION_UP: onFingerUp(); break; } return false; } int mViewSize = 0; void onFingerDown(View v, float x, float y){ mTouchRipple.cx = mBackgroundRipple.cx = x; mTouchRipple.cy = mBackgroundRipple.cy = y; mTouchRipple.radius = mBackgroundRipple.radius = 0f; mViewSize = Math.max(v.getWidth(), v.getHeight()); // Если прошлая анимация закончилась создадим новую if(mCurrentAnimator == null){ // Укажем состояние по умолчанию для нашего фонового круга // тоесть восстановим его прозрачность на дефолтный mRippleBackgroundPaint.setAlpha(RIPPLE_BACKGROUND_ALPHA); // Создадим анимашку, здесь константа CREATE_TOUCH_RIPPLE это геттеры и сеттеры // для отправки состояния анимации mCurrentAnimator = ObjectAnimator.ofFloat(this, CREATE_TOUCH_RIPPLE, 0f, 1f); mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION); } // Если анимация играет ничего не делаем ждем пока закончится if(!mCurrentAnimator.isRunning()){ mCurrentAnimator.start(); } } float mAnimationValue; /** * ObjectAnimator вызывает эту функции * * @param value состояние анимации от 0 до 1 */ void createTouchRipple(float value){ mAnimationValue = value; // step by step увеличиваем круги, минимальный радиус 40px mTouchRipple.radius = 40f + (mAnimationValue * (END_RIPPLE_TOUCH_RADIUS - 40f)); mBackgroundRipple.radius = mAnimationValue * (mViewSize * END_SCALE); // и плавное исчезновние еще не появивщихся кругов, // тоесть при старте анимации их opacity максимальная, // и в конце она падает до минимального значения int min = RIPPLE_TOUCH_MIN_ALPHA; int max = RIPPLE_TOUCH_MAX_ALPHA; int alpha = min + (int) (mAnimationValue * (max - min)); mRipplePaint.setAlpha((max + min) - alpha); // Перерисовываем invalidateSelf(); } void destroyTouchRipple(float value){ // Сохраняем состояние анимации mAnimationValue = value; // Увеличиваем радиус круга до фонового радиуса mTouchRipple.radius = END_RIPPLE_TOUCH_RADIUS + (mAnimationValue * (mViewSize * END_SCALE)); // и одновременно у обоих кругов создаем эффект затухания mRipplePaint.setAlpha((int) (RIPPLE_TOUCH_MIN_ALPHA - (mAnimationValue * RIPPLE_TOUCH_MIN_ALPHA))); mRippleBackgroundPaint.setAlpha ((int) (RIPPLE_BACKGROUND_ALPHA - (mAnimationValue * RIPPLE_BACKGROUND_ALPHA))); // ну и как же без перерисовки? invalidateSelf(); } float getAnimationState(){ return mAnimationValue; } void onFingerUp(){ // Заканчиваем анимацию if(mCurrentAnimator != null) { mCurrentAnimator.end(); mCurrentAnimator = null; createTouchRipple(1f); } // Создаем новую, и при завершении очищаем ее mCurrentAnimator = ObjectAnimator.ofFloat(this, DESTROY_TOUCH_RIPPLE, 0f, 1f); mCurrentAnimator.setDuration(DEFAULT_ANIM_DURATION); mCurrentAnimator.addListener(new SimpleAnimationListener(){ @Override public void onAnimationEnd(Animator animation) { super.onAnimationEnd(animation); mCurrentAnimator = null; } }); mCurrentAnimator.start(); } void onFingerMove(float x, float y){ mTouchRipple.cx = x; mTouchRipple.cy = y; invalidateSelf(); } @Override public boolean setState(int[] stateSet) { if(mOriginalBackground != null){ return mOriginalBackground.setState(stateSet); } return super.setState(stateSet); } @Override public int[] getState() { if(mOriginalBackground != null){ return mOriginalBackground.getState(); } return super.getState(); } final static class Circle{ float cx; float cy; float radius; public void draw(Canvas canvas, Paint paint){ canvas.drawCircle(cx, cy, radius, paint); } } }
В итоге:
ссылка на оригинал статьи http://habrahabr.ru/post/237173/
Добавить комментарий