RippleDrawable для Pre-L устройств

imageДоброго времени суток! Те, кто следил за 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 CREATE_TOUCH_RIPPLE = new FloatProperty(«createTouchRipple») { @Override public void setValue (RippleDrawable object, float value) { object.createTouchRipple (value); }

@Override public Float get (RippleDrawable object) { return object.getAnimationState (); } };

final static Property DESTROY_TOUCH_RIPPLE = new FloatProperty(«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 увеличиваем круги, минимальный радиус 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.

© Habrahabr.ru