自定义View之仿启动页圆形倒计时

  • 先来看一下效果图

使用场景
  • 有时候,在应用的启动页需要有一个圆形View的倒计时,倒计时结束后跳转页面
主要思路
  • 首先对于View我们要考虑整个View的测量问题,当View被设置为wrap_content时,默认View的宽高会被设置成match_parent.所以我们需要在onMeasure()为其设置默认值

  • 对于整个View我们把它分为4个部分:中心圆,外部圆环,覆盖在中心圆上的文字,以及覆盖在外部圆环上的动态进度圆环

代码实现
  • 1.我们在attrs.xml自定义一些属性

  • 2.自定义CountdownCircleView,在自定义View中,获取自定义属性配置信息

        val mTypeArray=mContext.obtainStyledAttributes(attrs, R.styleable.CountdownCircleView)
           mOutCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_out_circle_color,mOutCircleColor)
           mOutCircleWidth=mTypeArray.getDimension(R.styleable.CountdownCircleView_out_circle_width,mOutCircleWidth)
           mInCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_in_circle_color,mInCircleColor)
           mCountDownCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_count_down_circle_color,mCountDownCircleColor)
           mCountDownStr=mTypeArray.getInt(R.styleable.CountdownCircleView_count_down_str,mCountDownStr)
           mCountDownTextColor=mTypeArray.getColor(R.styleable.CountdownCircleView_count_down_text_color,mCountDownTextColor)
           mCountDownTextSize=mTypeArray.getDimension(R.styleable.CountdownCircleView_count_down_text_size,mCountDownTextSize)
           mIsAutoPlayer=mTypeArray.getBoolean(R.styleable.CountdownCircleView_is_auth_player,mIsAutoPlayer)
           //释放
           mTypeArray.recycle()
    
  • 3.在onMeasure()中,根据测量模式为其View设置默认值

          /**
          * 对View进行测量
          * 1.我们需要考虑View为wrap_content时的宽高尺寸,我们需要给予一个默认值,
          *  否则将会和match_parent一样,占据整个父布局
          */
         override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
             super.onMeasure(widthMeasureSpec, heightMeasureSpec)
             var width=getMeasureWidth(widthMeasureSpec)
             var height=getMeasureHeight(heightMeasureSpec)
             setMeasuredDimension(width,height)
         }
     
         /**
          * 对View的宽度进行测量
          */
         private fun getMeasureWidth(widthMeasureSpec: Int): Int {
             val mode=MeasureSpec.getMode(widthMeasureSpec)
             var width=MeasureSpec.getSize(widthMeasureSpec)
             when(mode){
                 MeasureSpec.EXACTLY ->{//match_parent  100dp
                 }
                 MeasureSpec.AT_MOST ->{//wrap_content
                     width=100
                 }
                 MeasureSpec.UNSPECIFIED ->{
                 }
             }
             return  width
         }
         /**
          * 对View的高度进行测量
          */
         private fun getMeasureHeight(heightMeasureSpec: Int): Int {
             val mode=MeasureSpec.getMode(heightMeasureSpec)
             var height=MeasureSpec.getSize(heightMeasureSpec)
             when(mode){
                 MeasureSpec.EXACTLY ->{//match_parent  100dp
                 }
                 MeasureSpec.AT_MOST ->{//wrap_content
                     height=100
                 }
                 MeasureSpec.UNSPECIFIED ->{
                 }
             }
             return  height
         }
    
    • 根据用户设置的倒计时时间mCountDownStr*1000来获取总共有多少毫秒,然后我们每50毫秒累计一次时间,将相加的时间相比,判断时否等于,若等于则表示时间已到,停止倒计时。因此可以使runable来实现

        /**
          * 1. 计算整个外部倒计时圆环的进度,整个进度最大值为100,
          * 2. 计算显示的时间,因为是50毫秒一次,所以相当于50毫秒时间要加一次,所以走的时间为mCurrentCountDownTime+=50/1000 单位为妙
          *    因为是剩余时间,所以要用总时间来减 mCountDownStr-mCurrentCountDownTime/1000
          */
         override fun run() {
             //计算显示的时间
             mCurrentCountDownTime+=50
             if(mCurrentCountDownTime%1000==0){
                 mShowText=if((mCountDownStr-(mCurrentCountDownTime/1000))==0) "跳过" else (mCountDownStr-mCurrentCountDownTime/1000).toString()+"秒"
             }
             //计算进度
             mCurrentProgress+=5/(mCountDownStr*1.0f).toInt()
             //刷新
             invalidate()
             //再次发送延时消息
             if(mCurrentProgress>=100){
                 removeCallbacks { this }
             }else{
                 postDelayed(this,50)
             }
             Log.d(TAG,"mShowText:$mShowText ->mCurrentCountDownTime:$mCurrentCountDownTime")
      
         }
      
    • 在View被销毁的时候需要通知延时消息

        /**
         * 当View从界面移除时触发
         */
        override fun onDetachedFromWindow() {
            super.onDetachedFromWindow()
            stopCountDown()
            Log.d(TAG,"onDetachedFromWindow")
        }
        /**
         * 倒计时停止
         */
          fun stopCountDown() {
            removeCallbacks(this)
            mIsRunning=false
            mCurrentCountDownTime=0
            mCurrentProgress=0
            mShowText=""
        }
      
    • 当我们知道进度后,现在就需要在onDraw()中对View进行绘制,绘制后才能在界面显示

         /**
         * 对View进行绘制
         */
        override fun onDraw(canvas: Canvas) {
            super.onDraw(canvas)
            //1.计算中心点x,y的坐标
            val mCenterX=width/2
            val mCenterY=height/2
            //2.计算坐标  因为是圆,所以需要考虑设置的宽高不相等时的情况,在这里我们取最小边的尺寸,用是要处理padding的影响
            var mRadius=if(width>height) (height-paddingTop-paddingBottom-mOutCircleWidth*2)/2 else  (width-paddingLeft-paddingRight-mOutCircleWidth*2)/2
            //3.绘制内部圆
            //设置画笔填充模式
            mPaint.style=Paint.Style.FILL
            //设置抗锯齿
            mPaint.isAntiAlias=true
            //设置画笔绘制内容的颜色
            mPaint.color=mInCircleColor
            //绘制圆
            canvas.drawCircle(mCenterX.toFloat(), mCenterY.toFloat(),mRadius,mPaint)
            //4.绘制外部圆
            //设置画笔填充模式
            mPaint.style=Paint.Style.STROKE
            //设置画笔的宽度
            mPaint.strokeWidth=mOutCircleWidth
            //设置画笔绘制内容的颜色
            mPaint.color=mOutCircleColor
            //绘制
            canvas.drawCircle(mCenterX.toFloat(), mCenterY.toFloat(), mRadius.toFloat(),mPaint)
            //5.绘制文字
            if(!mShowText.isEmpty()) {
                mPaint.style = Paint.Style.FILL
                //设置颜色
                mPaint.color = mCountDownTextColor
                //设置中心对称
                mPaint.textAlign = Paint.Align.CENTER
                //设置字体的大小
                mPaint.textSize = mCountDownTextSize
                //设置字体颜色
                mPaint.color = mCountDownTextColor
                val mFontMetrics=mPaint.fontMetrics
                val bottom=mFontMetrics.bottom
                val top=mFontMetrics.top
                canvas.drawText(mShowText, mCenterX.toFloat(), mCenterY-(top+bottom)/2, mPaint)
            }
            //6.绘制环形
            if(mCurrentProgress>=0){
                val oval=RectF(mCenterX-mRadius,mCenterY-mRadius,mCenterX+mRadius,mCenterY+mRadius)
                //设置画笔颜色
                mPaint.color=mCountDownCircleColor
                //设置画笔填充模式
                mPaint.style=Paint.Style.STROKE
                //设置画笔的宽度
                mPaint.strokeWidth=mOutCircleWidth
                //画圆弧
                canvas.drawArc(oval,-90f, (360-360*mCurrentProgress/100).toFloat(),false,mPaint)
                if(mCurrentProgress==100){
                    mListener?.onCountDownFinish()
                }
            }
        }
      
完整代码
  • 1.自定义属性

       <declare-styleable name="CountdownCircleView">
           <!--倒计时时间数字-->
           <attr name="count_down_str"  format="integer"/>
           <!--倒计时的字体的颜色-->
           <attr name="count_down_text_color" format="color"/>
           <!--倒计时文字的大小-->
           <attr name="count_down_text_size" format="dimension"/>
           <!--实心圆之外部圆环的颜色-->
           <attr name="out_circle_color" format="color"/>
           <!--实心圆之内部圆的颜色-->
           <attr name="in_circle_color" format="color"/>
           <!--实心圆之倒计时的圆环的颜色-->
           <attr name="count_down_circle_color" format="color"/>
           <!--实心圆之外部圆环的宽度-->
           <attr name="out_circle_width" format="dimension"/>
           <!--是否自动实现倒计时功能-->
           <attr name="is_auth_player" format="boolean"/>
       </declare-styleable>
    
  • 2.自定义CountdownCircleView

    	/**
    	 
    	 * Time: 2018/10/19 15:17
    	 * 描述:启动页圆形的倒计时
    	 *      1.可自定义实心外部圆颜色和宽度
    	 *      2.可自定义实心内部圆的颜色
    	 *      3.可自定义倒计时时圆圈的颜色
    	 *      4.可自定义中间文字的颜色和字体以及初始时间
    	 *      5.倒计时是在界面添加到界面时就触发了
    	 */
    	
    	class  CountdownCircleView @JvmOverloads constructor(
    	        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    	) : View(context, attrs, defStyleAttr),Runnable {
    	
    	    private val TAG="CountdownCircleView"
    	    /**倒计时字体时间  默认5秒*/
    	    private var mCountDownStr=5
    	    /**圆中心显示的文字*/
    	    private var mShowText=""
    	    /**倒计时字体大小*/
    	    private var mCountDownTextSize=30f
    	    /**倒计时字体颜色*/
    	    private var mCountDownTextColor=Color.WHITE
    	    /**实心圆之外部圆环的颜色*/
    	    private var mOutCircleColor=Color.GRAY
    	    /**实心圆之外部圆环的宽度*/
    	    private var mOutCircleWidth=15f
    	    /**实心圆的内部圆的颜色*/
    	    private var mInCircleColor=Color.GREEN
    	    /**倒计时圆环的颜色*/
    	    private var mCountDownCircleColor=Color.RED
    	    /**是否自动触发倒计时功能 默认true View被添加到界面时触发 false 需要自己触发*/
    	    private var mIsAutoPlayer=true
    	    /**是否正在倒计时*/
    	    private var mIsRunning=false
    	    /**倒计时 绘制倒计时圆环的进度 0-100 */
    	    private var mCurrentProgress=0
    	    /**当前已经倒计时过去了多少时间  毫秒*/
    	    private var mCurrentCountDownTime=0
    	    /**事件监听*/
    	    private var mListener:OnCountDownListener?=null
    	    /**画笔*/
    	    private val mPaint:Paint by lazy {
    	        Paint()
    	    }
    	
    	    init {
    	        initAttrs(context,attrs)
    	    }
    	    /**
    	     * 初始化 获取用户自定义属性
    	     */
    	    private fun initAttrs(mContext: Context, attrs: AttributeSet?) {
    	        val mTypeArray=mContext.obtainStyledAttributes(attrs, R.styleable.CountdownCircleView)
    	        mOutCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_out_circle_color,mOutCircleColor)
    	        mOutCircleWidth=mTypeArray.getDimension(R.styleable.CountdownCircleView_out_circle_width,mOutCircleWidth)
    	        mInCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_in_circle_color,mInCircleColor)
    	        mCountDownCircleColor=mTypeArray.getColor(R.styleable.CountdownCircleView_count_down_circle_color,mCountDownCircleColor)
    	        mCountDownStr=mTypeArray.getInt(R.styleable.CountdownCircleView_count_down_str,mCountDownStr)
    	        mCountDownTextColor=mTypeArray.getColor(R.styleable.CountdownCircleView_count_down_text_color,mCountDownTextColor)
    	        mCountDownTextSize=mTypeArray.getDimension(R.styleable.CountdownCircleView_count_down_text_size,mCountDownTextSize)
    	        mIsAutoPlayer=mTypeArray.getBoolean(R.styleable.CountdownCircleView_is_auth_player,mIsAutoPlayer)
    	        //释放
    	        mTypeArray.recycle()
    	        setOnClickListener {
    	           var isJumpActvity= mListener?.onViewClick(mCurrentCountDownTime/1000,mCountDownStr)
    	           if(isJumpActvity==true) {
    	               stopCountDown()
    	           }
    	        }
    	    }
    	
    	    /**
    	     * 对View进行测量
    	     * 1.我们需要考虑View为wrap_content时的宽高尺寸,我们需要给予一个默认值,
    	     *  否则将会和match_parent一样,占据整个父布局
    	     */
    	    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    	        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    	        var width=getMeasureWidth(widthMeasureSpec)
    	        var height=getMeasureHeight(heightMeasureSpec)
    	        setMeasuredDimension(width,height)
    	    }
    	
    	    /**
    	     * 对View的宽度进行测量
    	     */
    	    private fun getMeasureWidth(widthMeasureSpec: Int): Int {
    	        val mode=MeasureSpec.getMode(widthMeasureSpec)
    	        var width=MeasureSpec.getSize(widthMeasureSpec)
    	        when(mode){
    	            MeasureSpec.EXACTLY ->{//match_parent  100dp
    	            }
    	            MeasureSpec.AT_MOST ->{//wrap_content
    	                width=100
    	            }
    	            MeasureSpec.UNSPECIFIED ->{
    	            }
    	        }
    	        return  width
    	    }
    	    /**
    	     * 对View的高度进行测量
    	     */
    	    private fun getMeasureHeight(heightMeasureSpec: Int): Int {
    	        val mode=MeasureSpec.getMode(heightMeasureSpec)
    	        var height=MeasureSpec.getSize(heightMeasureSpec)
    	        when(mode){
    	            MeasureSpec.EXACTLY ->{//match_parent  100dp
    	            }
    	            MeasureSpec.AT_MOST ->{//wrap_content
    	                height=100
    	            }
    	            MeasureSpec.UNSPECIFIED ->{
    	            }
    	        }
    	        return  height
    	    }
    	
    	    /**
    	     * 对View进行绘制
    	     */
    	    override fun onDraw(canvas: Canvas) {
    	        super.onDraw(canvas)
    	        //1.计算中心点x,y的坐标
    	        val mCenterX=width/2
    	        val mCenterY=height/2
    	        //2.计算坐标  因为是圆,所以需要考虑设置的宽高不相等时的情况,在这里我们取最小边的尺寸,用是要处理padding的影响
    	        var mRadius=if(width>height) (height-paddingTop-paddingBottom-mOutCircleWidth*2)/2 else  (width-paddingLeft-paddingRight-mOutCircleWidth*2)/2
    	        //3.绘制内部圆
    	        //设置画笔填充模式
    	        mPaint.style=Paint.Style.FILL
    	        //设置抗锯齿
    	        mPaint.isAntiAlias=true
    	        //设置画笔绘制内容的颜色
    	        mPaint.color=mInCircleColor
    	        //绘制圆
    	        canvas.drawCircle(mCenterX.toFloat(), mCenterY.toFloat(),mRadius,mPaint)
    	        //4.绘制外部圆
    	        //设置画笔填充模式
    	        mPaint.style=Paint.Style.STROKE
    	        //设置画笔的宽度
    	        mPaint.strokeWidth=mOutCircleWidth
    	        //设置画笔绘制内容的颜色
    	        mPaint.color=mOutCircleColor
    	        //绘制
    	        canvas.drawCircle(mCenterX.toFloat(), mCenterY.toFloat(), mRadius.toFloat(),mPaint)
    	        //5.绘制文字
    	        if(!mShowText.isEmpty()) {
    	            mPaint.style = Paint.Style.FILL
    	            //设置颜色
    	            mPaint.color = mCountDownTextColor
    	            //设置中心对称
    	            mPaint.textAlign = Paint.Align.CENTER
    	            //设置字体的大小
    	            mPaint.textSize = mCountDownTextSize
    	            //设置字体颜色
    	            mPaint.color = mCountDownTextColor
    	            val mFontMetrics=mPaint.fontMetrics
    	            val bottom=mFontMetrics.bottom
    	            val top=mFontMetrics.top
    	            canvas.drawText(mShowText, mCenterX.toFloat(), mCenterY-(top+bottom)/2, mPaint)
    	        }
    	        //6.绘制环形
    	        if(mCurrentProgress>=0){
    	            val oval=RectF(mCenterX-mRadius,mCenterY-mRadius,mCenterX+mRadius,mCenterY+mRadius)
    	            //设置画笔颜色
    	            mPaint.color=mCountDownCircleColor
    	            //设置画笔填充模式
    	            mPaint.style=Paint.Style.STROKE
    	            //设置画笔的宽度
    	            mPaint.strokeWidth=mOutCircleWidth
    	            //画圆弧
    	            canvas.drawArc(oval,-90f, (360-360*mCurrentProgress/100).toFloat(),false,mPaint)
    	            if(mCurrentProgress==100){
    	                mListener?.onCountDownFinish()
    	            }
    	        }
    	    }
    	
    	
    	    /**
    	     * 当View被添加到界面时触发
    	     */
    	    override fun onAttachedToWindow() {
    	        super.onAttachedToWindow()
    	        if(mIsAutoPlayer) startCountDown()
    	        Log.d(TAG,"onAttachedToWindow:$mIsAutoPlayer")
    	    }
    	
    	
    	
    	
    	    /**
    	     * 倒计时开始
    	     */
    	      fun startCountDown() {
    	        if(mIsRunning) return
    	        mIsRunning=true
    	        mCurrentCountDownTime=0
    	        mCurrentProgress=0
    	        mShowText=mCountDownStr.toString()+"秒"
    	        postDelayed(this,50)
    	    }
    	
    	    /**
    	     * 当View从界面移除时触发
    	     */
    	    override fun onDetachedFromWindow() {
    	        super.onDetachedFromWindow()
    	        stopCountDown()
    	        Log.d(TAG,"onDetachedFromWindow")
    	    }
    	    /**
    	     * 倒计时停止
    	     */
    	      fun stopCountDown() {
    	        removeCallbacks(this)
    	        mIsRunning=false
    	        mCurrentCountDownTime=0
    	        mCurrentProgress=0
    	        mShowText=""
    	    }
    	
    	    /**
    	     * 1. 计算整个外部倒计时圆环的进度,整个进度最大值为100,
    	     * 2. 计算显示的时间,因为是50毫秒一次,所以相当于50毫秒时间要加一次,所以走的时间为mCurrentCountDownTime+=50/1000 单位为妙
    	     *    因为是剩余时间,所以要用总时间来减 mCountDownStr-mCurrentCountDownTime/1000
    	     */
    	    override fun run() {
    	        //计算显示的时间
    	        mCurrentCountDownTime+=50
    	        if(mCurrentCountDownTime%1000==0){
    	            mShowText=if((mCountDownStr-(mCurrentCountDownTime/1000))==0) "跳过" else (mCountDownStr-mCurrentCountDownTime/1000).toString()+"秒"
    	        }
    	        //计算进度
    	        mCurrentProgress+=5/(mCountDownStr*1.0f).toInt()
    	        //刷新
    	        invalidate()
    	        //再次发送延时消息
    	        if(mCurrentProgress>=100){
    	            removeCallbacks { this }
    	        }else{
    	            postDelayed(this,50)
    	        }
    	        Log.d(TAG,"mShowText:$mShowText ->mCurrentCountDownTime:$mCurrentCountDownTime")
    	
    	    }
    	
    	
    	    interface  OnCountDownListener{
    	        /**view点击事件回调*/
    	        fun  onViewClick(mCurrentTime:Int,mAllTime:Int):Boolean
    	        /**倒计时结束回调*/
    	        fun  onCountDownFinish()
    	    }
    	
    	    fun setCountDownListener(listener: OnCountDownListener){
    	        this.mListener=listener
    	    }
    	}
    
  • 布局

       <com.view.circleview.widget.CountdownCircleView
        android:id="@+id/mCustomCircleView"
        android:layout_width="80dp"
        android:layout_height="80dp"
        app:out_circle_color="#A9A9A9"
        app:in_circle_color="#D3D3D3"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"/>
    
  • 调用

      mCustomCircleView.setCountDownListener(object : CountdownCircleView.OnCountDownListener {
            override fun onViewClick(mCurrentTime: Int, mAllTime: Int):Boolean {
                startActivity(Intent(this@MainActivity,LoginActivity::class.java))
                return true
            }
    
            override fun onCountDownFinish() {
    
             }
    
        })
    
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值