需求分析
- UI效果如下
这里的目的是实现一个类似刻度尺一样的自定义View,看起来总共分为三部分
三角形,蓝色圆角矩形,白色圆角矩形,实际上是两个部分-蓝色和白色
实现
因此这里通过自定义属性来设置背景和进度的颜色
- 自定义属性如下
分别是背景色,前景进度色,进度的最小值,进度的最大值,进度的当前值
<declare-styleable name="SeekStripeView">
<attr name="progressBgColor" format="color" />
<attr name="progressFgColor" format="color" />
<attr name="progress_min" format="integer" />
<attr name="progress_max" format="integer" />
<attr name="progress_cur" format="integer" />
</declare-styleable>
- 然后自定义view的代码获取设置的属性值(知识点-context.withStyledAttributes方法是一个扩展函数)
init {
context.withStyledAttributes(attributeSet, R.styleable.SeekStripeView) {
setMin(getInt(R.styleable.SeekStripeView_progress_min, mMin))
setMax(getInt(R.styleable.SeekStripeView_progress_max, mMax))
setProgress(getInt(R.styleable.SeekStripeView_progress_cur, mProgress))
progressBgColor = getInt(R.styleable.SeekStripeView_progressBgColor, progressBgColor)
progressFgColor = getInt(R.styleable.SeekStripeView_progressFgColor, progressFgColor)
}
mPaint.style = Paint.Style.FILL
mPaint.color = progressBgColor
bgPath = Path()
fgPath = Path()
}
//扩展函数 Context.withStyledAttributes的源码
/**
* Executes [block] on a [TypedArray] receiver. The [TypedArray] holds the attribute
* values in [set] that are listed in [attrs]. In addition, if the given [AttributeSet]
* specifies a style class (through the `style` attribute), that style will be applied
* on top of the base attributes it defines.
*
* @param set The base set of attribute values.
* @param attrs The desired attributes to be retrieved. These attribute IDs must be
* sorted in ascending order.
* @param defStyleAttr An attribute in the current theme that contains a reference to
* a style resource that supplies defaults values for the [TypedArray].
* Can be 0 to not look for defaults.
* @param defStyleRes A resource identifier of a style resource that supplies default values
* for the [TypedArray], used only if [defStyleAttr] is 0 or can not be found
* in the theme. Can be 0 to not look for defaults.
*
* @see Context.obtainStyledAttributes
* @see android.content.res.Resources.Theme.obtainStyledAttributes
* 这里主要就是帮你添加了回收的方法,避免写代码时忘记导致内存泄露,点赞..
*/
public inline fun Context.withStyledAttributes(
set: AttributeSet? = null,
attrs: IntArray,
@AttrRes defStyleAttr: Int = 0,
@StyleRes defStyleRes: Int = 0,
block: TypedArray.() -> Unit
) {
obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes).apply(block).recycle()
}
- 添加好属性之后需要根据控件的宽高计算所需要画出的格子数,这里我们在onSizeChanged方法得到控件的宽高,然后最主要的方法是computeProgressPath方法
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mWidth = (w + paddingLeft + paddingRight)
mHeight = (h + paddingTop + paddingBottom)
computeProgressPath()
}
private fun computeProgressPath() {
//根据进度和最大最小值得到当前的百分比
val percentWidth: Float = (mProgress - mMin) / (mMax - mMin).toFloat()
LogInfo(TAG, "percentWidth = $percentWidth")
//根据控件的宽高和每个小矩形的宽高计算出要总共要多少个矩形-这个就是背景色了
val totalSum = (mWidth / (mBlockSpace + mBlockWidth))
//然后根据进度百分比得到进度有多少个矩形
val progressSum = (percentWidth * totalSum).roundToInt()
LogInfo(TAG, "sum = $totalSum ,progressSum = $progressSum")
//每次设置路径前都需要先重置,否则不会生效
fgPath.reset()
bgPath.reset()
//为了避免重复绘制,所以这里有分成两个path 一个是进度一个是背景
for (i in 0..totalSum) {
val left = (mBlockWidth + mBlockSpace) * i
val right = left + mBlockWidth
val top = thumbHeight
val bottom = mHeight
val rectF = RectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
when {
i < progressSum -> fgPath.addRoundRect(rectF, 5f, 5f, Path.Direction.CCW)
i == progressSum -> {
fgPath.addRoundRect(rectF, 5f, 5f, Path.Direction.CCW)
//三角标的位置在最后一个进度矩形上
fgPath.moveTo(left.toFloat(), 0f)
fgPath.lineTo(right.toFloat(), 0f)
fgPath.lineTo(right - (mBlockWidth.toFloat() / 2), thumbHeight.toFloat())
}
else -> {
bgPath.addRoundRect(rectF, 5f, 5f, Path.Direction.CCW)
}
}
}
bgPath.close()
fgPath.close()
}
- 路径添加完成之后,就是绘制路径了方法很简单重新onDraw方法即可
override fun onDraw(canvas: Canvas) {
mPaint.color = progressBgColor
canvas.drawPath(bgPath, mPaint)
mPaint.color = progressFgColor
canvas.drawPath(fgPath, mPaint)
}
- 因为它是一个可滑动的view 所以当然少不了重写触摸事件了
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
if (!isEnabled) {
return false
}
when (event.action) {
MotionEvent.ACTION_DOWN -> {
mOnProgressChangeListener?.onStartTrackingTouch(this)
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
mOnProgressChangeListener?.onStopTrackingTouch(this)
}
MotionEvent.ACTION_MOVE -> {
var x = event.x
if (x > mWidth) {
x = mWidth.toFloat()
}
if (x < 0) {
x = 0f
}
/**
* (progress-mMin)/(mMax-mMin) = x/width
* so progress = (x *(max-min)/width)+min
*/
var ctrlProgress =
(x * (mMax - mMin) / mWidth).toInt() + mMin
if (ctrlProgress < mMin) {
ctrlProgress = mMin
}
if (ctrlProgress != mProgress) {
mOnProgressChangeListener?.onProgressChanged(this, ctrlProgress, true)
mProgress = ctrlProgress
computeProgressPath()
postInvalidate()
}
}
}
return true
}
实现的效果
哈哈,对自定义View的印象又加深了一点,感谢观看,欢迎点赞