文档章节

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

Qiujuer
 Qiujuer
发布于 2014/10/07 15:28
字数 2246
阅读 1790
收藏 63
点赞 8
评论 9

原创作品,转载请注明出处:http://blog.csdn.net/qiujuer/article/details/39831451

前段时间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);  
        }  
    };  
}




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

介于大家可能没有 Android Studio 无法看到效果,特意把 Apk 上传了,如果Eclipse不知道怎么导入的话 就加我QQ,我给你说一下!

地址:APK

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

Genius-Android


进阶:[Material Design] MaterialButton 效果进阶 动画自动移动进行对齐效果


© 著作权归作者所有

共有 人打赏支持
Qiujuer

Qiujuer

粉丝 139
博文 25
码字总数 60206
作品 2
深圳
程序员
加载中

评论(9)

Qiujuer
Qiujuer

引用来自“小心小新”的评论

values文件写啥啊?
Values 里边就是一些颜色和大小而已。自己设置一个就好。
小心小新
values文件写啥啊?
王洋cYx
王洋cYx
好啊,明天看看
Qiujuer
Qiujuer

引用来自“魏曼奇”的评论

对于这种钻研精神,很赞!79
Thanks#Genius-Android#
魏曼奇
魏曼奇
对于这种钻研精神,很赞!79
Qiujuer
Qiujuer

引用来自“mcloud”的评论

不错,官方后期可能释出support包来支持低版本系统吧
这个倒是有可能会出现这样的情况,不过在官方发布之前可以自己捣鼓一下也是不错的。 谢谢支持。
mcloud
mcloud
不错,官方后期可能释出support包来支持低版本系统吧
Qiujuer
Qiujuer

引用来自“南湖船老大”的评论

果然很炫酷
谢谢啊 以后会有更多的效果 这个是最基本的。
南湖船老大
南湖船老大
果然很炫酷
Android UI框架(中)

唯美清新的switch按钮 优雅的switch状态按钮 通用的引导页和轮播器 仿微信图片选择器 快捷设置沉浸式状态栏 复杂表格布局 最全的开源集合 阻尼效果的界面 炫酷的纸飞机下拉刷新控件 仿ios水滴...

Moosphon ⋅ 2017/12/31 ⋅ 0

【学习笔记】Android 5.X UI 设计初识

Android 5.x系列开始使用新的设计风格Material Design来统一整个Android系统的界面设计风格。 1、材料的形态模拟 材料的形态模拟是Material Design中最核心也是改变最大的一个设计,Google通...

小树coding ⋅ 2016/02/23 ⋅ 0

创建Material Design风格的Android应用--应用主题

昨天正式发布了android 5,同时android developer网站也更新了,增加了创建Material Design风格的Android应用指南,也更新了Support Library,在support library增加了一些Material Design风格...

码农明明 ⋅ 2014/10/19 ⋅ 8

Android 5.X的新特性及Material Design

导语 说来惭愧,Android8.0奥利奥都出了,我还在学5.0的新特性,看来得再努把力了,Material Design动画确实好看,查看实例戳这里。 主要内容 Android5.X UI设计初步 Palette 视图与阴影 Ti...

一个有故事的程序员 ⋅ 2017/10/24 ⋅ 0

初识Material-UI

简介 Material-UI是一组实现了谷歌Material Design设计原则的React组件集合,江湖传言使用Material-UI可以使我们的页面颜色更鲜艳,动画效果更突出(符合Material Design的设计风格),那么接...

桂圆_noble ⋅ 2016/06/23 ⋅ 0

在低版本android系统上实现Material设计应用

Material Design真的很好看,动画效果真的很实用。前面也写了一些文章介绍如何编写Material风格的程序,但是很多都是一些新的api,低版本上面没有这些api,我们没办法使用。但是不用气馁,g...

码农明明 ⋅ 2014/11/17 ⋅ 12

2016年最流行的Android组件、工具、框架大全

首发链接:2016年最流行的Android组件、工具、框架大全 Android 是目前最流行的移动操作系统之一。 随着新版本的不断发布, Android的功能也日益强大, 涌现了很多流行的应用程序, 也催生了...

xhmj12 ⋅ 2016/12/12 ⋅ 0

探索 Android Design Support Library v28 新增内容

[译] 探索 Android Design Support Library v28 新增内容 Android Support Library v28 版本最近被宣布推出 -- 在当前的 alpha 版本中, 我们又有了一系列令人兴奋的新组件. 在这篇文章中, 我...

TonnyL ⋅ 04/26 ⋅ 0

Android最佳的开源库(四)

网络 OkHttp:Android的HTTP客户端库。 AndroidAsync:异步通信库。 通知推送 PubNub:用来处理推送通知的通信服务。 Gandalf:给用户发送更新或维护消息库。 支付 Android In-App Billing v...

博为峰教研组 ⋅ 2016/11/15 ⋅ 0

Android-UI控件

手摸手教你写 Slack 的 Loading 动画 四步实现: 画布旋转及线条变化动画(Canvas Rotate Line Change) 画布旋转动画(Canvas Rotate) 画布旋转圆圈变化动画(Canvas Rotate Circle Change...

掘金官方 ⋅ 01/12 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

SAS笔记-宏2

宏是一种文本,一般来说其编译是在程序执行之前。 宏变量的创建 %let语句 %let macro_variables = text; %let是常见的宏变量建立方式,其编译就在执行前。如下例中,想要宏变量test等于数据集...

tonorth123 ⋅ 9分钟前 ⋅ 0

如何使用serverchan微信推送告警

之前实现推送告警信息到微信的方法有如下几种: 1、通过企业公众号实现----收费: 2、通过QQ邮箱,在微信平台上开启收到邮件进行提醒; 3、第三方告警平台API,一般也是收费的; 不过最近看文...

问题终结者 ⋅ 36分钟前 ⋅ 0

TCP的RPC

RPC就是远程方法调用(Remote Process Call ),包含了客户端和服务端,涉及了对象的序列化传输。 1.服务端启动,注册远程调用的类2.客户端发送请求信息包含类、方法、参数的一些信息、序列化传...

Cobbage ⋅ 56分钟前 ⋅ 0

IOS-UI UI初步代码布局添加事件

ISO开发界面,UI是必须学习的一部分,其实很早之前想学来了,一直没有沉下心来学习。看到IOS的代码风格和布局就别扭的不行,跟java代码和android布局比较显得不是那么方便,所以一直到现在。...

京一 ⋅ 今天 ⋅ 0

浅谈OpenDaylight的二次开发

OpenDaylight作为一款开源SDN网络控制器,依托于强大的社区支持以及功能特性,成为了目前主流的SDN网络控制器开发平台。在比较稳定的OpenDaylight Helium版本中,已经为开发者提供了大量的网...

wangxuwei ⋅ 今天 ⋅ 0

API 开发中可选择传递 token 接口遇到的一个坑

在做 API 开发时,不可避免会涉及到登录验证,我使用的是jwt-auth 在登录中会经常遇到一个token过期的问题,在config/jwt.php默认设置中,这个过期时间是一个小时,不过为了安全也可以设置更...

等月人 ⋅ 今天 ⋅ 0

Java NIO之文件处理

程序要操作本地操作系统的一个文件,可以分为以下三个部分: 对文件位置的操作 对文件的操作 对文件内容的操作 其中,对文件内容的操作在 Java NIO之Channel 中已经有了介绍,通过FileChann...

士别三日 ⋅ 今天 ⋅ 0

Maven的pom.xml配置文件详解

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.......

小海bug ⋅ 今天 ⋅ 0

解决httpclient超时设置不生效的问题

最近公司有项目需要通过http调用第三方服务,且第三方服务偶有超时,故需要设置一定的超时时间防止不响应的情况出现。 初始设置如下: [java] view plain copy //超时设置 RequestConfig re...

Mr_Tea伯奕 ⋅ 今天 ⋅ 0

过滤器Filter和拦截器HandlerInterceptor

过滤器 依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,但是缺点是一个过滤器实例只能在容器初始化时调用一次。使用过滤器的目的是用来做一些过滤操作,获取我们想要...

hutaishi ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部