Android 自定義電動(dòng)車充電進(jìn)度條

??最近在做電動(dòng)車的充電功能,其中有個(gè)充電過程中,隔一段時(shí)間去更新充電狀態(tài)的功能。充電過程中,相對(duì)于數(shù)據(jù)的改變,電池的電量更受用戶關(guān)注。所以這里面就涉及到自定義View,整個(gè)過程主要涉及到測(cè)量和繪制,這其中又包括背景顏色繪制、線條的繪制、文本繪制、多邊形的繪制。

測(cè)量

??測(cè)試量設(shè)置一個(gè)寬高最小值,并且整個(gè)自定義包含電池,剩余部分有文字繪制等,因此電池的寬高并不是測(cè)量的寬高。如果對(duì)測(cè)量模糊的,建議去學(xué)一下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)
    }

多邊形繪制

??因?yàn)槌潆姷碾姵貙儆诓皇情L方形,因此不能使用繪制長方形的方式。通過計(jì)算進(jìn)度過程中各個(gè)點(diǎn)的坐標(biāo),連接各個(gè)點(diǎn)使之形成一個(gè)不規(guī)則圖形。項(xiàng)目中主要是繪制等腰梯形,計(jì)算每個(gè)進(jìn)度下的四個(gè)坐標(biāo)點(diǎn),當(dāng)進(jìn)度有更新的時(shí)候去更新path路徑,最后合閉路徑形成梯形。主要代碼如下:

/**
     * 繪制進(jìn)度
     *
     * @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是為了形成等腰梯形設(shè)置一個(gè)偏移量,當(dāng)OFFSET為0時(shí)候則是長方形。

線條繪制

??線條的繪制就很簡(jiǎn)單,直接調(diào)drawLine()方法,其中關(guān)于虛線的繪制,需要設(shè)置Paint的屬性pathEffect,其中填虛實(shí)線各占多少。

/**
     * 繪制虛線
     *
     * @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)
    }

文本的繪制

??文字的繪制與其他圖形繪制最大的不同就是其繪制的坐標(biāo)點(diǎn),文本的繪制是找文本的左下坐標(biāo)為基點(diǎn)。并且文本的繪制還涉及到文本居中的問題,文本的繪制是以textbaseline為Y方向的坐標(biāo),這樣才能使文本在垂直方向上居中。其中找textbaseline有兩種方式:

  • 第一種:測(cè)算獲取文本的高度,這種適合文本不再更改的情況
  • 第二種:測(cè)算文本FontMetrics屬性,然后計(jì)算其baseline
/**
     * 繪制進(jìn)度文本
     *
     * @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)
    }

狀態(tài)保存

自定義View的話有些時(shí)候遇到橫豎屏切換,狀態(tài)就會(huì)改變,因此像系統(tǒng)的很多View組件都是做了狀態(tài)保存功能,如果對(duì)View狀態(tài)保存不是太熟悉的可以上網(wǎng)搜一下相關(guān)的文章看一下。

//*************保存進(jìn)度狀態(tài)**********************
    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 {
        //屬性默認(rèn)值
        const val PROGRESS = 10                                //默認(rèn)進(jìn)度
        const val LIMIT_MAX = 90                               //進(jìn)度最大值默認(rèn)
        const val PROGRESS_MAX = 100                           //默認(rèn)最大進(jìn)度
        const val BACKGROUND_COLOR = Color.GRAY                //默認(rèn)背景顏色
        const val PROGRESS_COLOR = Color.GREEN                 //默認(rèn)進(jìn)度顏色
        const val SECONDARY_START_COLOR = Color.TRANSPARENT    //默認(rèn)灰色進(jìn)度開始顏色
        const val SECONDARY_END_COLOR = Color.GREEN            //默認(rèn)灰色進(jìn)度結(jié)束顏色
        const val LINE_COLOR = Color.WHITE                     //默認(rèn)灰色進(jìn)度結(jié)束顏色
        const val LINE_WIDTH = 1F                              //默認(rèn)線條寬度
        const val PROGRESS_TEXT_COLOR = Color.WHITE            //默認(rèn)進(jìn)度文本字體顏色
        const val PROGRESS_TEXT_SIZE = 15F                     //默認(rèn)進(jìn)度文本字體大小
        const val LIMIT_TEXT_COLOR = Color.WHITE               //默認(rèn)限制進(jìn)度文本字體顏色
        const val LIMIT_TEXT_SIZE = 15F                        //默認(rèn)限制進(jìn)度文本字體大小

        //參數(shù)默認(rèn)值
        const val MIN_WIDTH = 400           //默認(rèn)最小寬度
        const val MIN_HEIGHT = 80           //默認(rèn)最小寬度
        const val OFFSET = 0F               //上部偏移量
        const val FLOAT_0 = 0F              // float 0
        const val TRIANGLE_HEIGHT = 10      //三角形高度
        const val LIMIT_TEXT_OFFSET_X = 5   //限制進(jìn)度文本X偏移量
        const val LIMIT_TEXT_OFFSET_Y = 5   //限制進(jìn)度文本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

    //可設(shè)置屬性 目前只設(shè)置這兩種
    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()
        }

    //參數(shù)
    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

    //繪制灰色進(jìn)度
    private var secondaryPath: Path
    private var secondaryPaint: Paint

    //繪制進(jìn)度
    private var progressPath: Path
    private var progressPaint: Paint

    //繪制進(jìn)度文字
    private var progressText: String
    private var progressTextWidth = 0
    private val progressTextPaint: Paint
    private val progressTextBound: Rect
    private var progressFontMetrics: FontMetrics

    //繪制三角形標(biāo)簽
    private val trianglePaint: Paint
    private val trianglePath: Path
    private val triangleHeight = TRIANGLE_HEIGHT

    //繪制進(jìn)度上限文本
    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
        //繪制灰色進(jìn)度
        secondaryPath = Path()
        secondaryPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        secondaryPaint.style = Paint.Style.FILL
        //繪制進(jìn)度
        progressPath = Path()
        progressPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        progressPaint.color = progressColor
        progressPaint.style = Paint.Style.FILL
        //繪制進(jìn)度文字
        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
        //繪制三角形標(biāo)簽
        trianglePath = Path()
        trianglePaint = Paint(Paint.ANTI_ALIAS_FLAG)
        trianglePaint.color = Color.WHITE
        trianglePaint.style = Paint.Style.FILL
        //繪制進(jìn)度上限文本
        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)
        //繪制進(jìn)度限制線條
        drawDashLine(canvas, dashLinePaint)
        //繪制灰色進(jìn)度
        drawSecondaryProgress(canvas, secondaryPath, secondaryPaint)
        //繪制進(jìn)度
        drawProgress(canvas, progressPath, progressPaint)
        //繪制進(jìn)度文字
        drawProgressText(canvas, progressTextPaint)
        //繪制三角形標(biāo)簽
        drawTriangle(canvas, trianglePath, trianglePaint)
        //繪制最大閾值的文本
        drawLimitText(canvas, limitTextPaint)
    }

    /**
     * 繪制進(jìn)度限度文本
     *
     * @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)
    }

    /**
     * 繪制進(jìn)度限度三角標(biāo)
     *
     * @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)
    }

    /**
     * 繪制進(jìn)度文本
     *
     * @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)
    }

    /**
     * 繪制進(jìn)度
     *
     * @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)
    }

    /**
     * 繪制灰色進(jìn)度
     *
     * @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)
    }

    //*************保存進(jìn)度狀態(tài)**********************
    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">
        <!--進(jìn)度-->
        <attr name="charging_progress" format="integer" />
        <!--最大進(jìn)度-->
        <attr name="charging_max" format="integer" />
        <!--最大閾值-->
        <attr name="charging_limit" format="integer" />
        <!--進(jìn)度條顏色-->
        <attr name="charging_progressColor" format="color|reference" />
        <!--灰色進(jìn)度條開始顏色-->
        <attr name="charging_secondaryStartColor" format="color|reference" />
        <!--灰色進(jìn)度條結(jié)束顏色-->
        <attr name="charging_secondaryEndColor" format="color|reference" />
        <!--背景顏色-->
        <attr name="charging_backgroundColor" format="color|reference" />
        <!--線條顏色-->
        <attr name="charging_lineColor" format="color|reference" />
        <!--線條粗細(xì)-->
        <attr name="charging_lineWidth" format="float|reference" />
        <!--進(jìn)度文本顏色-->
        <attr name="charging_progressTextColor" format="color|reference" />
        <!--進(jìn)度文本字體大小-->
        <attr name="charging_progressTextSize" format="float|reference" />
        <!--限制進(jìn)度文本顏色-->
        <attr name="charging_limitTextColor" format="color|reference" />
        <!--進(jìn)度文本字體大小-->
        <attr name="charging_limitTextSize" format="float|reference" />

    </declare-styleable>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容