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

от автора

image

Доброго времени суток!

Те, кто следил за 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);         }     }  }   

В итоге:

Проект на Github.

ссылка на оригинал статьи http://habrahabr.ru/post/237173/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *