打造极致Material Design动画风格Button
Posted Qiujuer
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了打造极致Material Design动画风格Button相关的知识,希望对你有一定的参考价值。
========================================================
作者:qiujuer
博客:blog.csdn.net/qiujuer
网站:www.qiujuer.net
开源库: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;大家可以试试;然后把效果分别切换一下;个人感觉很棒的~
附件
算是分析完了,下面附上源码和我分析时画的一些图,辅助解释。
图
代码
==============更新分割线========================================
更新日期:2015-01-10
今天在二次看代码并优化的时候发现一个错误的地方,在此修正一下;对于给大家带来的不便还请谅解;不过也不影响大局的。
就是在上面中,Ripple 扩散模式下的一个关于其最大半径的运算上的问题。
if (mTouchEffect == TouchEffect.Move)
mMaxRadius *= 0.75;
else
mMaxRadius *= 2.5;
在这里犯了一个数学的错误以及一个体验上的不够细腻的地方:
- 首先说说不够细腻的地方,在上面中关于其最大半径,在上面我说了是直接采用控件的对角线进行获取其最大半径;这里其是不应该采用对角线;因为用户通常情况下不会点击最边缘的地方,而是靠中60%左右区域;所以如果其最大半径取对角线将会有很大的浪费;应该根据具体的点击情况;判断出位置算出其距离最远的点;然后算出其半径。
- 然后就是数学上的错误,在这里我采用的 2.5倍;也解释了就是1.25*2得来;而1.25是勾三股四玄五中,得出的玄五是股四的1.25倍;但是其实一个按钮控件常常并不是满足这样的特殊情况;而我以偏概全的方式采用了1.25倍来计算;其是应该是(A的平方+B的平方)然后开根号 得到C值。
如上图,定点(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
——学之开源,用于开源;初学者的心态,与君共勉!
========================================================
以上是关于打造极致Material Design动画风格Button的主要内容,如果未能解决你的问题,请参考以下文章
[Material Design] 打造简单朴实的CheckBox
开发Google Material Design风格的WPF程序
如何在 WPF 中禁用 Material Design 风格