Created
November 10, 2014 19:00
-
-
Save CasperPas/c094c45814cae9b58e81 to your computer and use it in GitHub Desktop.
Helper class for creating Ripple effect Like Paper-Ripple from Polymer or in Material Design
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<LinearLayout | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
android:orientation="vertical"> | |
<vn.casperpas.bbnext.entities.PaperRippleLayout | |
android:layout_width="match_parent" | |
android:layout_height="wrap_content" > | |
<TextView | |
android:layout_width="fill_parent" | |
android:layout_height="wrap_content" | |
android:id="@+id/label" | |
android:textSize="16sp" | |
android:singleLine="true" | |
android:textColor="#333333" | |
android:text="Hello world!" | |
android:padding="10sp" /> | |
</vn.casperpas.bbnext.entities.PaperRippleLayout> | |
</LinearLayout> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package vn.casperpas.bbnext.entities; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Paint; | |
import android.graphics.Path; | |
import android.graphics.RectF; | |
import android.support.v4.view.MotionEventCompat; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.view.animation.Animation; | |
import android.view.animation.AnimationSet; | |
import android.view.animation.DecelerateInterpolator; | |
import android.view.animation.Transformation; | |
import java.util.ArrayList; | |
import java.util.Iterator; | |
/** | |
* Created by casper on 11/8/14. | |
* This is where the magic happens :D | |
*/ | |
public class PaperRipple { | |
private final int EASE_ANIM_DURATION = 200; | |
private final int MAX_RIPPLE_ALPHA = 255; | |
private final int MAX_RIPPLE_RADIUS = 300; | |
private View mView; | |
private RectF mDrawRect; | |
private int mClipRadius; | |
private Path mCirclePath = new Path(); | |
private boolean hasRippleEffect = false; | |
private int mEffectColor = Color.LTGRAY; | |
private int mMaxRadius = MAX_RIPPLE_RADIUS; | |
private int mMaxAlpha = MAX_RIPPLE_ALPHA; | |
private int animDuration = EASE_ANIM_DURATION; | |
private AnimationSet mAnimationSet = null; | |
private ArrayList<RippleWave> mWavesList = new ArrayList<RippleWave>(); | |
private RippleWave mCurrentWave = null; | |
public PaperRipple(View mView) { | |
this.mView = mView; | |
this.mDrawRect = new RectF(0, 0, mView.getWidth(), mView.getHeight()); | |
this.mAnimationSet = new AnimationSet(true); | |
} | |
public void setHasRippleEffect(boolean hasRippleEffect) { | |
this.hasRippleEffect = hasRippleEffect; | |
} | |
public void setAnimDuration(int animDuration) { | |
this.animDuration = animDuration; | |
} | |
public void setEffectColor(int effectColor) { | |
mEffectColor = effectColor; | |
} | |
public void setClipRadius(int mClipRadius) { | |
this.mClipRadius = mClipRadius; | |
} | |
public void setRippleSize(int radius) { | |
mMaxRadius = radius; | |
} | |
public void setMaxAlpha(int alpha) { | |
mMaxAlpha = alpha; | |
} | |
public void onTouchEvent(final MotionEvent event) { | |
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_CANCEL) { | |
mCurrentWave.fadeOutWave(); | |
} else if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_UP) { | |
mCurrentWave.fadeOutWave(); | |
} else if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) { | |
mCurrentWave = new RippleWave(mView, mAnimationSet, animDuration, mEffectColor); | |
mCurrentWave.setMaxAlpha(mMaxAlpha); | |
mCurrentWave.setWaveMaxRadius(mMaxRadius); | |
mCurrentWave.startWave(event.getX(), event.getY()); | |
mWavesList.add(mCurrentWave); | |
} | |
} | |
public void onDraw(final Canvas canvas) { | |
if (hasRippleEffect && mWavesList.size() > 0) { | |
mDrawRect.set(0, 0, mView.getWidth(), mView.getHeight()); | |
mCirclePath.reset(); | |
mCirclePath.addRoundRect(this.mDrawRect, | |
mClipRadius, mClipRadius, Path.Direction.CW); | |
canvas.clipPath(mCirclePath); | |
// Draw ripples | |
for (Iterator<RippleWave> iterator = mWavesList.iterator(); iterator.hasNext();) { | |
RippleWave wave = iterator.next(); | |
if (wave.isFinished()) { | |
iterator.remove(); | |
} else { | |
wave.onDraw(canvas); | |
} | |
} | |
} | |
} | |
class ValueGeneratorAnim extends Animation { | |
private InterpolatedTimeCallback interpolatedTimeCallback; | |
ValueGeneratorAnim(InterpolatedTimeCallback interpolatedTimeCallback) { | |
this.interpolatedTimeCallback = interpolatedTimeCallback; | |
} | |
@Override | |
protected void applyTransformation(float interpolatedTime, Transformation t) { | |
this.interpolatedTimeCallback.onTimeUpdate(interpolatedTime); | |
} | |
} | |
interface InterpolatedTimeCallback { | |
public void onTimeUpdate(float interpolatedTime); | |
} | |
private class RippleWave { | |
private final float opacityDecayVelocity = 1.75f; | |
private int waveMaxRadius = MAX_RIPPLE_RADIUS; | |
private boolean waveFinished = false; | |
private AnimationSet mAnimationSet = new AnimationSet(true); | |
private View mView; | |
private int animDuration = EASE_ANIM_DURATION; | |
private int mCircleAlpha = MAX_RIPPLE_ALPHA; | |
private int mMaxAlpha = MAX_RIPPLE_ALPHA; | |
private Paint mCirclePaint = new Paint(); | |
private float mDownX; | |
private float mDownY; | |
private float mRadius; | |
private Animation.AnimationListener radiusAnimationListener = new Animation.AnimationListener() { | |
@Override | |
public void onAnimationStart(Animation animation) { | |
} | |
@Override | |
public void onAnimationEnd(Animation animation) { | |
} | |
@Override | |
public void onAnimationRepeat(Animation animation) { | |
} | |
}; | |
private Animation.AnimationListener opacityAnimationListener = new Animation.AnimationListener() { | |
@Override | |
public void onAnimationStart(Animation animation) { | |
} | |
@Override | |
public void onAnimationEnd(Animation animation) { | |
waveFinished = true; | |
} | |
@Override | |
public void onAnimationRepeat(Animation animation) { | |
} | |
}; | |
public RippleWave(View view, AnimationSet animationSet, int animDuration, int effectColor) { | |
this.mView = view; | |
this.mAnimationSet = animationSet; | |
this.animDuration = animDuration; | |
this.mCirclePaint.setColor(effectColor); | |
} | |
private int waveOpacity(float interpolatedTime) { | |
int opacity = (int)Math.max(0, mMaxAlpha - (mMaxAlpha * interpolatedTime * this.opacityDecayVelocity)); | |
return opacity; | |
} | |
private float waveRadius(float interpolatedTime) { | |
float radius = waveMaxRadius * 1.1f + 5; | |
float size = radius * (1 - (float)Math.pow(80, -interpolatedTime)); | |
return size; | |
} | |
public boolean isFinished() { | |
return waveFinished; | |
} | |
public void startWave(float x, float y) { | |
mDownX = x; | |
mDownY = y; | |
mCircleAlpha = mMaxAlpha; | |
ValueGeneratorAnim valueGeneratorAnim = new ValueGeneratorAnim(new InterpolatedTimeCallback() { | |
@Override | |
public void onTimeUpdate(float interpolatedTime) { | |
mRadius = RippleWave.this.waveRadius(interpolatedTime); | |
mView.invalidate(); | |
} | |
}); | |
valueGeneratorAnim.setInterpolator(new DecelerateInterpolator()); | |
valueGeneratorAnim.setDuration(animDuration); | |
valueGeneratorAnim.setAnimationListener(radiusAnimationListener); | |
mAnimationSet.addAnimation(valueGeneratorAnim); | |
if (!mAnimationSet.hasStarted() || mAnimationSet.hasEnded()) { | |
mView.startAnimation(mAnimationSet); | |
} | |
} | |
public void fadeOutWave() { | |
ValueGeneratorAnim valueGeneratorAnim = new ValueGeneratorAnim(new InterpolatedTimeCallback() { | |
@Override | |
public void onTimeUpdate(float interpolatedTime) { | |
mCircleAlpha = RippleWave.this.waveOpacity(interpolatedTime); | |
mView.invalidate(); | |
} | |
}); | |
valueGeneratorAnim.setDuration(animDuration); | |
valueGeneratorAnim.setAnimationListener(opacityAnimationListener); | |
// If all animations stopped | |
if (mAnimationSet.hasEnded()) { | |
mAnimationSet.getAnimations().clear(); | |
mAnimationSet.addAnimation(valueGeneratorAnim); | |
mView.startAnimation(mAnimationSet); | |
} else { | |
// Add new animation to current set | |
mAnimationSet.addAnimation(valueGeneratorAnim); | |
} | |
} | |
public void onDraw(final Canvas canvas) { | |
mCirclePaint.setAlpha(mCircleAlpha); | |
canvas.drawCircle(mDownX, mDownY, mRadius, mCirclePaint); | |
} | |
public void setWaveMaxRadius(int radius) { | |
waveMaxRadius = radius; | |
} | |
public void setMaxAlpha(int alpha) { | |
mMaxAlpha = alpha; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package vn.casperpas.bbnext.entities; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.util.AttributeSet; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.widget.RelativeLayout; | |
import vn.casperpas.bbnext.R; | |
/** | |
* User: casper | |
* Date: 8.11.2014 | |
* Time: 17:23 | |
*/ | |
// Using RelativeLayout is just an example. You can use PaperRipple with any View object | |
public class PaperRippleLayout extends RelativeLayout { | |
private PaperRipple paperRipple; | |
public PaperRippleLayout(Context context) { | |
super(context); | |
init(context, null); | |
} | |
public PaperRippleLayout(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(context, attrs); | |
} | |
private void init(Context context, AttributeSet attrs) { | |
// you should set a background to view for effect to be visible. in this sample, this | |
// linear layout contains a transparent background which is set inside the XML | |
// giving the view to animate on | |
paperRipple = new PaperRipple(this); | |
// enabling ripple effect. it only performs ease effect without enabling ripple effect | |
paperRipple.setHasRippleEffect(true); | |
int rippleColor = Color.LTGRAY; | |
int animationDuration = 1000; | |
int clipRadius = 0; | |
// Remove this if you don't use custom style attrs via XML | |
if (attrs != null) { | |
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.PaperRippleLayout); | |
rippleColor = a.getColor(R.styleable.PaperRippleLayout_ripple_color, rippleColor); | |
animationDuration = a.getInt(R.styleable.PaperRippleLayout_duration, animationDuration); | |
clipRadius = a.getInt(R.styleable.PaperRippleLayout_clip_radius, clipRadius); | |
} | |
// setting the effect color | |
paperRipple.setEffectColor(rippleColor); | |
// setting the duration | |
paperRipple.setAnimDuration(animationDuration); | |
// setting radius to clip the effect. use it if you have a rounded background | |
paperRipple.setClipRadius(clipRadius); | |
// the view must contain an onClickListener to receive UP touch events. paperRipple | |
// doesn't return any value in onTouchEvent for flexibility. so it is developers | |
// responsibility to add a listener | |
setOnClickListener(new OnClickListener() { | |
@Override | |
public void onClick(View view) { | |
} | |
}); | |
} | |
@Override | |
public boolean onTouchEvent(final MotionEvent event) { | |
// send the touch event to animator | |
paperRipple.onTouchEvent(event); | |
return super.onTouchEvent(event); | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
// let animator show the animation by applying changes to view's canvas | |
paperRipple.onDraw(canvas); | |
super.onDraw(canvas); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is great!! Is this code open source?