RippleDrawable для Pre-L устройств
Доброго времени суток! Те, кто следил за Google IO/2014, знают о новом Material Design и новых фишках. Одной из них является пульсирующий эффект при нажатии. Вчера я решил его портировать для старых устройств.В Android L перешли на новый эффект — пульсирование, он используется по умолчанию в ответной реакции на касание. То есть при касании на экран появляется большой исчезающий (fades) овал с размером родительского слоя и вместе с ним растет круг в точке прикосновения. Эта анимация меня вдохвновила использовать в своем проекте и я решил попробовать его сделать.[embedded content]
Примеры анимации на 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 мс, радиус круга по умолчанию в 150 px. Во сколько раз увеличивать фоновой круг, примечания, все цифры взяты на глаз.
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 увеличиваем круги, минимальный радиус 40 px 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
@Override public Float get (RippleDrawable object) { return object.getAnimationState (); } };
final static Property
@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 увеличиваем круги, минимальный радиус 40 px 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); } }
}
В итоге:
[embedded content]
Проект на Github.