========================================================
作者:qiujuer
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119
——学之开源,用于开源;初学者的心态,与君共勉!
========================================================
序
在我的文章中曾经有两篇关于Material Design风格的按钮实现。在第一章中只是简单的实现了动画的波纹效果,而在第二篇中对此进行了一定的扩充与优化,最后实现可以自动移动到中心位置的动画;虽然两者都可用,但是在我的使用中却发现了一定的问题,如有些位置点击会出现波纹速度的运算上的问题。
在这一章中将带你打造一个极致的Material Design动画风格Button;至少在我看来与官方的相当接近了。
效果
个人
官方
可以看出其基本上差不多了。
分析
首先我们来解析一下官方的:
在这里我截取了最后一个按钮相应的连续几张图片的情况,从图片我们可以看出以下情况:
- 官方也是采用圆形水波,非圆角矩形水波(这个与我最开始所想不太一样)
- 其扩散速度逐渐递减,圆心的时候基本一闪就过
- 圆形波纹颜色一直没有变化
- 控件按钮整体背景色逐渐加深
- 点击位置在右下角,但是从扩散情况来看其水波圆心逐渐向按钮控件中心靠拢
- 这些也就是我们需要实现的部分。
实现原理
- 圆形扩散,第一章中有讲述 [Material Design] 教你做一个Material风格、动画的按钮(MaterialButton)
- 中心靠拢,第二章中有讲述 [Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果,不过有一定的偏差,不过其偏差在于时间的控制,并不影响大局
- 扩散速度递减,这个可以通过设置动画的 Interpolator 来解决,这个在 渗透理解Animation时间插值Interpolator类 中有详细的介绍
- 背景色加深,这个无非就是在圆形波纹下面再绘制一层,该层的颜色逐渐加深就OK;当然还需要注意的是其圆角情况。
我们第二张中的按钮之所以有很大的差距我总结出以下几点:
- 中心靠拢的速度控制上不对
- 整体的减速 Interpolator 类设置不对,虽然同样是减速,但是可以看出官方的起步很快,而后递减很慢,这个可以通过初始化的时候传入 Interpolator 参数解决
- 水波颜色控制不对,颜色应该不变化,变化的是背景色的颜色
- 没有背景色变化的过程,这个过程需要添加,同时这里有一个细节,其最后的颜色并没有加到最深,大约相当于波纹颜色的80%左右
- 没有考虑圆角情况,在第二章中如果控件是圆角,其波纹将会超出圆角而后消失。
代码
不知道你们在做的过程中是否想过,我们的动画是在用户点击 onTouch() 的基础上不断的刷新触发 onDraw() 然后绘制来的,与一个按钮的结合点也就是这么两个地方,最多为了方便我们结合的地方还有一个 onMeasure() .所以我们能得出这样一个类:
Class
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); }
- 三个方法中,取消最简单了,调用时判断,然后取消,并把监听器设置为 null.
- 淡出动画中:我们在其方法回调中设置我们的透明度为递减的形式,从最大递减到最小;每次都刷新一次界面;后面是设置其时间,动画为先慢然后一下变快消失掉,然后启动动画。
- 在开始动画方法中:我们同样在回调中除了我们的变量数据;在这里我们需要判断,如果是普通扩散,那么我们就扩散到对应的半径就OK,如果是Move 类型我们则需要变化其坐标。其公式为 C = A+(B-A)*T;而后设置透明度逐渐增加到最大,该透明度是用于全部区域非圆形区域。
触发方法
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(); } }
在触发方法中,我们分别需要判断是:取消/抬起/按下 操作。
- 在取消和抬起操作中 我们都进行了:变化按钮状态变量 isTouchReleased 为释放,而后判断是否结束动画,如果结束则触发淡出动画。
- 按下操作:计算出最长半径,其中 0.75 代表上面说的:75%;2.5代表的是上面说的 1.25倍,这里因为是一半,所以乘2 了;其是这一部分应该放在 onMeasure() 方法中。
- 而后我们设置 释放按钮变量 isTouchReleased 为 false,设置动画开始 isAnimatingFadeIn 为 true。得到点击坐标,设置透明度为0,然后进行一次取消,然后开始动画。
onDraw()
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。
其实现在来说该动画类,并不局限于Button,你可以随意的设置到你的控件上面,如TextView 也可以不是自定义的控件,Android 原生的也可以;只需要设置其中的3个方法回调也就OK;大家可以试试;然后把效果分别切换一下;个人感觉很棒的~
附件
算是分析完了,下面附上源码和我分析时画的一些图,辅助解释。
图
代码
========================================================
作者:qiujuer
开源库:Genius-Android
转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119
——学之开源,用于开源;初学者的心态,与君共勉!
========================================================