自定义Switch过程详解

作者: remcarpediem

联系方式:segmentfaultcsdn简书

本文转载请注明作者、文章来源,链接,版权归作者所有。

?前段时间,我看到了一篇关于Android动画的文章Android View 仿iOS SwitchButton Material Design,十分喜欢文章作者的笔风,可惜每个人的笔风都不同,不过我倒是实现了一个类似的Switch组件,项目地址为https://github.com/ztelur/FunSwitch,就用这篇文章来讲述一下实现过程和机制吧。

简介

?我的自定义Switch是模仿github上的LLSwitch,其UI设计来源于Dribbble,链接摸我,其效果图如下

自定义View需要重载的函数

?我们都知道以View为父类来自定义视图需要重载一系列函数,下面我们就来按照调用顺序来介绍一下这些函数。需要重载的函数列表如下:

  • onMeasure
  • onSizeChanged
  • onDraw
  • onTouchEvent
  • onSaveInstanceState
  • onRestoreInstanceState

?首先就是onMeasure函数,用于确定自定义视图的长和高。对于本文的Switch,我们让其高为宽的固定比例大小就可以了,所以重构函数实现得十分简单。这个函数确定的只是测量的长和高,并不是最终视图所显示的长和高

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = (int) (width * DEFAULT_WIDTH_HEIGHT_PERCENT);
    setMeasuredDimension(width,height);
}

?然后就是视图确定真正大小(onLayout)之后要调用的onSizeChanged函数了。这个函数调用之后,draw函数就可能被调用,所以,一般我们在这个函数中计算绘制时所需要的数据。

?接着是draw函数,在这个函数中,我们绘制各种图像来构成视图的UI。需要注意的是,这个函数会被频繁的调用,所以不要在函数内执行耗时的操作。

?最后是onTouchEvent函数,这个函数是用户触摸屏幕时才会被调用的,主要进行视图的触摸处理,由于我们的自定义Switch支持的触摸事件比较简单,只是支持点击事件,所以此函数的实现也比较简单。

?最后就是涉及到视图状态保存的两个函数。我们都知道,一定情况下,activity会被销毁,然后重新建立,比如你旋转屏幕时。这个时候,你需要保存视图的一些属性数据,以备重新建立视图时使用,来恢复之前的视图。你需要注意的是,光重载这两个函数还是不够的,还需要设置View ID和调用setSaveEnabled函数

?我们接下来就一步一步的来实现这个自定义组件吧。

田径场式背景

?我们先来看一下这个Switch的背景,它是一个形如田径场跑道的形状,由两个半圆和一个矩形组成,我们先来看一下如何来绘制出这样的图案。我们使用Path来构造出这样的图案,然后再进行绘制,代码如下所示:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        //TODO:还有padding的问题偶!!!
        mWidth = w;
        mHeight = h;
        float top = 0;
        float left = 0;
        float bottom = h*0.8f; //下边预留0.2空间来画阴影
        float right = w;

        RectF backgroundRecf = new RectF(left,top,bottom,bottom);
        mBackgroundPath = new Path();
        //TODO:???????????
        mBackgroundPath.arcTo(backgroundRecf,90,180);

        backgroundRecf.left = right - bottom;
        backgroundRecf.right = right;
        mBackgroundPath.arcTo(backgroundRecf,270,180);
        mBackgroundPath.close();
        ........
    }

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

    private void drawBackground(Canvas canvas) {
        mPaint.setColor(mCurrentColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(mBackgroundPath,mPaint);
        mPaint.reset();
    }

?我们使用arcTo(RectF oval, float startAngle, float sweepAngle)这儿函数来绘制田径场图案。这个函数,需要传入一个RectF对象,将要绘制的圆是这个对象所代表矩形的内切圆,我们只要计算出来这个矩形的上下左右四点的坐标就可以了。我们先计算绘制左侧半圆所需要的矩形,然后函数后两个参数为90,和180。注意的是,这个函数中,角度的正方向是顺时针的,startAngle为90,也就是我们数学坐标系中角度为270所代表的方向。

?由于Path会自动连接绘制个点之间的连线,所以,我们只需要再绘制出右侧半圆的曲线即可。

?我们只需要将绘制左侧圆曲线的矩形进行一定距离的平移,就可以绘制出右侧曲线。所以矩形的右边界就等于整个视图的right,由于矩形的长为bottom,所以矩形的左边界就为right-bottom。然后再次调用arcTo函数,这次的起始角度就变成270了。

?最后调用Pathclose函数,让上边画的两段圆弧连接起来,就形成了上述的田径场图案。

绘制脸部图形

?笑脸图案看似复杂,其实就是几个图形组合在一起。首先是一个大圆,然后是里边的两个椭圆型的眼睛,然后是嘴巴。我们只要在正确的位置将这些图形绘制出来即可。

?和绘制背景图形的顺序类似,我们首先在onSizeChanged函数中进行相关函数的计算。

?首先是大圆脸的绘制,我们还是使用drawPath函数,只不过这次Path对象只绘制一个圆;而双眼则是使用drawOval函数来花椭圆;最后使用drawRect来绘制矩形。

Switch动画

?我们仔细查看自定义Switch的动画效果,可以发现,主要涉及三部分的动画效果:

  • 背景颜色动画转变。
  • 脸部图形的平移和转动(可以看出相当于脸部水平转动了360度)。
  • 脸部表情动画,眨眼睛和嘴巴咧开。

?由于动画涉及的操作比较多,所以我们选择使用ValueAnimator+AnimatorUpdateListener的动画实现方式,在onAnimationUpdate函数中记录下来当前的animatedValue,然后调用invalidate函数来让界面重绘,在绘制界面计算数据过程中,使用记录下来的数值,从而产生动画效果。

    private void startCloseAnimation() {
        mValueAnimator = ValueAnimator.ofFloat(NORMAL_ANIM_MAX_FRACTION,0);
        mValueAnimator.setDuration(mOffAnimationDuration);
        mValueAnimator.addUpdateListener(this);
        mValueAnimator.addListener(this);
        mValueAnimator.setInterpolator(mInterpolator);
        mValueAnimator.start();
        startColorAnimation();
    }
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        mAnimationFraction = (float)animation.getAnimatedValue();
        invalidate(); //产生动画的关键步骤
    }

?所以,最终动画问题又变成了绘制静态图像问题,我们根据不同的mAnimationFraction的值来绘制不同的图像。

?接下来我们就来描述一下几个比较关键的动画的逻辑。

脸部转动动画

?其实这个脸部动画还是比较难实现的,主要是转动的这个效果没有直接的API可以实现。我们的动画只是让用户产生脸部转动的假象。由于脸部图案就是一个大圆加上充当眼睛和嘴巴的椭圆和矩形,我们可以让眼睛和嘴巴向转动方向平移,让它们平移出大圆,然后在一定时间后从另外一个方向再平移进入大圆,最终回到原来位置。这样就实现了一种脸部转动的效果。

?如何让眼睛和嘴巴移动到大圆边缘就消失呢?而且是随着移动渐渐的一部分一部分的消失呢?我们这里使用了另外一种思路,使用clipPath函数,将画布进行裁剪,只留下大圆范围内的图案。这样的话,当眼睛和嘴巴移动出大圆时,就会逐渐消失。

?至于眼睛和嘴巴如何平移呢?大家首先想到的方法一定是根据mAnimationFraction来计算它们的位置,然后在相应位置上将它们绘制出来,但是这样不是最优的方法,我们可以在绘制这些图像时,对画布进行平移,这样的话,我们绘制眼睛和嘴巴的函数就不会涉及到mAnimationFraction,实现比较简单。

public void drawFace(Canvas canvas,float fraction) {
        mPaint.setAntiAlias(true);
        //面部背景
        mPaint.setColor(mFaceColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawPath(mFacePath,mPaint);
        //先裁剪并平移画布,然后再绘制眼部五官
        translateAndClipFace(canvas,fraction);
        drawEye(canvas,fraction);
        drawMouth(canvas,fraction);
    }

    private void translateAndClipFace(Canvas canvas,float fraction) {
        //截掉超出face的部分。
        canvas.clipPath(mFacePath);

        float faceTransition ;
        //TODO:合理的转动区间,眼睛出现和消失的时间比为1:1,所以当fraction=0.25时,应该只显示侧脸,计算faceTransition。
        if (fraction >=0.0f && fraction <0.5f) {
            faceTransition = fraction * mFaceRadius *4;
        } else if (fraction <=NORMAL_ANIM_MAX_FRACTION){
            faceTransition = - (NORMAL_ANIM_MAX_FRACTION - fraction) * mFaceRadius * 4;
        } else if (fraction <=(NORMAL_ANIM_MAX_FRACTION+FACE_ANIM_MAX_FRACTION)/2) {
            faceTransition =  (fraction - NORMAL_ANIM_MAX_FRACTION) * mFaceRadius * 2;
        } else {
            faceTransition = (FACE_ANIM_MAX_FRACTION - fraction) * mFaceRadius * 2;
        }
        canvas.translate(faceTransition,0);
    }

眨眼睛和变笑脸动画

?眨眼睛动画十分简单,我们只要在绘制眼睛之前对画布进行缩放即可,然后在绘制玩眼睛之后,在将画布转变回来。但是后来我发现,画布缩放的中心点不容易确认,所以,采取了使用mAnimationValue计算椭圆数据的方式来进行椭圆大小的缩放。

?变笑脸动画主要就是嘴巴的动画效果,在静止情况下,我们使用drawRect来绘制嘴部图形;但在动画过程中,我们使用drawPathquadTo来共同绘制嘴巴形状。

?PathquadTo是用来绘制贝塞尔曲线,具体使用方法请查看Path之贝塞尔曲线。我们主要使用其二阶曲线版本,即两个数据点,一个控制点。我们计算出A,B这两个数据点,也就是静止状态下矩形的左上点和右上点,然后根据mAnimationValue来计算控制点c的坐标,然后完成绘制。

?嘴部图案的绘制如下所示。

    private void drawMouth(Canvas canvas,float fraction) {
        .......
        //嘴巴
        if (fraction <=0.75) { //
            canvas.drawRect(mouthLeft, mouthTop, mouthLeft + mouthWidth, mouthTop + mouthHeight, mPaint);
        } else {
            Path path = new Path();
            path.moveTo(mouthLeft,mouthTop);
            float controlX = mouthLeft + mouthWidth/2;
            float controlY = mouthTop + mouthHeight + mouthHeight * 15 * (fraction - 0.75f);
        path.quadTo(controlX,controlY,mouthLeft+mouthWidth,mouthTop);
            path.close();
            canvas.drawPath(path,mPaint);
        }
    }

总结

?其实还有一些细节问题我没有在这篇文章上讲出,一方面是因为讲述起来太过复杂,还是大家自己查阅代码比较好,另一方面是,我觉得自己实现的方式也不是很好,就不在这里献丑了。

?项目还没有完全完成,比如自定义监听器和自定义属性的相关逻辑都没有添加,希望感兴趣的同学可以自行研究代码并完善它。项目地址摸我我的github

时间: 2024-10-27 10:53:50

自定义Switch过程详解的相关文章

自定义转场详解(一)

前言 本文是我学习了onevcat的这篇转场入门做的一点笔记. 今天我们来实现一个简单的自定义转场,我们先来看看这篇文章将要实现的一个效果图吧: 过程详解 热身准备 我们先创建一个工程,首先用storyboard快速的创建两个控制器,一个作为主控制器,叫ViewController,另外一个作为present出来的控制器,叫PresentViewController,并且用autoLayout快速搭建好界面.就像这样: 我们先做好点击ViewController上面的按钮,present出 Pr

Nginx实现集群的负载均衡配置过程详解

Nginx实现集群的负载均衡配置过程详解 Nginx 的负载均衡功能,其实实际上和 nginx 的代理是同一个功能,只是把代理一台机器改为多台机器而已. Nginx 的负载均衡和 lvs 相比,nginx属于更高级的应用层,不牵扯到 ip 和内核的修改,它只是单纯地把用户的请求转发到后面的机器上.这就意味着,后端的 RS 不需要配置公网. 一.实验环境 Nginx 调度器 (public 172.16.254.200 privite 192.168.0.48)RS1只有内网IP (192.168

uboot主Makefile分析(t配置和编译过程详解)

1.编译uboot前需要三次make make distcleanmake x210_sd_configmake -j4 make distclean为清楚dist文件. make x210_sd_config  跳转执行mkconfig用来配置并生成config.mk(board/samsung/x210目录下为指定链接地址的与主uboot目录的config.mk不同) autuconfig.mk 2.框图 3.uboot主Makefile分析 3.1.uboot version确定(Make

Hadoop MapReduce执行过程详解(带hadoop例子)

https://my.oschina.net/itblog/blog/275294 摘要: 本文通过一个例子,详细介绍Hadoop 的 MapReduce过程. 分析MapReduce执行过程 MapReduce运行的时候,会通过Mapper运行的任务读取HDFS中的数据文件,然后调用自己的方法,处理数据,最后输出.Reducer任务会接收Mapper任务输出的数据,作为自己的输入数据,调用自己的方法,最后输出到HDFS的文件中.整个流程如图: Mapper任务的执行过程详解 每个Mapper任

Linux(RHEL6)启动过程详解

Linux(红帽RHEL6)启动过程详解: RHEL的一个重要和强大的方面是它是开源的,并且系统的启动过程是用户可配置的.用户可以自由的配置启动过程的许多方面,包括可以指定启动时运行的程序.同样的,系统关机时所要终止的进程也是可以进行组织和配置的,即使这个过程的自定义很少被需要. 理解系统的启动和关机过程是如何实现的不仅可以允许自定义,而且也可以更容易的处理与系统的启动或者关机相关的故障.  1.启动过程  以下是启动过程的几个基本阶段:   ① 系统加载并允许boot loader.此过程的细

View绘制过程详解

View绘制过程详解 界面窗口的根布局是DecorView,该类继承自FrameLayout.说到View绘制,想到的就是从这里入手,而FrameLayout继承自ViewGroup.感觉绘制肯定会在ViewGroup或者View中, 但是木有找到.发现ViewGroup实现ViewParent接口,而ViewParent有一个实现类是ViewRootImpl, ViewGruop中会使用ViewRootImpl- /** * The top of a view hierarchy, imple

uboot配置和编译过程详解【转】

本文转载自:http://blog.csdn.net/czg13548930186/article/details/53434566 uboot主Makefile分析1 1.uboot version确定(Makefile的24-29行) Makefile代码部分: [plain] view plain copy VERSION = 1 PATCHLEVEL = 30 SUBLEVEL = 4 EXTRAVERSION = U_BOOT_VERSION = $(VERSION).$(PATCHL

下拉刷新-过程详解

Android的ListView是应用最广的一个组件,功能强大,扩展性灵活(不局限于ListView本身一个类),前面的文章有介绍分组,拖拽,3D立体,游标,圆角,而今天我们要介绍的是另外一个扩展ListView:下拉刷新的ListView.    下拉刷新界面最初流行于iphone应用界面,如图:     然后在Android中也逐渐被应用,比如微博,资讯类.    所以,今天要实现的结果应该也是类似的,先贴出最终完成效果,如下图,接下来我们一步一步实现. 1. 流程分析    下拉刷新最主要

Hadoop学习之MapReduce执行过程详解

转自:http://my.oschina.net/itblog/blog/275294 分析MapReduce执行过程 MapReduce运行的时候,会通过Mapper运行的任务读取HDFS中的数据文件,然后调用自己的方法,处理数据,最后输出.Reducer任务会接收Mapper任务输出的数据,作为自己的输入数据,调用自己的方法,最后输出到HDFS的文件中.整个流程如图: Mapper任务的执行过程详解 每个Mapper任务是一个java进程,它会读取HDFS中的文件,解析成很多的键值对,经过我