当前位置: 首页 > news >正文

Android -- 自定义view实现keep欢迎页倒计时效果

1,最近打开keep的app的时候,发现它的欢迎页面的倒计时效果还不错,所以打算自己来写写,然后就有了这篇文章。

2,还是老规矩,先看一下我们今天实现的效果

   相较于我们常见的倒计时,这次实现的效果是多了外面圆环的不断减少,这也是我们这次自定义view的有意思的一点。

   知道了效果我们先来效果分析一波,首先是一个倒计时效果,计时的时候上面的圆弧不断的减少,里面的文字也不断的变化,在视觉上的改变就大致为这两部分,但是实际上我们的ui是由三部分来构成的:里面的实心圆、外面的圆弧、里面的文字。知道了我们ui的组成,我们就来开撸开撸。

  在开撸之前我们还是回顾一下我们简单的自定义view的基本流程

/**
 * 自定义View的几个步骤
 * 1,自定义View属性
 * 2,在View中获得我们的自定义的属性
 * 3,重写onMeasure
 * 4,重写onDraw
 * 5,重写onLayout(这个决定view放置在哪儿)
 */

 ①、确定自定义属性

   我们根据上面的基本步骤,我们知道首先我们根据效果图先来确定我们这次的自定义属性,这里我简单的分析了一下,主要添加了八个自定义属性,分别是里面实心圆的半径和颜色、圆弧的颜色和半径、里面文字的大小和颜色、总倒计时时间的长度、圆弧减少的方向(分为顺时针和逆时针),所以首先在res/values目录下创建attrs.xml文件,添加以下属性:(这里如果有对自定义属性不太了解的同学可以去了解我以前写过的这篇文章,可以更加深刻的理解)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleTimerView">

        <attr name="solid_circle_radius" format="dimension"/>
        <attr name="solid_circle_color" format="color"/>
        <attr name="empty_circle_color" format="color"/>
        <attr name="empty_circle_radius" format="dimension"/>
        <attr name="circle_text_size" format="dimension"/>
        <attr name="circle_text_color" format="color"/>

        <attr name="circle_draw_orientation" format="enum">
            <!--顺时针-->
            <enum name="clockwise" value="1"/>
            <!--逆时针-->
            <enum name="anticlockwise" value="2"/>
        </attr>

        <attr name="time_length" format="integer"/>

    </declare-styleable>
</resources>

  ②、获取自定义属性、初始化一些属性

  首先创建CircleTimerView类,继承自View类

public class CircleTimerView extends View {

    private Context context ;

    //里面实心圆颜色
    private int mSolidCircleColor ;
    //里面圆的半径
    private int mSolidCircleRadius;
    //外面圆弧的颜色
    private int mEmptyCircleColor ;
    //外面圆弧的半径(可以使用画笔的宽度来实现)
    private int mEmptyCircleRadius ;
    //文字大小
    private int mTextSize ;
    //文字颜色
    private int mTextColor ;
    //文字
    private String mText ;
    //绘制的方向
    private int mDrawOrientation;
    //圆弧绘制的速度
    private int mSpeed;
    //圆的画笔
    private Paint mPaintCircle ;
    //圆弧的画笔
    private Paint mPaintArc ;
    //绘制文字的画笔
    private Paint mPaintText;
    //时长
    private int mTimeLength ;

    //默认值
    private int defaultSolidCircleColor ;
    private int defaultEmptyCircleColor ;
    private int defaultSolidCircleRadius ;
    private int defaultEmptyCircleRadius ;
    private int defaultTextColor ;
    private int defaultTextSize ;
    private int defaultTimeLength ;
    private int defaultDrawOritation ;

    //当前扇形的角度
    private int startProgress ;
    private int endProgress ;
    private float currProgress ;

    //动画集合
    private AnimatorSet set ;

    //回调
    private OnCountDownFinish onCountDownFinish ;

    public CircleTimerView(Context context) {
        this(context,null);
    }

    public CircleTimerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context ;

        //初始化默认值
        defaultSolidCircleColor = getResources().getColor(R.color.colorPrimary);
        defaultEmptyCircleColor = getResources().getColor(R.color.colorAccent);
        defaultTextColor = getResources().getColor(R.color.colorYellow);

        defaultSolidCircleRadius = (int) getResources().getDimension(R.dimen.dimen_20);
        defaultEmptyCircleRadius = (int) getResources().getDimension(R.dimen.dimen_25);
        defaultTextSize = (int) getResources().getDimension(R.dimen.dimen_16);

        defaultTimeLength = 3 ;
        defaultDrawOritation = 1 ;

        //获取自定义属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleTimerView);
        mSolidCircleColor = a.getColor(R.styleable.CircleTimerView_solid_circle_color,defaultSolidCircleColor);
        mSolidCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_solid_circle_radius ,defaultSolidCircleRadius);

        mEmptyCircleColor = a.getColor(R.styleable.CircleTimerView_empty_circle_color,defaultEmptyCircleColor);
        mEmptyCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_empty_circle_radius ,defaultEmptyCircleRadius);

        mTextColor = a.getColor(R.styleable.CircleTimerView_circle_text_color,defaultTextColor);
        mTextSize = a.getDimensionPixelOffset(R.styleable.CircleTimerView_circle_text_size ,defaultTextSize);

        mDrawOrientation = a.getInt(R.styleable.CircleTimerView_circle_draw_orientation,defaultDrawOritation);
        mTimeLength = a.getInt(R.styleable.CircleTimerView_time_length ,defaultTimeLength);

        a.recycle();

        init();
    }

    private void init() {
        //初始化画笔
        mPaintCircle = new Paint();
        mPaintCircle.setStyle(Paint.Style.FILL);
        mPaintCircle.setAntiAlias(true);
        mPaintCircle.setColor(mSolidCircleColor);

        mPaintArc = new Paint();
        mPaintArc.setStyle(Paint.Style.STROKE);
        mPaintArc.setAntiAlias(true);
        mPaintArc.setColor(mEmptyCircleColor);
        mPaintArc.setStrokeWidth(mEmptyCircleRadius - mSolidCircleRadius);

        mPaintText = new Paint();
        mPaintText.setStyle(Paint.Style.STROKE);
        mPaintText.setAntiAlias(true);
        mPaintText.setTextSize(mTextSize);
        mPaintText.setColor(mTextColor);

        mText= mTimeLength +"" ;
        if(defaultDrawOritation == 1){
            startProgress = 360 ;
            endProgress = 0 ;
        }else {
            startProgress = 0 ;
            endProgress = 360 ;
        }
        currProgress = startProgress ;
    }

  这里我在构造函数里面先初始化一些默认的值,然后获取自定义属性,然后再初始化三个画笔,分别代表:实心圆、圆弧、Text的画笔(这个很好理解),然后根据顺时针和逆时针来初始化开始角度和结束角度,很简单就不在过多的废话了。

  ③、重写onMeasure方法

  这里由于我们的效果很简单,基本上就是一个正方形,所以这里我是以外面圆弧的半径当这个view 的宽高的,就没去判断match_parent、wrap_content之类的情况,代码如下:

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置宽高
        setMeasuredDimension(mEmptyCircleRadius*2,mEmptyCircleRadius*2);
    }

  ④,重写onDraw方法

  这也是我们自定义view关键,首先我们绘制圆弧和文字很简单,绘制圆弧的话可能有些同学没有接触过,这里我以前写过一篇,大家可以去看看,我们这里要用的知识点 都是一样的,所以就不再废话

  

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制背景圆
        canvas.drawCircle(mEmptyCircleRadius,mEmptyCircleRadius,mSolidCircleRadius,mPaintCircle);

        //绘制圆弧
        RectF oval = new RectF((mEmptyCircleRadius - mSolidCircleRadius)/2, (mEmptyCircleRadius - mSolidCircleRadius)/2
                , mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius); // 用于定义的圆弧的形状和大小的界限

        canvas.drawArc(oval, -90, currProgress, false, mPaintArc); // 根据进度画圆弧

        //绘制文字
        Rect mBound = new Rect();
        mPaintText.getTextBounds(mText, 0, mText.length(), mBound);
        canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaintText);
    }

  在这个时候,我们就可以来看一下我们自定义view的效果了,将我们currProgress先写死成270,来看看我们的效果,这里注意一项在使用我们的自定义属性的时候,记得在布局文件中添加我们自定义空间。运行效果如下:

  可以看到这里我们的效果基本上试出来了,关键是怎么让它动起来,这里我们的第一反应是handle或者timer来实现一个倒计时,一开始阿呆哥哥也是使用timer来实现的,不过发现由于ui的改变中是有两个不同速率的view在改变:圆弧的不断减小、textView字体的逐渐变小,所以这里使用一个timer无法实现,得用两个,如果用两个就不怎么软件工程了,所以这里打算使用动画来实现,具体代码如下:

/**
     * 通过外部开关控制
     */
    public void start(){
       
        ValueAnimator animator1 = ValueAnimator.ofFloat(startProgress,endProgress);
        animator1.setInterpolator(new LinearInterpolator());
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currProgress = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });

        ValueAnimator animator2 = ValueAnimator.ofInt(mTimeLength,0);
        animator2.setInterpolator(new LinearInterpolator());
        animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mTimeLength = (int) valueAnimator.getAnimatedValue();
                if (mTimeLength == 0)
                    return;
                mText =mTimeLength+ "";
            }
        });

        set = new AnimatorSet();
        set.playTogether(animator1,animator2);
        set.setDuration(mTimeLength * 1000);

        set.start();

        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (onCountDownFinish != null){
                    onCountDownFinish.onFinish();
                }
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });


    }

  很简单,就是两个ValueAnimator,监听值的改变,然后再最后完成的动画的时候使用接口回调,通知宿主完成ToDo操作,所以到这里我们基本上完全实现了我们的view 的自定义,CircleTimerView的完整代码如下:

package com.ysten.circletimerdown.view;


import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AnimationSet;
import android.view.animation.LinearInterpolator;

import com.ysten.circletimerdown.R;

import java.util.Timer;
import java.util.TimerTask;


/**
 * author : wangjitao
 * e-mail : 543441727@qq.com
 * time   : 2017/08/14
 * desc   :
 * version: 1.0
 */
public class CircleTimerView extends View {

    private Context context ;

    //里面实心圆颜色
    private int mSolidCircleColor ;
    //里面圆的半径
    private int mSolidCircleRadius;
    //外面圆弧的颜色
    private int mEmptyCircleColor ;
    //外面圆弧的半径(可以使用画笔的宽度来实现)
    private int mEmptyCircleRadius ;
    //文字大小
    private int mTextSize ;
    //文字颜色
    private int mTextColor ;
    //文字
    private String mText ;
    //绘制的方向
    private int mDrawOrientation;
    //圆弧绘制的速度
    private int mSpeed;
    //圆的画笔
    private Paint mPaintCircle ;
    //圆弧的画笔
    private Paint mPaintArc ;
    //绘制文字的画笔
    private Paint mPaintText;
    //时长
    private int mTimeLength ;

    //默认值
    private int defaultSolidCircleColor ;
    private int defaultEmptyCircleColor ;
    private int defaultSolidCircleRadius ;
    private int defaultEmptyCircleRadius ;
    private int defaultTextColor ;
    private int defaultTextSize ;
    private int defaultTimeLength ;
    private int defaultDrawOritation ;

    //当前扇形的角度
    private int startProgress ;
    private int endProgress ;
    private float currProgress ;

    //动画集合
    private AnimatorSet set ;

    //回调
    private OnCountDownFinish onCountDownFinish ;

    public CircleTimerView(Context context) {
        this(context,null);
    }

    public CircleTimerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public CircleTimerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context ;

        //初始化默认值
        defaultSolidCircleColor = getResources().getColor(R.color.colorPrimary);
        defaultEmptyCircleColor = getResources().getColor(R.color.colorAccent);
        defaultTextColor = getResources().getColor(R.color.colorYellow);

        defaultSolidCircleRadius = (int) getResources().getDimension(R.dimen.dimen_20);
        defaultEmptyCircleRadius = (int) getResources().getDimension(R.dimen.dimen_25);
        defaultTextSize = (int) getResources().getDimension(R.dimen.dimen_16);

        defaultTimeLength = 3 ;
        defaultDrawOritation = 1 ;

        //获取自定义属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleTimerView);
        mSolidCircleColor = a.getColor(R.styleable.CircleTimerView_solid_circle_color,defaultSolidCircleColor);
        mSolidCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_solid_circle_radius ,defaultSolidCircleRadius);

        mEmptyCircleColor = a.getColor(R.styleable.CircleTimerView_empty_circle_color,defaultEmptyCircleColor);
        mEmptyCircleRadius = a.getDimensionPixelOffset(R.styleable.CircleTimerView_empty_circle_radius ,defaultEmptyCircleRadius);

        mTextColor = a.getColor(R.styleable.CircleTimerView_circle_text_color,defaultTextColor);
        mTextSize = a.getDimensionPixelOffset(R.styleable.CircleTimerView_circle_text_size ,defaultTextSize);

        mDrawOrientation = a.getInt(R.styleable.CircleTimerView_circle_draw_orientation,defaultDrawOritation);
        mTimeLength = a.getInt(R.styleable.CircleTimerView_time_length ,defaultTimeLength);

        a.recycle();

        init();
    }

    private void init() {
        //初始化画笔
        mPaintCircle = new Paint();
        mPaintCircle.setStyle(Paint.Style.FILL);
        mPaintCircle.setAntiAlias(true);
        mPaintCircle.setColor(mSolidCircleColor);

        mPaintArc = new Paint();
        mPaintArc.setStyle(Paint.Style.STROKE);
        mPaintArc.setAntiAlias(true);
        mPaintArc.setColor(mEmptyCircleColor);
        mPaintArc.setStrokeWidth(mEmptyCircleRadius - mSolidCircleRadius);

        mPaintText = new Paint();
        mPaintText.setStyle(Paint.Style.STROKE);
        mPaintText.setAntiAlias(true);
        mPaintText.setTextSize(mTextSize);
        mPaintText.setColor(mTextColor);

        mText= mTimeLength +"" ;
        if(defaultDrawOritation == 1){
            startProgress = 360 ;
            endProgress = 0 ;
        }else {
            startProgress = 0 ;
            endProgress = 360 ;
        }
        currProgress = startProgress ;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置宽高
        setMeasuredDimension(mEmptyCircleRadius*2,mEmptyCircleRadius*2);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //绘制背景圆
        canvas.drawCircle(mEmptyCircleRadius,mEmptyCircleRadius,mSolidCircleRadius,mPaintCircle);

        //绘制圆弧
        RectF oval = new RectF((mEmptyCircleRadius - mSolidCircleRadius)/2, (mEmptyCircleRadius - mSolidCircleRadius)/2
                , mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius, mEmptyCircleRadius + (mEmptyCircleRadius - mSolidCircleRadius)/2+mSolidCircleRadius); // 用于定义的圆弧的形状和大小的界限

        canvas.drawArc(oval, -90, currProgress, false, mPaintArc); // 根据进度画圆弧

        //绘制文字
        Rect mBound = new Rect();
        mPaintText.getTextBounds(mText, 0, mText.length(), mBound);
        canvas.drawText(mText, getWidth() / 2 - mBound.width() / 2, getHeight() / 2 + mBound.height() / 2, mPaintText);
    }

    public OnCountDownFinish getOnCountDownFinish() {
        return onCountDownFinish;
    }

    public void setOnCountDownFinish(OnCountDownFinish onCountDownFinish) {
        this.onCountDownFinish = onCountDownFinish;
    }


    /**
     * 通过外部开关控制
     */
    public void start(){

        ValueAnimator animator1 = ValueAnimator.ofFloat(startProgress,endProgress);
        animator1.setInterpolator(new LinearInterpolator());
        animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                currProgress = (float) valueAnimator.getAnimatedValue();
                invalidate();
            }
        });

        ValueAnimator animator2 = ValueAnimator.ofInt(mTimeLength,0);
        animator2.setInterpolator(new LinearInterpolator());
        animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                mTimeLength = (int) valueAnimator.getAnimatedValue();
                if (mTimeLength == 0)
                    return;
                mText =mTimeLength+ "";
            }
        });

        set = new AnimatorSet();
        set.playTogether(animator1,animator2);
        set.setDuration(mTimeLength * 1000);

        set.start();

        set.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {

            }

            @Override
            public void onAnimationEnd(Animator animator) {
                if (onCountDownFinish != null){
                    onCountDownFinish.onFinish();
                }
            }

            @Override
            public void onAnimationCancel(Animator animator) {

            }

            @Override
            public void onAnimationRepeat(Animator animator) {

            }
        });


    }

    public void cancelAnim(){
        if(set != null)
        set.pause();
    }

    public interface OnCountDownFinish{
        void onFinish();
    }
}

  最后实现的效果如下:

    Github代码地址,有需要源码的同学可以去下载一下。

转载于:https://www.cnblogs.com/wjtaigwh/p/7359114.html

相关文章:

  • mysql 100%占用的解决
  • 天天爱跑步NOIP
  • 白话经典之String字符串详解
  • C++ STL疑惑知识点
  • Python基础
  • 理解浏览器关键的渲染路径
  • Android RecyclerView 水平滚动+自动循环轮播
  • GBDT和随机森林的区别
  • C#使用Xamarin开发可移植移动应用(3.Xamarin.Views控件)附源码
  • 在客户端先通过JS验证后再将表单提交到服务器
  • hihocoder 1320 压缩字符串(字符串+dp)
  • 一元线性回归模型的基本假设
  • marathon小知识点分享之如何远程调试marathon
  • AJAX的get和post请求原生编写方法
  • C/C++ 数据范围
  • 【Leetcode】101. 对称二叉树
  • CSS 提示工具(Tooltip)
  • Docker 笔记(2):Dockerfile
  • Java 最常见的 200+ 面试题:面试必备
  • Nginx 通过 Lua + Redis 实现动态封禁 IP
  • node-glob通配符
  • vue2.0项目引入element-ui
  • 基于web的全景—— Pannellum小试
  • 经典排序算法及其 Java 实现
  • 前端 CSS : 5# 纯 CSS 实现24小时超市
  • 前端攻城师
  • 让你成为前端,后端或全栈开发程序员的进阶指南,一门学到老的技术
  • 如何编写一个可升级的智能合约
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 入手阿里云新服务器的部署NODE
  • 设计模式走一遍---观察者模式
  • 一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
  • No resource identifier found for attribute,RxJava之zip操作符
  • #HarmonyOS:基础语法
  • #if 1...#endif
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (android 地图实战开发)3 在地图上显示当前位置和自定义银行位置
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (附源码)springboot炼糖厂地磅全自动控制系统 毕业设计 341357
  • (南京观海微电子)——I3C协议介绍
  • (十六)串口UART
  • (十三)Java springcloud B2B2C o2o多用户商城 springcloud架构 - SSO单点登录之OAuth2.0 根据token获取用户信息(4)...
  • (四) Graphivz 颜色选择
  • (转)GCC在C语言中内嵌汇编 asm __volatile__
  • (转)socket Aio demo
  • (转)大型网站架构演变和知识体系
  • .NET 4.0中的泛型协变和反变
  • .net core 6 redis操作类
  • .NET CORE使用Redis分布式锁续命(续期)问题
  • .NET Entity FrameWork 总结 ,在项目中用处个人感觉不大。适合初级用用,不涉及到与数据库通信。
  • .Net的DataSet直接与SQL2005交互
  • @SuppressWarnings(unchecked)代码的作用
  • [Android View] 可绘制形状 (Shape Xml)
  • [ASP.NET MVC]Ajax与CustomErrors的尴尬
  • [C++]高精度 bign (重载运算符版本)