📜 ⬆️ ⬇️

RippleDrawable for Pre-L devices

image

Good day!


Those who followed Google IO / 2014 are aware of the new Material Design and the new features. One of them is a pulsating effect when pressed. Yesterday I decided to port it for old devices.

In Android L, they switched to a new effect - pulsation, it is used by default in response to a touch. That is, when you touch the screen, a large disappearing (fades) oval appears with the size of the parent layer and with it grows a circle at the touch point. This animation inspired me to use in my project and I decided to try it out.


')
Animation examples on Google Design .

Create a RippleDrawable class with the auxiliary class Circle that will help us draw circles:

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); } } } 


We need the Circle helper to save the touch point. Now we need two circles: a background circle that covers the entire parent and a smaller circle to display the tangency point. Oh yes, and we'll also declare constants, the default animation value will be 250ms, the default radius of the circle is 150px. How many times to increase the background circle, notes, all the figures are taken by eye.

  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); 


The Paint.ANTI_ALIAS_FLAG flag is intended for smoothing the circles to be circles, and not figs understand some daubs, now initialize our variables in a separate method, indicate that the fill style is “fill” and create circles, then call it in the constructor:

 void initRippleElements(){ mTouchRipple = new Circle(); mBackgroundRipple = new Circle(); mRipplePaint.setStyle(Paint.Style.FILL); mRippleBackgroundPaint.setStyle(Paint.Style.FILL); } 


Done, let's move on to the most interesting touch processing, let's add the OnTouchListener interface to our class:

  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; } ... 

When you touch the screen, we first save the touch coordinates on the circles and the size of the View (for the background circle), then we start the animation, if it has not previously started. By the way, both circles have opacity (transparency), I defined them as 100 for a background circle and from 160 to 40 for a small circle. All the numbers were again taken from the ceiling (a keen eye) (if someone did not understand, the numbers are from 0 to 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(); } 


Now, if the user has touched, we have 2 circles, user and background, but do not go away, and do not even move with the movement of a finger, it's time to fix it:

  void onFingerMove(float x, float y){ mTouchRipple.cx = x; mTouchRipple.cy = y; invalidateSelf(); } 


Check is the circle moving now, huh?

The logic when removing the finger from the trigger, that is, from the screen. If the animation was launched, we should finish it and bring it to the final state, then start the animation, disappearing circles, where the user circle will increase and disappear simultaneously, let's start:

 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(); } 


The animation is ready, we can safely check.

Source
 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); } } } 



Eventually:



Project on Github .

Source: https://habr.com/ru/post/237173/


All Articles