最近在做电动车的充电功能,其中有个充电过程中,隔一段时间去更新充电状态的功能。充电过程中,相对于数据的改变,电池的电量更受用户关注。所以这里面就涉及到自定义View,整个过程主要涉及到测量和绘制,这其中又包括背景颜色绘制、线条的绘制、文本绘制、多边形的绘制。
测量
测试量设置一个宽高最小值,并且整个自定义包含电池,剩余部分有文字绘制等,因此电池的宽高并不是测量的宽高。如果对测量模糊的,建议去学一下MeasureSpec,这是我之前写的一篇文章
MeasureSpec理解
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val dw = MIN_WIDTH + paddingLeft + paddingRight
val dh = MIN_HEIGHT + paddingTop + paddingBottom
measureWidth = resolveSizeAndState(dw, widthMeasureSpec, 0)
measureHeight = resolveSizeAndState(dh, heightMeasureSpec, 0)
batteryHeight = measureHeight / 8 * 5
batteryWidth = if (measureWidth / 10 < limitTextWidth / 2) {
measureWidth - (limitTextWidth / 2 - measureWidth / 10) - LIMIT_TEXT_OFFSET_X
} else {
measureWidth - LIMIT_TEXT_OFFSET_X
}
setMeasuredDimension(measureWidth, measureHeight)
}
多边形绘制
因为充电的电池属于不是长方形,因此不能使用绘制长方形的方式。通过计算进度过程中各个点的坐标,连接各个点使之形成一个不规则图形。项目中主要是绘制等腰梯形,计算每个进度下的四个坐标点,当进度有更新的时候去更新path路径,最后合闭路径形成梯形。主要代码如下:
/**
* 绘制进度
*
* @param canvas
* @param path
* @param paint
*/
private fun drawProgress(canvas: Canvas, path: Path, paint: Paint) {
path.reset()
path.moveTo(OFFSET, FLOAT_0)
path.lineTo(FLOAT_0, batteryHeight.toFloat())
path.lineTo((batteryWidth * progress / max).toFloat(), batteryHeight.toFloat())
path.lineTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.lineTo(OFFSET, FLOAT_0)
path.close()
canvas.drawPath(path, paint)
}
说明:其中OFFSET是为了形成等腰梯形设置一个偏移量,当OFFSET为0时候则是长方形。
线条绘制
线条的绘制就很简单,直接调drawLine()方法,其中关于虚线的绘制,需要设置Paint的属性pathEffect,其中填虚实线各占多少。
/**
* 绘制虚线
*
* @param canvas
* @param paint
*/
private fun drawDashLine(canvas: Canvas, paint: Paint) {
val startX = (batteryWidth - OFFSET * 2) / 10f * 9 + OFFSET
val endX = batteryWidth / 10f * 9
canvas.drawLine(startX, FLOAT_0, endX, batteryHeight.toFloat(), paint)
}
文本的绘制
文字的绘制与其他图形绘制最大的不同就是其绘制的坐标点,文本的绘制是找文本的左下坐标为基点。并且文本的绘制还涉及到文本居中的问题,文本的绘制是以textbaseline为Y方向的坐标,这样才能使文本在垂直方向上居中。其中找textbaseline有两种方式:
- 第一种:测算获取文本的高度,这种适合文本不再更改的情况
- 第二种:测算文本FontMetrics属性,然后计算其baseline
/**
* 绘制进度文本
*
* @param canvas
* @param paint
*/
private fun drawProgressText(canvas: Canvas, paint: Paint) {
progressText = String.format(context.getString(R.string.charging_precent_placeholder), progress)
val textBaseLine = batteryHeight / 2 - (progressFontMetrics.descent + progressFontMetrics.ascent) / 2
canvas.drawText(progressText, 0, progressText.length, (batteryWidth - progressTextWidth) / 2.toFloat(), textBaseLine, paint)
}
状态保存
自定义View的话有些时候遇到横竖屏切换,状态就会改变,因此像系统的很多View组件都是做了状态保存功能,如果对View状态保存不是太熟悉的可以上网搜一下相关的文章看一下。
//*************保存进度状态**********************
override fun onSaveInstanceState(): Parcelable {
val parcelable = super.onSaveInstanceState()
val ss = SavedState(parcelable)
ss.progress = progress
return ss
}
override fun onRestoreInstanceState(state: Parcelable) {
val ss = state as SavedState
super.onRestoreInstanceState(ss.superState)
progress = ss.progress
}
internal class SavedState : BaseSavedState {
var progress = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(save: Parcel) : super(save) {
progress = save.readInt()
}
override fun writeToParcel(restore: Parcel, flags: Int) {
super.writeToParcel(restore, flags)
restore.writeValue(progress)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
完整代码
代码
class ChargingProgressView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
companion object {
//属性默认值
const val PROGRESS = 10 //默认进度
const val LIMIT_MAX = 90 //进度最大值默认
const val PROGRESS_MAX = 100 //默认最大进度
const val BACKGROUND_COLOR = Color.GRAY //默认背景颜色
const val PROGRESS_COLOR = Color.GREEN //默认进度颜色
const val SECONDARY_START_COLOR = Color.TRANSPARENT //默认灰色进度开始颜色
const val SECONDARY_END_COLOR = Color.GREEN //默认灰色进度结束颜色
const val LINE_COLOR = Color.WHITE //默认灰色进度结束颜色
const val LINE_WIDTH = 1F //默认线条宽度
const val PROGRESS_TEXT_COLOR = Color.WHITE //默认进度文本字体颜色
const val PROGRESS_TEXT_SIZE = 15F //默认进度文本字体大小
const val LIMIT_TEXT_COLOR = Color.WHITE //默认限制进度文本字体颜色
const val LIMIT_TEXT_SIZE = 15F //默认限制进度文本字体大小
//参数默认值
const val MIN_WIDTH = 400 //默认最小宽度
const val MIN_HEIGHT = 80 //默认最小宽度
const val OFFSET = 0F //上部偏移量
const val FLOAT_0 = 0F // float 0
const val TRIANGLE_HEIGHT = 10 //三角形高度
const val LIMIT_TEXT_OFFSET_X = 5 //限制进度文本X偏移量
const val LIMIT_TEXT_OFFSET_Y = 5 //限制进度文本Y偏移量
}
//自定义属性
private var max: Int
private val backgroundColor: Int
private val progressColor: Int
private val secondaryStartColor: Int
private val secondaryEndColor: Int
private val lineColor: Int
private val lineWidth: Float
private val progressTextColor: Int
private val progressTextSize: Float
private val limitTextColor: Int
private val limitTextSize: Float
//可设置属性 目前只设置这两种
var limit: Int = LIMIT_MAX
set(value) {
if (value < 0 || value >= max || value == limit) {
return
}
field = value
invalidate()
}
var progress: Int = PROGRESS
set(value) {
if (value < 0 || value > limit || value == progress) {
return
}
field = value
invalidate()
}
//参数
private var measureWidth = 0 //宽度
private var measureHeight = 0 //高度
private var batteryWidth = 0 //电池的宽度
private var batteryHeight = 0 //电池的高度
//绘制背景
private var backgroundPath: Path
private var backgroundPaint: Paint
//绘制线条
private var linePaint: Paint
private var dashLinePaint: Paint
//绘制灰色进度
private var secondaryPath: Path
private var secondaryPaint: Paint
//绘制进度
private var progressPath: Path
private var progressPaint: Paint
//绘制进度文字
private var progressText: String
private var progressTextWidth = 0
private val progressTextPaint: Paint
private val progressTextBound: Rect
private var progressFontMetrics: FontMetrics
//绘制三角形标签
private val trianglePaint: Paint
private val trianglePath: Path
private val triangleHeight = TRIANGLE_HEIGHT
//绘制进度上限文本
private var limitText: String
private var limitTextWidth = 0
private var limitTextHeight = 0
private val limitTextBound: Rect
private val limitTextPaint: Paint
private val limitFontMetrics: FontMetrics
init {
val ta = context.obtainStyledAttributes(attrs, R.styleable.ChargingProgressView)
progress = ta.getInteger(R.styleable.ChargingProgressView_charging_progress, PROGRESS)
max = ta.getInteger(R.styleable.ChargingProgressView_charging_max, PROGRESS_MAX)
limit = ta.getInteger(R.styleable.ChargingProgressView_charging_limit, LIMIT_MAX)
backgroundColor = ta.getColor(R.styleable.ChargingProgressView_charging_backgroundColor, BACKGROUND_COLOR)
progressColor = ta.getColor(R.styleable.ChargingProgressView_charging_progressColor, PROGRESS_COLOR)
secondaryStartColor = ta.getColor(R.styleable.ChargingProgressView_charging_secondaryStartColor, SECONDARY_START_COLOR)
secondaryEndColor = ta.getColor(R.styleable.ChargingProgressView_charging_secondaryEndColor, SECONDARY_END_COLOR)
lineColor = ta.getColor(R.styleable.ChargingProgressView_charging_lineColor, LINE_COLOR)
lineWidth = ta.getFloat(R.styleable.ChargingProgressView_charging_lineWidth, LINE_WIDTH)
progressTextColor = ta.getColor(R.styleable.ChargingProgressView_charging_progressTextColor, PROGRESS_TEXT_COLOR)
progressTextSize = ta.getFloat(R.styleable.ChargingProgressView_charging_progressTextSize, PROGRESS_TEXT_SIZE)
limitTextColor = ta.getColor(R.styleable.ChargingProgressView_charging_limitTextColor, LIMIT_TEXT_COLOR)
limitTextSize = ta.getFloat(R.styleable.ChargingProgressView_charging_limitTextSize, LIMIT_TEXT_SIZE)
ta.recycle()
//绘制背景
backgroundPath = Path()
backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG)
backgroundPaint.color = backgroundColor
backgroundPaint.style = Paint.Style.FILL
//绘制线条
linePaint = Paint(Paint.ANTI_ALIAS_FLAG)
linePaint.color = lineColor
linePaint.strokeWidth = lineWidth
linePaint.style = Paint.Style.FILL
dashLinePaint = Paint(Paint.ANTI_ALIAS_FLAG)
dashLinePaint.color = lineColor
dashLinePaint.strokeWidth = lineWidth
dashLinePaint.pathEffect = DashPathEffect(floatArrayOf(5f, 5f), FLOAT_0)
dashLinePaint.style = Paint.Style.FILL
//绘制灰色进度
secondaryPath = Path()
secondaryPaint = Paint(Paint.ANTI_ALIAS_FLAG)
secondaryPaint.style = Paint.Style.FILL
//绘制进度
progressPath = Path()
progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
progressPaint.color = progressColor
progressPaint.style = Paint.Style.FILL
//绘制进度文字
progressTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
progressTextPaint.color = progressTextColor
progressTextPaint.textSize = progressTextSize
progressText = String.format(context.getString(R.string.charging_precent_placeholder), progress)
progressTextBound = Rect()
progressTextPaint.getTextBounds(progressText, 0, progressText.length, progressTextBound)
progressTextWidth = progressTextBound.width()
progressFontMetrics = progressTextPaint.fontMetrics
//绘制三角形标签
trianglePath = Path()
trianglePaint = Paint(Paint.ANTI_ALIAS_FLAG)
trianglePaint.color = Color.WHITE
trianglePaint.style = Paint.Style.FILL
//绘制进度上限文本
limitTextPaint = Paint(Paint.ANTI_ALIAS_FLAG)
limitTextPaint.color = limitTextColor
limitTextPaint.textSize = limitTextSize
limitText = String.format(context.getString(R.string.charging_limit_max), limit)
limitTextBound = Rect()
limitTextPaint.getTextBounds(limitText, 0, limitText.length, limitTextBound)
limitTextWidth = limitTextBound.width()
limitTextHeight = limitTextBound.height()
limitFontMetrics = limitTextPaint.fontMetrics
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val dw = MIN_WIDTH + paddingLeft + paddingRight
val dh = MIN_HEIGHT + paddingTop + paddingBottom
measureWidth = resolveSizeAndState(dw, widthMeasureSpec, 0)
measureHeight = resolveSizeAndState(dh, heightMeasureSpec, 0)
batteryHeight = measureHeight / 8 * 5
batteryWidth = if (measureWidth / 10 < limitTextWidth / 2) {
measureWidth - (limitTextWidth / 2 - measureWidth / 10) - LIMIT_TEXT_OFFSET_X
} else {
measureWidth - LIMIT_TEXT_OFFSET_X
}
setMeasuredDimension(measureWidth, measureHeight)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//先绘制固定背景色
drawBackground(canvas, backgroundPath, backgroundPaint)
//绘制线条
drawLine(canvas, linePaint)
//绘制进度限制线条
drawDashLine(canvas, dashLinePaint)
//绘制灰色进度
drawSecondaryProgress(canvas, secondaryPath, secondaryPaint)
//绘制进度
drawProgress(canvas, progressPath, progressPaint)
//绘制进度文字
drawProgressText(canvas, progressTextPaint)
//绘制三角形标签
drawTriangle(canvas, trianglePath, trianglePaint)
//绘制最大阈值的文本
drawLimitText(canvas, limitTextPaint)
}
/**
* 绘制进度限度文本
*
* @param canvas
* @param paint
*/
private fun drawLimitText(canvas: Canvas, paint: Paint) {
limitText = String.format(context.getString(R.string.charging_limit_max), limit)
val textBaseLine = batteryHeight + triangleHeight +
limitFontMetrics.descent - limitFontMetrics.ascent + LIMIT_TEXT_OFFSET_Y
canvas.drawText(limitText, 0, limitText.length, (batteryWidth * limit / max - limitTextWidth / 2).toFloat(), textBaseLine, paint)
}
/**
* 绘制进度限度三角标
*
* @param canvas
* @param path
* @param paint
*/
private fun drawTriangle(canvas: Canvas, path: Path, paint: Paint) {
path.reset()
path.moveTo((batteryWidth * limit / max).toFloat(), (batteryHeight).toFloat())
path.lineTo((batteryWidth * limit / max + triangleHeight / 2).toFloat(), (batteryHeight + triangleHeight).toFloat())
path.lineTo((batteryWidth * limit / max - triangleHeight / 2).toFloat(), (batteryHeight + triangleHeight).toFloat())
path.lineTo((batteryWidth * limit / max).toFloat(), (batteryHeight).toFloat())
path.close()
canvas.drawPath(path, paint)
}
/**
* 绘制进度文本
*
* @param canvas
* @param paint
*/
private fun drawProgressText(canvas: Canvas, paint: Paint) {
progressText = String.format(context.getString(R.string.charging_precent_placeholder), progress)
val textBaseLine = batteryHeight / 2 - (progressFontMetrics.descent + progressFontMetrics.ascent) / 2
canvas.drawText(progressText, 0, progressText.length, (batteryWidth - progressTextWidth) / 2.toFloat(), textBaseLine, paint)
}
/**
* 绘制进度
*
* @param canvas
* @param path
* @param paint
*/
private fun drawProgress(canvas: Canvas, path: Path, paint: Paint) {
path.reset()
path.moveTo(OFFSET, FLOAT_0)
path.lineTo(FLOAT_0, batteryHeight.toFloat())
path.lineTo((batteryWidth * progress / max).toFloat(), batteryHeight.toFloat())
path.lineTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.lineTo(OFFSET, FLOAT_0)
path.close()
canvas.drawPath(path, paint)
}
/**
* 绘制灰色进度
*
* @param canvas
* @param path
* @param paint
*/
private fun drawSecondaryProgress(canvas: Canvas, path: Path, paint: Paint) {
path.reset()
path.moveTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.lineTo((batteryWidth - OFFSET * 2) * limit / max + OFFSET, FLOAT_0)
path.lineTo((batteryWidth * limit / max).toFloat(), batteryHeight.toFloat())
path.lineTo((batteryWidth * progress / max).toFloat(), batteryHeight.toFloat())
path.lineTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.close()
paint.shader = LinearGradient(
FLOAT_0, FLOAT_0, batteryWidth.toFloat(),
FLOAT_0, intArrayOf(secondaryStartColor, secondaryEndColor),
null, Shader.TileMode.CLAMP
)
canvas.drawPath(path, paint)
}
/**
* 绘制虚线
*
* @param canvas
* @param paint
*/
private fun drawDashLine(canvas: Canvas, paint: Paint) {
val startX = (batteryWidth - OFFSET * 2) / 10f * 9 + OFFSET
val endX = batteryWidth / 10f * 9
canvas.drawLine(startX, FLOAT_0, endX, batteryHeight.toFloat(), paint)
}
/**
* 绘制线条
* @param canvas
* @param paint
*/
private fun drawLine(canvas: Canvas, paint: Paint) {
for (i in 0..8) {
if (i + 1 != limit * 10 / max) {
val startX = (batteryWidth - 2 * OFFSET) / 10f * (i + 1) + OFFSET
val endX = batteryWidth / 10f * (i + 1).toFloat()
canvas.drawLine(startX, FLOAT_0, endX, batteryHeight.toFloat(), paint)
}
}
}
/**
* 绘制背景
* @param canvas
* @param path
* @param paint
*/
private fun drawBackground(canvas: Canvas, path: Path, paint: Paint) {
path.reset()
path.moveTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.lineTo((batteryWidth * progress / max).toFloat(), batteryHeight.toFloat())
path.lineTo(batteryWidth.toFloat(), batteryHeight.toFloat())
path.lineTo(batteryWidth - OFFSET, FLOAT_0)
path.lineTo((batteryWidth - OFFSET * 2) * progress / max + OFFSET, FLOAT_0)
path.close()
canvas.drawPath(path, paint)
}
//*************保存进度状态**********************
override fun onSaveInstanceState(): Parcelable {
val parcelable = super.onSaveInstanceState()
val ss = SavedState(parcelable)
ss.progress = progress
return ss
}
override fun onRestoreInstanceState(state: Parcelable) {
val ss = state as SavedState
super.onRestoreInstanceState(ss.superState)
progress = ss.progress
}
internal class SavedState : BaseSavedState {
var progress = 0
constructor(superState: Parcelable?) : super(superState)
private constructor(save: Parcel) : super(save) {
progress = save.readInt()
}
override fun writeToParcel(restore: Parcel, flags: Int) {
super.writeToParcel(restore, flags)
restore.writeValue(progress)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
}
自定义属性
<declare-styleable name="ChargingProgressView">
<!--进度-->
<attr name="charging_progress" format="integer" />
<!--最大进度-->
<attr name="charging_max" format="integer" />
<!--最大阈值-->
<attr name="charging_limit" format="integer" />
<!--进度条颜色-->
<attr name="charging_progressColor" format="color|reference" />
<!--灰色进度条开始颜色-->
<attr name="charging_secondaryStartColor" format="color|reference" />
<!--灰色进度条结束颜色-->
<attr name="charging_secondaryEndColor" format="color|reference" />
<!--背景颜色-->
<attr name="charging_backgroundColor" format="color|reference" />
<!--线条颜色-->
<attr name="charging_lineColor" format="color|reference" />
<!--线条粗细-->
<attr name="charging_lineWidth" format="float|reference" />
<!--进度文本颜色-->
<attr name="charging_progressTextColor" format="color|reference" />
<!--进度文本字体大小-->
<attr name="charging_progressTextSize" format="float|reference" />
<!--限制进度文本颜色-->
<attr name="charging_limitTextColor" format="color|reference" />
<!--进度文本字体大小-->
<attr name="charging_limitTextSize" format="float|reference" />
</declare-styleable>
网友评论