当前位置:首页 > 安卓源码 > 技术博客 >

Android自定义View分享 打钩动画

时间:2018-07-18 09:25 来源:互联网 作者:源码搜藏 浏览: 收藏 挑错 推荐 打印

效果图 这是今天要做的效果,没图没真相: 需求分析 首先大方向分成两个:选中/未选中状态。未选中状态很简单,静态的,画一个空心圆,一个小钩就可以了,小钩可以用Path来实现。下面主要说说动态的选中状态。 绘制弧线:这是一个动态的过程,所以是不断重绘

效果图

这是今天要做的效果,没图没真相: 

Android自定义View分享 打钩动画

需求分析

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

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

选中状态绘制流程设计

需求分析里面说了那么多“废话”,还是来张图更清晰些。 

Android自定义View分享 打钩动画

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

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

区分选中和未选中状态

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

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if(isCheck){
        drawChecked(canvas);
    }else{
        drawUnChecked(canvas);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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

移动坐标系

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

canvas.save();
canvas.translate(halfWidth, halfHeight);
  • 1
  • 2

绘制彩色弧线

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

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

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

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

// 注意这个判断标记,说明绘制动态弧线的阶段已经过了
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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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

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

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

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

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

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

恢复坐标系,重置计数器

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

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

未选中状态的静态效果

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

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

添加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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

// 获取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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

暴露一些控制接口

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

public void setCheck(boolean check) {
    isCheck = check;
    // 记得要重置计数器,这很重要
    reset();
    invalidate();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

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

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

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

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

@Override
protected 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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

小结

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

  1. 区分选中和未选中状态,一个是静态效果(不需要反复绘制),一个是动态效果(需要不断重绘)。这两个效果建议写在两个不同方法里面,不要扎堆地写在onDraw()方法里面。
  2. 在重绘的过程里,通过计数器判断处于哪个绘制阶段,并且在动态效果绘制结束后,注意重置计数器值,以避免一些bug。
Android自定义View分享 打钩动画 转载https://www.codesocang.com/appboke/38740.html

技术博客阅读排行

最新文章