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

xiaoxiao2025-04-09  14

先来看一下效果图

使用场景

有时候,在应用的启动页需要有一个圆形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() { } })
转载请注明原文地址: https://www.6miu.com/read-5027873.html

最新回复(0)