Android自定义View分享——打钩动画

来源:互联网 发布:幼儿教师网络研修计划 编辑:程序博客网 时间:2024/06/06 09:01

写在前面

这是笔者在学习Android自定义View以来,分享的第五篇不太复杂但是“长的”还算可以的View效果。之前分享过一篇动态时钟效果的自定义View,如果有兴趣的可以看看:
Android自定义View分享——一个时钟

这是今天要做的效果,没图没真相:
动画演示

需求分析

首先大方向分成两个:选中/未选中状态。未选中状态很简单,静态的,画一个空心圆,一个小钩就可以了,小钩可以用Path来实现。下面主要说说动态的选中状态。

  1. 绘制弧线:这是一个动态的过程,所以是不断重绘,并且不断增大弧线扫过的角度,直至360°。
  2. 变小的白色圆:当弧线扫满360°,在一个彩色实心圆的背景下,有一个半径不断变小的白色的圆。所以实现的方式是,先绘制一个彩色实心圆,然后再绘制白色圆,当然还是通过不断重绘实现动画效果,在重绘的同时白色圆的半径不断变小。
  3. 彩色变大的圆和小钩:在白色圆的半径减小到零之后,绘制彩色变大的圆,动画效果还是通过不断重绘来实现的。在不断重绘的过程中,将彩色圆半径一点点变大。绘制圆之后,绘制小钩,小钩的实现和未选中状态一致,通过Path即可实现。
  4. 彩色变小的圆和小钩:当前面那个阶段的圆扩大到一定程度(程度由你来决定),开始绘制彩色圆缩小回初始尺寸的效果。实现方式和前一步类似,只不过把扩大的半径改为缩小的。

选中状态绘制流程设计

需求分析里面说了那么多“废话”,还是来张图更清晰些。
选中状态绘制流程设计

拆解需求,按步骤实现代码

让我们拆解需求,一步步地写出代码。

区分选中和未选中状态

首先当然是区分大方向,用一个变量来标记即可,代码如下:

@Overrideprotected void onDraw(Canvas canvas) {    super.onDraw(canvas);    if(isCheck){        drawChecked(canvas);    }else{        drawUnChecked(canvas);    }}

接下来开始看看选中状态的绘制流程,也就是drawChecked()方法。

移动坐标系

由于绘制的主要是圆(包括弧线),所以觉得坐标系移到中间比较方便,所以在所有绘制的开始,先将坐标原点移动到View中央:

canvas.save();canvas.translate(halfWidth, halfHeight);

绘制彩色弧线

如前面的流程所述,需要绘制一个扫过角度不断变大的弧线,所以要这样子做:

// sweepAnglesCounter :已扫过角度计数器,每次加多少都可以,但是要保证要是360的约数。当然了,加的太大,视觉效果就不好。// MAX_SWEEP_ANGLES:最大角度,其实就是360°if(sweepAnglesCounter < MAX_SWEEP_ANGLES){    sweepAnglesCounter += 12;}// 绘制弧线,注意是不过圆心的弧线,所以传入了falsecanvas.drawArc(-radius, -radius, radius, radius, START_ANGLES, sweepAnglesCounter, false, checkedPaint);

绘制彩色圆以及白色变小的圆

如前面的流程图所示,这里需要先绘制一个彩色的圆,再绘制一个白色的圆,且白色圆的半径逐渐变小。来看代码:

// 注意这个判断标记,说明绘制动态弧线的阶段已经过了if(sweepAnglesCounter == MAX_SWEEP_ANGLES){    // 绘制彩色圆(静态)    checkedPaint.setStyle(Paint.Style.FILL);    canvas.drawCircle(0, 0, radius, checkedPaint);    // 白色变小的圆半径计数    if(whiteRadiusCounter >= 20){        whiteRadiusCounter -= 20;    }    // 绘制白色逐渐变小的圆(动态)    whitePaint.setStyle(Paint.Style.FILL);    canvas.drawCircle(0, 0, whiteRadiusCounter, whitePaint);}

绘制彩色扩大的圆以及小钩

嗯这里我们需要一个彩色圆不断变大的半径计数,彩色圆的半径上限,还有描述小钩路径的Path对象。所以需要这样写:

// 注意这个标记位,表示“白色逐渐变小的圆”绘制阶段已经结束,所以开始进入彩色圆和小钩绘制阶段if(whiteRadiusCounter < 20){    whitePaint.setStyle(Paint.Style.STROKE);    // 半径计数器小于半径的上限    if(expandRadiusCounter < maxExpandRadius){        // 绘制彩色圆变大(动态)同时绘制“小钩”(静态)        expandRadiusCounter += 20;        canvas.drawCircle(0, 0, expandRadiusCounter, checkedPaint);        canvas.drawPath(tickPath, whitePaint); // 绘制小钩    }}

绘制彩色变小的圆以及小钩

逻辑和前面一步差不多,只不过是计数方式反过来了,不多说了,看代码:

if(expandRadiusCounter == maxExpandRadius){    // 彩色圆半径缩小计数器仍大于等于圆初始大小    if(narrowRadiusCounter >= radius) {        // 绘制彩色圆缩回变大前效果(动态)同时绘制“小钩”(静态)        narrowRadiusCounter -= 20;        canvas.drawCircle(0, 0, narrowRadiusCounter, checkedPaint);        canvas.drawPath(tickPath, whitePaint);// 画小勾    }}

恢复坐标系,重置计数器

动态绘制完成了,当然是要把东西还原回去,像这样子:

canvas.restore(); // 恢复坐标系// “绘制彩色变小的圆和小钩”阶段还没结束(或者可能还没开始),说明选中状态的动画还没结束,继续重绘// 注意这里的继续重绘,这就是动画效果实现的原因if(narrowRadiusCounter >= radius){    // 也可以改成调用postInvalidateDelayed()方法控制动画速度    invalidate();} else {    // 动态效果绘制结束立刻重置变量    // 避免窗口在onStop()-->onReStar()之后导致该View绘制异常    reset();}

未选中状态的静态效果

这个效果比较简单,是静态的,几行代码就搞定了:

private void drawUnChecked(Canvas canvas){    canvas.save();    canvas.translate(halfWidth, halfHeight);    // 绘制一个灰色的圆圈、小钩    canvas.drawCircle(0, 0, radius, unCheckedPaint);    canvas.drawPath(tickPath, unCheckedPaint);    canvas.restore();}

添加xml属性

其实关于绘制的过程,已经讲完了,不过一个完整的自定义View,应该支持xml属性,那我们就写几个来意思一下。首先在res/values/路径下面,新建attrs.xml文件,然后写入我们想要支持的属性:

<?xml version="1.0" encoding="utf-8"?><resources>    <!--打钩小动画的属性-->    <declare-styleable name="TickView">        <!--选中时圆的颜色-->        <attr name="checked_color" format="color"/>        <!--是否选中-->        <attr name="checked" format="boolean"/>        <!--圆半径-->        <attr name="radius" format="dimension"/>    </declare-styleable></resources>

然后在构造方法里读取并设置这些属性

// 获取xml属性TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TickView);// 选中状态画笔的颜色checkedPaint.setColor(typedArray.getColor(R.styleable.TickView_checked_color, DEFAULT_CHECKED_COLOR));// 初始化为选中还是未选中状态isCheck = typedArray.getBoolean(R.styleable.TickView_checked, false);// 半径radius = (int)typedArray.getDimension(R.styleable.TickView_radius, DEFAULT_RADIUS);typedArray.recycle();

暴露一些控制接口

看我们前面贴的效果图,点击按钮可以改变选中效果,所以肯定是有提供控制接口,也很简单,直接看代码:

public void setCheck(boolean check) {    isCheck = check;    // 记得要重置计数器,这很重要    reset();    invalidate();}

关于测量——重写onMeasure()方法

主要是为了支持wrap_content属性,总不能总是占满全屏,或者迫使调用者写个固定尺寸。那么这个默认尺寸该怎么设计呢?很简单,彩色圆变大的时候,有一个上限半径,这个就可以作为默认尺寸。不过我们在xml文件里面支持了圆形扩大前的半径,如果用户设置了该怎么办呢?只要在两者之间做一个简单计算就可以了,像这样子:

// radius是来自xml里面设置的半径maxExpandRadius = radius + 60;

那么现在onMeasure()方法就可以这样写了:

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    setMeasuredDimension(getMeasureSize(widthMeasureSpec), getMeasureSize(heightMeasureSpec));}private int getMeasureSize(int measureSpec){    int modeSpec = MeasureSpec.getMode(measureSpec);    int sizeSpec = MeasureSpec.getSize(measureSpec);    int result;    if(modeSpec == MeasureSpec.EXACTLY){        result = sizeSpec;    } else {        result = maxExpandRadius<<1;        if(modeSpec == MeasureSpec.AT_MOST){            result = Math.min(sizeSpec, result);        }    }    return result;}

小结

总体来说是一个不复杂的自定义View,非常适合新手尝试绘制动画效果。其中主要注意两点:

  1. 区分选中和未选中状态,一个是静态效果(不需要反复绘制),一个是动态效果(需要不断重绘)。这两个效果建议写在两个不同方法里面,不要扎堆地写在onDraw()方法里面。
  2. 在重绘的过程里,通过计数器判断处于哪个绘制阶段,并且在动态效果绘制结束后,注意重置计数器值,以避免一些bug。

源码

github地址:https://github.com/JaffarOu/SimpleCustomView
这个项目收集了几个不同的自定义View,本文介绍的这个对应的名字叫做TickView。

阅读全文
0 0
原创粉丝点击