打造极致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;至少在我看来与官方的相当接近了。

效果

个人

官方

可以看出其基本上差不多了。

分析

首先我们来解析一下官方的:

在这里我截取了最后一个按钮相应的连续几张图片的情况,从图片我们可以看出以下情况:

  • 官方也是采用圆形水波,非圆角矩形水波(这个与我最开始所想不太一样)
  • 其扩散速度逐渐递减,圆心的时候基本一闪就过
  • 圆形波纹颜色一直没有变化
  • 控件按钮整体背景色逐渐加深
  • 点击位置在右下角,但是从扩散情况来看其水波圆心逐渐向按钮控件中心靠拢
  • 这些也就是我们需要实现的部分。

实现原理

我们第二张中的按钮之所以有很大的差距我总结出以下几点:

  1. 中心靠拢的速度控制上不对
  2. 整体的减速 Interpolator 类设置不对,虽然同样是减速,但是可以看出官方的起步很快,而后递减很慢,这个可以通过初始化的时候传入 Interpolator 参数解决
  3. 水波颜色控制不对,颜色应该不变化,变化的是背景色的颜色
  4. 没有背景色变化的过程,这个过程需要添加,同时这里有一个细节,其最后的颜色并没有加到最深,大约相当于波纹颜色的80%左右
  5. 没有考虑圆角情况,在第二章中如果控件是圆角,其波纹将会超出圆角而后消失。

代码

不知道你们在做的过程中是否想过,我们的动画是在用户点击 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();
        }
    }

在触发方法中,我们分别需要判断是:取消/抬起/按下 操作。

  1. 在取消和抬起操作中 我们都进行了:变化按钮状态变量 isTouchReleased 为释放,而后判断是否结束动画,如果结束则触发淡出动画。
  2. 按下操作:计算出最长半径,其中 0.75 代表上面说的:75%;2.5代表的是上面说的 1.25倍,这里因为是一半,所以乘2 了;其是这一部分应该放在 onMeasure() 方法中。
  3. 而后我们设置 释放按钮变量 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

博客:blog.csdn.net/qiujuer

网站:www.qiujuer.net

开源库:Genius-Android

转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119

——学之开源,用于开源;初学者的心态,与君共勉!

========================================================

时间: 2024-10-13 12:26:52

打造极致Material Design动画风格Button的相关文章

手把手教你打造一个Material Design风格的App(二)

--接上文. 3.1添加ToolBar(ActionBar) 添加ToolBar非常简单,你需要做的仅仅是为toolbar创建一个单独的layout布局,如果你想在哪里展示toolbar,只要在对应布局里将toolbar的布局文件include进来即可. (8)在res-->layout文件夹下创建一个名为toolbar.xml的文件,然后在里面添加一个android.support.v7.widget.Toolbar元素,这样就创建了一个具有特定高度和主题的toolbar. toolbar.x

手把手教你打造一个Material Design风格的App(三)

--接上文. 3.2添加抽屉导航 添加导航抽屉跟Android 5.0之前是一样的,只是以前我们使用ListView来作为菜单容器,现在我们则使用Material Design风格的RecyclerView. (14)在你工程的java文件夹中,创建3个名为activity.adapter.model的包,将MainActivity.java移到activtiy包中,这样做使得你的代码可以很好地组织和管理. (15)打开位于app模块下的build.gradle文件并添加如下依赖.添加完依赖之后

手把手教你打造一个Material Design风格的App(一)

你应该听说过Android的Material Design,它是在Android 5.0(Lollipop)版本引入的.在Material Design中还引入了很多新东西,比如Material Theme,新的小部件,自定义的阴影,矢量图片及自定义动画等.如果你之前没有用过Material Design,那么本文将是一个很好的入门教程. 在这篇教程中,我们将会学习Material Design开发的基本步骤,即编写自定义的主题以及使用RecyclerView来实现抽屉导航. 通过下面的两个链接

Android动画最新最全详解包含Material Design动画

以前写动画也是零零种种,需要的时候就查API或找现成的,不够系统.现在通过学习将Android整个动画体系勾勒出来,做到有的放矢. 安卓框架提供了2个动画系统:属性动画(Android 3.0)和View动画.这两种动画系统都是可行的,但是,在一般情况下,属性动画是首选的方法,因为它是更灵活,提供更多的功能.除了这两个系统,你可以利用Drawable动画,它允许你一帧一个的加载显示Drawable资源.所以总体来说Android API提供了三类的动画: - Tween动画或View动画(API

Android Material Design动画

最近在看一些关于Material Design的东西,还记得在博客<你所不知道的Activity转场动画--ActivityOptions>中,我们介绍了一种优雅的activity过度动画.如果大家看了最后给出的参考链接,会发现还有很多内容是值得我们学习的,所以这篇博客,我们来学习一下这一页上剩下的东西. 一.触摸反馈 大家都知道,在Material Design中,触摸反馈的效果非常绚丽,是一种涟漪的效果,令我们高兴的是,这种效果也是可以自定义的. <LinearLayout xmln

material design动画

这是一篇material design 文档动画部分的学习! Summary: Material Design动画交互 动画速度的3个原则 3种交互方式 如何设计有意义的动画 使人高兴的动画细节 1 | Material Design动画交互 谷歌上一代设计语言是卡片设计,而这一代作为卡片的延伸,Material Design 以纸片与墨水作为灵感,由纸片与墨水组成的设计隐喻贯穿整个material design 的所有细节,动画设计也不例外.具体体现在哪?客官不急,听我一一道来: 首先,动画设

手把手教你打造一个Material Design风格的App(四)

--接上文. 3.3实现导航抽屉菜单项的选择 尽管导航抽屉已经实现了,但是你会发现选择抽屉列表项并没有反应,这是因为我们还没有实现RecycleView items的点击监听. 因为我们在导航抽屉里有3个菜单(Home,Friends & Messages),所以需要为每一个菜单项创建一个独立的Fragment. (24)在res-->layout里面,创建一个名为fragment_home.xml的文件并添加如下代码. fragment_home.xml <RelativeLayou

Android群英传笔记——第十二章:Android5.X 新特性详解,Material Design UI的新体验

Android群英传笔记--第十二章:Android5.X 新特性详解,Material Design UI的新体验 第十一章为什么不写,因为我很早之前就已经写过了,有需要的可以去看 Android高效率编码-第三方SDK详解系列(二)--Bmob后端云开发,实现登录注册,更改资料,修改密码,邮箱验证,上传,下载,推送消息,缩略图加载等功能 这一章很多,但是很有趣,也是这书的最后一章知识点了,我现在还在考虑要不要写这个拼图和2048的案例,在此之前,我们先来玩玩Android5.X的新特性吧!

MATERIAL DESIGN学习笔记

一.核心思想 ,aterial design的核心思想,就是把物理世界的体验带进屏幕.去掉现实中的杂质和随机性,保留其最原始纯净的形态.空间关系.变化与过渡,配合虚拟世界的灵活特性,还原最贴近真实的体验,达到简洁与直观的效果. Material design是最重视跨平台体验的一套设计语言.由于规范严格细致,保证它在各个平台使用体验高度一致.不过目前还只有Google自家的服务这么做,毕竟其他平台有自己的规范与风格. 二.材质与空间 材质 Material design中,最重要的信息载体就是魔