[Material Design] 教你做一个Material风格、动画的按钮(MaterialButton)

前段时间Android L 发布了,相信看过发布会了解过的朋友都为其中的 “Material
Design
” 感到由衷的惊艳吧!至少我是的。

在惊艳之余感到由衷的遗憾,因为其必须在 ”Android L“ 上才能使用,MD,郁闷啊。

之后便自己想弄一个点击动画试试,此念头一发不可收拾;干脆一不做二不休,就重写了一个 ”MaterialButton“ 控件出来。

在这里不讨论什么是 :“Material Design” 。

在这里将给大家分享一下我自己弄的 “Material Design” 风格的 ”MaterialButton
按钮动画实现。

预热一下:

上面的两张动画相信大家都看过吧?是不是挺不错的?反正我是觉得手机上有这样的动画是很爽的,比较手机是用来增加体验的。但是这些动画只能在Android L 才能体验到,对于现在国内的 Android 厂商的情况来看,估计谷歌出新的版本的时候我们就能用上这个 L 版本了。

下面给大伙看看我做的 “MaterialButton” 按钮:

效果还不错吧?好了开始开工了。

介绍一下我的工具:“Android Studio” 当然大家用其他也行。

第一步:新建项目(这个任意,自己捣鼓吧)

第二步:新建自定义控件:在java文件夹上右击选择自定义控件:

取个名字:“MaterialButton

现在来看看多了一个类(MaterialButton),一个布局文件
“sample_material_button”,一个属性文件 “attrs_material_button”

到这里第二步完成了。多了3个文件。

第三步:修改 “MaterialButton” 类:

分为几步走:删除示例代码,重新继承自
“Button” 类,复写 “onTouchEvent()” 方法。完成后的代码:

public class MaterialButton extends Button {
    public MaterialButton(Context context) {
        super(context);
        init(null, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.MaterialButton, defStyle, 0);
        a.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

}

是不是感觉干净多了?到此第三步完成了。

第四步:就是做实际的动画了,在这里需要给大家说说三个需要注意的东西:

1.点击事件响应,这个很好理解,在 “onTouchEvent()”
方法中完成,在该方法中我们需要完成的是点击后启动一个动画,同时需要获取到当时点击的位置。

2.动画,这里的动画不是放大动画而是属性动画,说实话 这个要说清楚还真不是一点点就能说清楚的事情。简单说就是在动画中可以控制一个属性的变化,而在这里来说就是在 “MaterialButton”
类中建立一个宽度和一个颜色的属性,然后在动画中控制这两个属性的变化。

3.属性的建立以及属性的变化区域确定问题。

首先建立两个属性:

    private Paint backgroundPaint;
    private float radius;
    private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {
        @Override
        public Float get(MaterialButton object) {
            return object.radius;
        }

        @Override
        public void set(MaterialButton object, Float value) {
            object.radius = value;
            //刷新Canvas
            invalidate();
        }
    };

    private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {
        @Override
        public Integer get(MaterialButton object) {
            return object.backgroundPaint.getColor();
        }

        @Override
        public void set(MaterialButton object, Integer value) {
            object.backgroundPaint.setColor(value);
        }
    };

两个属性对比一下可以发现在半径的属性 “set
操作中调用了 “invalidate()” 方法,该方法的作用是告诉系统刷新当前控件的 “Canvas”,也就是触发一次:“onDraw(Canvas
canvas)
” 方法。

然后复写 “onTouchEvent()
方法如下:

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //记录坐标
            paintX = event.getX();
            paintY = event.getY();
            //启动动画
            startAnimator();
        }
        return super.onTouchEvent(event);
    }

在该方法中,首先确定是否是点击下去的事件,然后记录坐标,并启动动画。

在启动动画方法 “startAnimator()” 方法中,我们这样写:

    private void startAnimator() {

        //计算半径变化区域
        int start, end;

        if (getHeight() < getWidth()) {
            start = getHeight();
            end = getWidth();
        } else {
            start = getWidth();
            end = getHeight();
        }

        float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;
        float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;

        //新建动画
        AnimatorSet set = new AnimatorSet();
        //添加变化属性
        set.playTogether(
                //半径变化
                ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),
                //颜色变化 黑色到透明
                ObjectAnimator.ofObject(this, mBackgroundColorProperty, new ArgbEvaluator(), Color.BLACK, Color.TRANSPARENT)
        );
        // 设置时间
        set.setDuration((long) (1200 / end * endRadius));
        //先快后慢
        set.setInterpolator(new DecelerateInterpolator());
        set.start();
    }

在这一步我们需要知道有些按钮并不是横向的,所以长不一定大于宽度,所以需要先判断获取到最长与最短,然后进行计算获取到开始的半径与结束的半径,这里有一个我的思路图:

我们知道在 Android 中都是以左上脚为圆心,然后右边为X正数,下边为Y正数。所以建立了如上坐标系。

蓝色矩形区域代表按钮,蓝色点代表点击的点。灰色矩形代表点击后的开始区域,然后4边开始扩散开;以上就是一个简单的原理。当然思路有些跳跃,如果不懂可以在下边评论我都会进行回复的。

第五步:画画,对就是画画;这一步就是利用上面的半径和画笔颜色进行实际的绘制。

这里需要了解的是:

1:画画是在:“onDraw(Canvas canvas)” 方法中完成

2:在画板(Canvas)上是分层级的,简单说就是先画背景然后画房子,然后画人,最后画人的一些小细节 自底向上的流程

3:画板每次画 都是新的画板,预示着你每次都需要从背景画起然后才到人;在编程中就是每次 “onDraw(Canvas canvas)” 方法中的画板(Canvas )都是新的(New)。

说了那么多其实很简单,因为复杂的都在上一步中完成了。 “onDraw(Canvas canvas)” 源码如下:

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.drawCircle(paintX, paintY, radius, backgroundPaint);
        canvas.restore();

        super.onDraw(canvas);
    }

在这里我们先保存了画板的状态,然后画一个圆,然后恢复上一次的状态,然后调用父类进行后面的绘制工作。

这里解释一下:

1.为什么 “super.onDraw(canvas)” 需要放在最后调用?

因为画板是分层级的,当调用 “super.onDraw(canvas)” 的时候进行的工作是绘制字体那些,如果放在前面调用那么造成的后果是我们的圆会覆盖到字体上面。所以我们需要先画圆背景。

2.为什么只有一次画圆操作(canvas.drawCircle())?

因为在半径属性中调用了 “invalidate()” ,当每次变化半径值的时候将进行一次 “onDraw(canvas)”
操作,也就画一次圆,在一定时间内快速重复画半径逐渐增大的圆的时候就形成了动画效果。

最后给出这次控件的代码:

public class MaterialButton extends Button {
    private Paint backgroundPaint;
    private float paintX, paintY, radius;

    public MaterialButton(Context context) {
        super(context);
        init(null, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public MaterialButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        final TypedArray a = getContext().obtainStyledAttributes(
                attrs, R.styleable.MaterialButton, defStyle, 0);
        a.recycle();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.save();
        canvas.drawCircle(paintX, paintY, radius, backgroundPaint);
        canvas.restore();

        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            //记录坐标
            paintX = event.getX();
            paintY = event.getY();
            //启动动画
            startAnimator();
        }
        return super.onTouchEvent(event);
    }

    private void startAnimator() {

        //计算半径变化区域
        int start, end;

        if (getHeight() < getWidth()) {
            start = getHeight();
            end = getWidth();
        } else {
            start = getWidth();
            end = getHeight();
        }

        float startRadius = (start / 2 > paintY ? start - paintY : paintY) * 1.15f;
        float endRadius = (end / 2 > paintX ? end - paintX : paintX) * 0.85f;

        //新建动画
        AnimatorSet set = new AnimatorSet();
        //添加变化属性
        set.playTogether(
                //半径变化
                ObjectAnimator.ofFloat(this, mRadiusProperty, startRadius, endRadius),
                //颜色变化 黑色到透明
                ObjectAnimator.ofObject(this, mBackgroundColorProperty, new ArgbEvaluator(), Color.BLACK, Color.TRANSPARENT)
        );
        // 设置时间
        set.setDuration((long) (1200 / end * endRadius));
        //先快后慢
        set.setInterpolator(new DecelerateInterpolator());
        set.start();
    }

    private Property<MaterialButton, Float> mRadiusProperty = new Property<MaterialButton, Float>(Float.class, "radius") {
        @Override
        public Float get(MaterialButton object) {
            return object.radius;
        }

        @Override
        public void set(MaterialButton object, Float value) {
            object.radius = value;
            //刷新Canvas
            invalidate();
        }
    };

    private Property<MaterialButton, Integer> mBackgroundColorProperty = new Property<MaterialButton, Integer>(Integer.class, "bg_color") {
        @Override
        public Integer get(MaterialButton object) {
            return object.backgroundPaint.getColor();
        }

        @Override
        public void set(MaterialButton object, Integer value) {
            object.backgroundPaint.setColor(value);
        }
    };
}

当然后续的工作还有:不同的颜色的按钮,按钮属性的问题;

这些我都在个人的项目中完成了,大家拿去试试:

Genius-Android

后面会分享点击后不是画圆,而是画圆角矩形条的动画实现。

时间: 2024-12-15 01:43:25

[Material Design] 教你做一个Material风格、动画的按钮(MaterialButton)的相关文章

[Material Design] 教你做一个Material风格、动画的按钮

前段时间Android L 发布了,相信看过发布会了解过的朋友都为其中的 “Material Design” 感到由衷的惊艳吧!至少我是的. 在惊艳之余感到由衷的遗憾,因为其必须在 ”Android L“ 上才能使用,MD,郁闷啊. 之后便自己想弄一个点击动画试试,此念头一发不可收拾:干脆一不做二不休,就重写了一个 ”MaterialButton“ 控件出来. 在这里不讨论什么是 :“Material Design” . 在这里将给大家分享一下我自己弄的 “Material Design” 风格

手把手教你打造一个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来实现抽屉导航. 通过下面的两个链接

手把手教你做一个吸引人的购物网站

购物网站盈利能力相信很多用户都是有目共睹的,因此不少的中小企业对购物网站的建设也是趋之若鹜,怎么企业设计购物网站有什么方法能够为购物网站提高人气呢?下面看看凡科网站建设带来的一些分析. 要对用户的跟随心理进行分析.无论是实体销售还是线上的销售,用户都会有一种莫名的跟随心理.网上购物网站的评论就好想是生活中的口口相传,购买过的用户可以对产品进行评论,这样可以给潜在用户一个引导作用. 企业要对购物网站网页的每一个角落都要发挥极限.企业都知道网页的每一个角落都是有用的,购物网站也一样.一个列表页不会是

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

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

Android应用系列:手把手教你做一个小米通讯录(附图附源码)

前言 最近心血来潮,突然想搞点仿制品玩玩,很不幸小米成为我苦逼的第一个试验品.既然雷布斯的MIUI挺受欢迎的(本人就是其的屌丝用户),所以就拿其中的一些小功能做一些小demo来玩玩.小米的通讯录大家估计用过小米的都清楚是啥子样的,没用过小米的也别着急,瞧瞧我的demo,起码也有七八分相似滴.先上图看效果 我是图: PS:吐槽一下,博客园上个图真难,所以搞了个短点的gif上才没失败....唉... 在这里仅仅是实现了逻辑交互的效果,并没有点击打电话的功能,因为也不难就懒得加了... 分析 我们说说

Servlet手把手教你做一个初级程序

[背景需求] 绘制一个界面,界面包含姓名,工资,年龄以及提交按钮. 将用户的基本信息填入后,按提交按钮,通过Servlet机制,将提交结果反馈给用户,同时将填入的用户信息写入到数据库中. [基本环境] 开发环境:myeclipse集成Tomcat OS:windows 7(64bit) 数据库:mysql [步骤] 1.创建数据库并建立用户表(emp) 打开mysql用户界面,并登录进mysql后(需要使用root账户进入),做如下操作: 1)创建数据库 create database EMPD

教你做一个牛逼的DBA(在大数据下)

一.基本概念 大数据量下,搞mysql,以下概念需要先达成一致 1)单库,不多说了,就是一个库 2)分片(sharding),水平拆分,用于解决扩展性问题,按天拆分表 3)复制(replication)与分组(group),用于解决可用性问题 4)分片+分组,这是大数据量下,架构的实际情况 二.大数据量下,mysql常见问题及解决思路 1)常见问题 如何保证可用性? 各色各异的读写比,怎么办? 如何做无缝倒库,加字段,扩容? 数据量大,怎么解决? 2)解决思路 2.1)可用性解决思路:复制 读库