写在前面的话
话说kotlin被google确立正统地位(Android开发官方语言)已经快一年了,之前也断断续续学习过kotlin,但是由于实际项目开发中并未使用kotlin,因此kotlin并不熟悉。由于最近偶得闲暇,于是重拾kotlin,并计划使用kotlin重构之前写过的app,以此作为自己一段成长记录。
本文是kotlin实现的水平方向带进度条(支持动画)的自定义view。
1.效果图
不知不觉又扯远了,好了,回归正题,直接来看效果图:
2.实现思路
- 绘制默认进度条(默认进度条两端是半圆,中间是矩形)
- 绘制进度条经过的区域(左边是半圆,右边矩形,当进度条为100%时,最右的半圆也要绘制)
- 绘制进度条下方的说明文字以及实际进度数值
3.细节阐述
3.1概述
该View舍弃继承ProgressBar,而继承自View,实现高度的可定制,尤其是该进度条两端是一些花哨的几何图形时,该View两端暂定为半圆(当然,如果两端图形不计较是啥时,继承ProgressBar会更轻松),首先重写View的三个构造方法,并初始化必要参数,接着获取该View宽高以及必要计算参数,然后就是绘制该View了,最后重写触摸事件方法,让该View响应点击以及滑动事件。
3.2
通过res/values/attrs定义如下细节参数
<declare-styleable name="HProgressView">
<attr name="progress" format="float" />
<!--水平方向边距-->
<attr name="h_margin" format="dimension" />
<attr name="progress_color" format="color" />
<attr name="normal_color" format="color" />
<attr name="text_des_color" format="color" />
<attr name="text_num_color" format="color" />
<attr name="text_size" format="dimension" />
<!--进度条的纵向宽度-->
<attr name="progress_v_width" format="dimension" />
<attr name="text_to_progress_margin" format="dimension" />
<attr name="with_anim" format="boolean" />
<attr name="with_click" format="boolean" />
</declare-styleable>
3.3在构造方法中初始化attrs参数以及画笔paint
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
defProgressColor = ContextCompat.getColor(context, R.color.progress_color)
defNormalColor = ContextCompat.getColor(context, R.color.progress_normal_color)
defTxtDesColor = ContextCompat.getColor(context, R.color.progress_txt_des_color)
defTxtNumColor = ContextCompat.getColor(context, R.color.progress_txt_num_color)
defTxtSize = dip2px(16f)
defProgressVWidth = dip2px(20f)
defTxt2ProgressMargin = dip2px(30f)
defHMargin = dip2px(10f)
val a = context.obtainStyledAttributes(attrs, R.styleable.HProgressView)
try {
mProgress = a.getFloat(R.styleable.HProgressView_progress, 0f)
hMargin = a.getDimension(R.styleable.HProgressView_h_margin, defHMargin)
progressColor = a.getColor(R.styleable.HProgressView_progress_color, defProgressColor)
normalColor = a.getColor(R.styleable.HProgressView_normal_color, defNormalColor)
txtDesColor = a.getColor(R.styleable.HProgressView_text_des_color, defTxtDesColor)
txtNumColor = a.getColor(R.styleable.HProgressView_text_num_color, defTxtNumColor)
txtSize = a.getDimension(R.styleable.HProgressView_text_size, defTxtSize)
progressVWidth = a.getDimension(R.styleable.HProgressView_progress_v_width, defProgressVWidth)
txt2ProgressMargin = a.getDimension(R.styleable.HProgressView_text_to_progress_margin, defTxt2ProgressMargin)
withAnim = a.getBoolean(R.styleable.HProgressView_with_anim, true)
} finally {
a.recycle()
}
if (mProgress < 0) mProgress = 0f
if (mProgress > 100) mProgress = 100f
normalPaint.color = normalColor
normalPaint.style = Paint.Style.FILL
progressPaint.color = progressColor
progressPaint.style = Paint.Style.FILL
txtPaint.style = Paint.Style.FILL
txtPaint.textSize = txtSize
bounds = Rect()
}
3.4在onSizeChanged方法中,获取该View宽高以及必要计算参数
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = w
mHeight = h
cirlceRadius = progressVWidth / 2
startCenterX = cirlceRadius + hMargin
endCenterX = mWidth - cirlceRadius - hMargin
startCenterY = mHeight / 2 - txt2ProgressMargin / 2
endCenterY = startCenterY
top = startCenterY - cirlceRadius
bottom = startCenterY + cirlceRadius
}
3.5绘制View
3.5.1普通进度条
private fun drawNormal(canvas: Canvas) {
recLeftCircle = RectF(hMargin, top, hMargin + progressVWidth, bottom)
canvas.drawArc(recLeftCircle, 90f, 180f, true, normalPaint)
recRightCircle = RectF(mWidth - progressVWidth - hMargin, top, mWidth - hMargin, bottom)
canvas.drawArc(recRightCircle, -90f, 180f, true, normalPaint)
rectProgressArea = RectF(hMargin + cirlceRadius, top, mWidth - hMargin - cirlceRadius, bottom)
canvas.drawRect(rectProgressArea, normalPaint)
}
3.5.2完成进度条
private fun drawProgress(canvas: Canvas) {
if (mProgress > 0) {
canvas.drawArc(recLeftCircle, 90f, 180f, true, progressPaint)
if (mProgress >= 0.5f) {//为了防止mprogress为0.1-0.5时左边绘制有误
val left = hMargin + cirlceRadius
val right = (mWidth - 2 * hMargin - progressVWidth) * mProgress / 100 + hMargin + cirlceRadius
rectProgressPass = RectF(left, top, right, bottom)
canvas.drawRect(rectProgressPass, progressPaint)
rectProgressPass = RectF(right - cirlceRadius, top, right + cirlceRadius, bottom)
canvas.drawArc(rectProgressPass, -90f, 180f, true, progressPaint)
}
}
}
3.5.3说明文字以及进度数值
private fun drawDesTxt(canvas: Canvas) {
val des = "已完成${mProgress}%"
txtPaint.getTextBounds(des, 0, des.length, bounds)
//获取整个说明文字宽高
val desW = bounds.width()
val desH = bounds.height()
//获取“已完成”三个字宽度
val lw = txtPaint.measureText(des, 0, 3)
txtPaint.color = txtDesColor
canvas.drawText(des, 0, 3, mWidth / 2 - desW / 2f, mHeight / 2 +
defTxt2ProgressMargin / 2 + desH / 2, txtPaint)
txtPaint.color = txtNumColor
canvas.drawText(des, 3, des.length, mWidth / 2 - desW / 2f + lw,
mHeight / 2 + defTxt2ProgressMargin / 2 + desH / 2, txtPaint)
}
3.6处理View添加Touch事件
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> dealMotionEvent(event.x, event.y, true)//默认滑动事件始终伴随动画
MotionEvent.ACTION_UP -> dealMotionEvent(event.x, event.y, withAnim)
}
return true
}
private fun dealMotionEvent(x: Float, y: Float, withAnim: Boolean) {
val rectLeftHalf = RectF(hMargin, top, hMargin + cirlceRadius, bottom)
val rectRightHalf = RectF(mWidth - hMargin - cirlceRadius, top, mWidth - hMargin, bottom)
if (rectLeftHalf.contains(x, y)) setProgress(0f)
if (rectRightHalf.contains(x, y)) setProgress(100f)
if (rectProgressArea.contains(x, y)) {
val progress = (x - hMargin - cirlceRadius) / (mWidth - 2 * hMargin - progressVWidth) * 100f
setProgress(String.format("%.1f", progress).toFloat(), withAnim)
}
}
3.7设置对外调用接口
fun setWithAnim(withAnim: Boolean) {//设置是否支持动画
this.withAnim = withAnim
}
fun setProgress(progress: Float) = setProgress(progress, withAnim)//方法重载
fun setProgress(progress: Float, withAnim: Boolean) {//方法重载
if (progress < 0) mProgress = 0f
if (progress > 100) mProgress = 100f
if (withAnim) {
val animator = ValueAnimator.ofFloat(mProgress, progress)
animator.addUpdateListener { anim ->
mProgress = String.format("%.1f", anim.animatedValue).toFloat()
invalidate()
}
animator.interpolator = LinearInterpolator()
animator.duration = (Math.abs(mProgress - progress) * 50).toLong()
animator.start()
} else {
mProgress = progress
invalidate()
}
}
4.调用
4.1xml调用
<com.ddlc.hprogress.HProgressView
android:id="@+id/hpv_progress"
android:layout_width="match_parent"
android:layout_height="100dp"
tangbuzhi:h_margin="20dp"
tangbuzhi:normal_color="#66000000"
tangbuzhi:progress="34.5"
tangbuzhi:progress_color="#ff00ff"
tangbuzhi:progress_v_width="15dp"
tangbuzhi:text_des_color="#000fff"
tangbuzhi:text_num_color="#ff00ff"
tangbuzhi:text_size="16dp"
tangbuzhi:text_to_progress_margin="30dp"
tangbuzhi:with_anim="false"
tangbuzhi:with_click="false" />
4.2java调用
hpv_progress.setWithAnim(true)
hpv_progress.setWithClick(true)
hpv_progress.setProgress(progress, true)