为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Android


写在前面

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

104204xoqaaiis3ka7aard.gif

需求分析

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

  1. 绘制弧线:这是一个动态的过程,所以是不断重绘,并且不断增大弧线扫过的角度,直至360°。

  2. 变小的白色圆:当弧线扫满360°,在一个彩色实心圆的背景下,有一个半径不断变小的白色的圆。所以实现的方式是,先绘制一个彩色实心圆,然后再绘制白色圆,当然还是通过不断重绘实现动画效果,在重绘的同时白色圆的半径不断变小。

  3. 彩色变大的圆和小钩:在白色圆的半径减小到零之后,绘制彩色变大的圆,动画效果还是通过不断重绘来实现的。在不断重绘的过程中,将彩色圆半径一点点变大。绘制圆之后,绘制小钩,小钩的实现和未选中状态一致,通过Path即可实现。

  4. 彩色变小的圆和小钩:当前面那个阶段的圆扩大到一定程度(程度由你来决定),开始绘制彩色圆缩小回初始尺寸的效果。实现方式和前一步类似,只不过把扩大的半径改为缩小的。

选中状态绘制流程设计

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

104212sl5qnll88q8k383t.jpg

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

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

区分选中和未选中状态

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

@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。


原文链接:http://www.apkbus.com/blog-822717-77324.html

点击查看更多内容
1人点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消