========================================================
作者:qiujuer
博客:blog.csdn.net/qiujuer
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119
——学之开源,用于开源;初学者的心态,与君共勉!
========================================================
在我的文章中曾经有两篇关于Material Design风格的按钮实现。在第一章中只是简单的实现了动画的波纹效果,而在第二篇中对此进行了一定的扩充与优化,最后实现可以自动移动到中心位置的动画;虽然两者都可用,但是在我的使用中却发现了一定的问题,如有些位置点击会出现波纹速度的运算上的问题。
在这一章中将带你打造一个极致的Material Design动画风格Button;至少在我看来与官方的相当接近了。
可以看出其基本上差不多了。
首先我们来解析一下官方的:
在这里我截取了最后一个按钮相应的连续几张图片的情况,从图片我们可以看出以下情况:
我们第二张中的按钮之所以有很大的差距我总结出以下几点:
不知道你们在做的过程中是否想过,我们的动画是在用户点击 onTouch() 的基础上不断的刷新触发 onDraw() 然后绘制来的,与一个按钮的结合点也就是这么两个地方,最多为了方便我们结合的地方还有一个 onMeasure() .所以我们能得出这样一个类:
public class TouchEffectAnimator {
public TouchEffectAnimator(View mView) {
}
public void onMeasure() {
}
public void onTouchEvent(final MotionEvent event) {
}
public void onDraw(final Canvas canvas) {
}
private void startAnimation() {
}
private void cancelAnimation() {
}
private void fadeOutEffect() {
}
}
一个类,这个类作用于一个控件,所以我们需要传入一个 View.
然后我们提供一个 onMeasure() 方法用于初始化高度宽度等数据;onTouchEvent() 当然是用来在控件中触发点击事件所用的;onDraw() 这个无需说也是控件中调用,用来绘制所用;一个动画当然需要启动方法和取消方法,当然在波纹动画后我们还需要的是 "淡出" 的动画。
而后我们想想,其是我们需要的动画类型无非就是那么几种,我们何不合在一起呢?
public enum TouchEffect {
Move,
Ease,
Ripple,
None
}
在这个枚举中分别代表:
一边扩散一边移动到中心,无波纹只有淡入淡出,纯扩散不移动的类型,没有动画的类型。
下面我们来看看主类中的变量情况。
private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(2.8f);
private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator();
private static final int EASE_ANIM_DURATION = 200;
private static final int RIPPLE_ANIM_DURATION = 300;
private static final int MAX_RIPPLE_ALPHA = (int) (255 * 0.8);
分别是:动画减速、加速效果;淡入淡出默认时间200毫秒,扩散时间默认300毫秒,最大的透明度为255的80%用于淡入淡出。主色为255 100%。
在这里,减速效果中之所有一个2.8,其主要作用是使扩散效果在初期尽量的快 (起到隐藏小圆圈),而后期尽量的慢(增强触摸感觉)
private View mView;
private int mClipRadius;
private int mAnimDuration = RIPPLE_ANIM_DURATION;
private TouchEffect mTouchEffect = TouchEffect.Move;
private Animation mAnimation = null;
一个View,一个圆角弧度,一个动画时间,一个动画类型,最后一个动画类(在这里没有使用属性动画,而是准备采用最基本的动画,采用回调来直接设置参数)
private float mMaxRadius;
private float mRadius;
一个最大半径,一个当前半径;之所以有最大半径,在我看来有多种情况:如果是移动模式那么其最大半径扫过地区域能达到最长边的75%就行了;如果是纯扩散,如果用户点击的是最右下角,那么其扫过区域最好能达到其对角的长度;更具勾股定理可以得出其为最长边的1.25倍。
private float mDownX, mDownY;
private float mCenterX, mCenterY;
private float mPaintX, mPaintY;
点击坐标,中心坐标,当前圆心坐标
private Paint mPaint = new Paint();
private RectF mRectRectR = new RectF();
private Path mRectPath = new Path();
private int mRectAlpha = 0;
一只画笔,一个区域,一个区域所生成的Path路径,一个区域透明度
private boolean isTouchReleased = false;
private boolean isAnimatingFadeIn = false;
这两个变量主要用于控制淡出动画触发的时机,我们可以这么想:
在用户一直按着控件的时候就算扩散动画完成了也不进行淡出动画,该动画在用户释放时触发;如果用户点击后立刻抬起那么在抬起时肯定不能触发淡出动画,要等到扩散动画完成后才触发;所以一个变量是是否释放按钮,另外一个是是否动画结束。
private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
isAnimatingFadeIn = true;
}
@Override
public void onAnimationEnd(Animation animation) {
isAnimatingFadeIn = false;
// Is un touch auto fadeOutEffect()
if (isTouchReleased) fadeOutEffect();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
上面刚刚说了,控制其释放触发淡出动画,那么这里这个监听器就是用来监听其开始动画状态的,结束后调整值,如果此时用户释放了按钮则触发淡出效果。OK,继续!
public TouchEffectAnimator(View mView) {
this.mView = mView;
onMeasure();
}
public void onMeasure() {
mCenterX = mView.getWidth() / 2;
mCenterY = mView.getHeight() / 2;
mRectRectR.set(0, 0, mView.getWidth(), mView.getHeight());
mRectPath.reset();
mRectPath.addRoundRect(mRectRectR, mClipRadius, mClipRadius, Path.Direction.CW);
}
在控件触发 onMeasure() 方法的时候回调该类的 onMeasure() 方法,在该方法中我们得出其中心坐标,初始化一个长方形区域,然后根据区域与圆角半径初始化一个Path路径。
public void setAnimDuration(int animDuration) {
this.mAnimDuration = animDuration;
}
public TouchEffect getTouchEffect() {
return mTouchEffect;
}
public void setTouchEffect(TouchEffect touchEffect) {
mTouchEffect = touchEffect;
if (mTouchEffect == TouchEffect.Ease)
mAnimDuration = EASE_ANIM_DURATION;
}
public void setEffectColor(int effectColor) {
mPaint.setColor(effectColor);
}
public void setClipRadius(int mClipRadius) {
this.mClipRadius = mClipRadius;
}
既然上面有那么多的变量,那么这里提供了一些方法用于初始化使用,分别是:
动画时间,获取动画类型,设置动画类型,设置颜色,设置控件的圆角弧度。
private void startAnimation() {
Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (mTouchEffect == TouchEffect.Move) {
mRadius = mMaxRadius * interpolatedTime;
mPaintX = mDownX + (mCenterX - mDownX) * interpolatedTime;
mPaintY = mDownY + (mCenterY - mDownY) * interpolatedTime;
} else if (mTouchEffect == TouchEffect.Ripple) {
mRadius = mMaxRadius * interpolatedTime;
}
mRectAlpha = (int) (interpolatedTime * MAX_RIPPLE_ALPHA);
mView.invalidate();
}
};
animation.setInterpolator(DECELERATE_INTERPOLATOR);
animation.setDuration(mAnimDuration);
animation.setAnimationListener(mAnimationListener);
mView.startAnimation(animation);
}
private void cancelAnimation() {
if (mAnimation != null) {
mAnimation.cancel();
mAnimation.setAnimationListener(null);
}
}
private void fadeOutEffect() {
Animation animation = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
mRectAlpha = (int) (MAX_RIPPLE_ALPHA - (MAX_RIPPLE_ALPHA * interpolatedTime));
mView.invalidate();
}
};
animation.setInterpolator(ACCELERATE_INTERPOLATOR);
animation.setDuration(EASE_ANIM_DURATION);
mView.startAnimation(animation);
}
public void onTouchEvent(final MotionEvent event) {
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
isTouchReleased = true;
if (!isAnimatingFadeIn) {
fadeOutEffect();
}
}
if (event.getActionMasked() == MotionEvent.ACTION_UP) {
isTouchReleased = true;
if (!isAnimatingFadeIn) {
fadeOutEffect();
}
} else if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
// Gets the bigger value (width or height) to fit the circle
mMaxRadius = mCenterX > mCenterY ? mCenterX : mCenterY;
// This circle radius is 75% or fill all
if (mTouchEffect == TouchEffect.Move)
mMaxRadius *= 0.75;
else
mMaxRadius *= 2.5;
// Set default operation to fadeOutEffect()
isTouchReleased = false;
isAnimatingFadeIn = true;
// Set this start point
mPaintX = mDownX = event.getX();
mPaintY = mDownY = event.getY();
// This color alpha
mRectAlpha = 0;
// Cancel and Start new animation
cancelAnimation();
startAnimation();
}
}
在触发方法中,我们分别需要判断是:取消/抬起/按下 操作。
public void onDraw(final Canvas canvas) {
// Draw Area
mPaint.setAlpha(mRectAlpha);
canvas.drawPath(mRectPath, mPaint);
// Draw Ripple
if (isAnimatingFadeIn && (mTouchEffect == TouchEffect.Move
|| mTouchEffect == TouchEffect.Ripple)) {
// Canvas Clip
canvas.clipPath(mRectPath);
mPaint.setAlpha(MAX_RIPPLE_ALPHA);
canvas.drawCircle(mPaintX, mPaintY, mRadius, mPaint);
}
}
这个方法是最后一个方法,也是较核心的一个地方,我们的成果就靠这个方法了。
首先当然是画出背景部分,在画之前当然就是设置背景色;该背景色是一个随动画时间变化的量,具体详见上面动画部分。
然后判断是否是启动动画,因为淡出时也会触发该方法但是却不绘制圆形区域部分,所以需要判断;之后判断是否是属于需要绘制圆形的动画类型;再然后就是绘制具体的圆形区域了,分别就是坐标和半径;但是这里需要注意的是,在绘制前我们调用了 canvas.clipPath(mRectPath); 。
canvas.clipPath(mRectPath):这个的作用就是剪切,意思是剪切画布部分,然后在剪切后的画布上绘制;这样就解决了圆角时溢出的问题,因为剪切后的画布就那么大你就算画到外部也是无法显示的。
public class GeniusButton extends Button implements Attributes.AttributeChangeListener {
private TouchEffectAnimator touchEffectAnimator = null;
public void setTouchEffect(TouchEffect touchEffect) {
if (touchEffect == TouchEffect.None)
touchEffectAnimator = null;
else {
if (touchEffectAnimator == null) {
touchEffectAnimator = new TouchEffectAnimator(this);
touchEffectAnimator.setTouchEffect(touchEffect);
touchEffectAnimator.setEffectColor("this color");
touchEffectAnimator.setClipRadius(20);
}
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (touchEffectAnimator != null)
touchEffectAnimator.onMeasure();
}
@Override
protected void onDraw(Canvas canvas) {
if (touchEffectAnimator != null)
touchEffectAnimator.onDraw(canvas);
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (touchEffectAnimator != null)
touchEffectAnimator.onTouchEvent(event);
return super.onTouchEvent(event);
}
}
在你自定义的控件中按着上面的方式进行实例化调用就OK。
算是分析完了,下面附上源码和我分析时画的一些图,辅助解释。
点击查看
==============更新分割线========================================
更新日期:2015-01-10
今天在二次看代码并优化的时候发现一个错误的地方,在此修正一下;对于给大家带来的不便还请谅解;不过也不影响大局的。
就是在上面中,Ripple 扩散模式下的一个关于其最大半径的运算上的问题。
if (mTouchEffect == TouchEffect.Move)
mMaxRadius *= 0.75;
else
mMaxRadius *= 2.5;
在这里犯了一个数学的错误以及一个体验上的不够细腻的地方:
如上图,定点(A B C D),中心点 E(CX,CY),点击区域(F G H I),点击点(DX,DY)
实际操作中,我们需求判断出距离点击位置最远的定点(A B C D)中的哪一个;所以有了下面的 X Y 值的获取,X Y 就是最远点,然后根据下面公式计算两点之间的距离。
更改后的代码为:
case Ripple:
float x = mDownX < mCenterX ? 2 * mCenterX : 0;
float y = mDownY < mCenterY ? 2 * mCenterY : 0;
mEndRadius = (float) Math.sqrt((x - mDownX) * (x - mDownX) + (y - mDownY) * (y - mDownY));
break;
其中:
mEndRadius 就是上面的
mMaxRadius ,只不过在最新代码中有开始与结束半径两个值。
要说细腻,其是上面还没有考虑圆角的情况,但是一般来说圆角对此的影响也不是很大了,没有必要为了那么点去耗时计算;对于实际使用来说上面已经足够了。
========================================================
作者:qiujuer
博客:blog.csdn.net/qiujuer
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119
——学之开源,用于开源;初学者的心态,与君共勉!
========================================================