Android自定義View-體重表盤

這是一個很簡單的自定義Viewjava

先上效果圖android

用到的技能點

1.SweepGradient

/**
     * A Shader that draws a sweep gradient around a center point.
     *
     * @param cx       The x-coordinate of the center
     * @param cy       The y-coordinate of the center
     * @param colors   The colors to be distributed between around the center.
     *                 There must be at least 2 colors in the array.
     * @param positions May be NULL. The relative position of
     *                 each corresponding color in the colors array, beginning
     *                 with 0 and ending with 1.0. If the values are not
     *                 monotonic, the drawing may produce unexpected results.
     *                 If positions is NULL, then the colors are automatically
     *                 spaced evenly.
     */
    public SweepGradient(float cx, float cy,
            @NonNull @ColorInt int colors[], @Nullable float positions[]) {
            }
複製代碼

其中 positions.size 要等於 colors.size,一一對應git

position的分佈(網圖,侵刪)

2.PathMeasure

經過PathMeasure.setPath()以後調用PathMeasure.getPosTan(PathMeasure.length,pos[],tan[])獲取pos[]座標點,就能夠作不少事情了github

public boolean getPosTan(float distance, float pos[], float tan[]) {
    ...
}
複製代碼

好比下圖的canvas

兩斷path的尾部,鏈接起來就是示意圖的需求了

代碼很少,就直接貼了bash

package com.sl.customdemo.dial

import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.PathMeasure
import android.graphics.RectF
import android.graphics.SweepGradient
import android.text.TextPaint
import android.util.AttributeSet
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import java.lang.Math.min

import java.math.BigDecimal


/**
 * Created by sl on 2018/3/5.
 */

class DialView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    /**
     * 頂部文字大小
     */
    private var mTopTextSize: Float = 0.toFloat()
    /**
     * 頂部文字的下間距
     */
    private var mTopTextBottomMargin: Float = 0.toFloat()
    /**
     * 中間文字大小
     */
    private var mCenterTextSize: Float = 0.toFloat()
    /**
     * 中間文字顏色
     */
    private var mCenterTvColor: Int = Color.parseColor("#333333")
    /**
     * 中間文字高度
     */
    private var mCenterValueHeight: Int = 0
    /**
     * 中間文字單位大小
     */
    private var mCenterUnitTextSize: Float = 0.toFloat()
    /**
     * 底部文字大小
     */
    private var mBottomTextSize: Float = 0.toFloat()
    /**
     * 底部文字顏色
     */
    private var mBottomTextColor: Int = 0
    /**
     * 外圓環寬度
     */
    private var mOuterArcWidth: Float = 0.toFloat()
    /**
     * 內環寬度
     */
    private var mInnerArcWidth: Float = 0.toFloat()
    /**
     * 結束點圓半徑
     */
    private var mEndCircleRadius: Float = 0.toFloat()
    /**
     * 結束點上的直線寬度
     */
    private var mEndLineWidth: Float = 0.toFloat()

    private var mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var mPathPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)

    private var mTextPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
    /**
     * 中間值
     */
    private var mCenterValue: Float = 0.toFloat()
    /**
     * 底部文字
     */
    private var mBottomText: String? = null
    /**
     * 動畫時展現的值
     */
    private var mShowValue = -1f

    /**
     * 底部空白角度
     */
    private val mBottomBlankAngle = 90f
    /**
     * 中間空白角度
     */
    private val mOtherBlankAngle = 12f
    /**
     * 三端圓弧每一個的角度
     */
    private var mEveryAngle: Float = 0.toFloat()
    /**
     * 開始的角度
     */
    private var mStartAngle: Float = 0.toFloat()
    /**
     * 結束的最大角度
     */
    private var mMaxEndAngle: Float = 0.toFloat()
    /**
     * 結束的角度
     */
    private var mResultAngle: Float = 0.toFloat()

    /**
     * 內環動畫時候的角度
     */
    private var mEndAngle: Float = 360f
    /**
     * 內環動畫時候的透明度
     */
    private var mInnerArcAlpha = 1

    private var mDrawBottom: Boolean = false

    private var maxWeight: Float = 150f

    /**
     * 內環Path
     */
    private var mInnerArcPath: Path = Path()
    /**
     * 結束點 直線的頂端Path
     */
    private var mOuterOrbitPath: Path = Path()
    private var mInnerArcColor: Int = 0
    /**
     * 內環結束點的座標
     */
    private var mInnerPos: FloatArray? = null

    /**
     * 內環結束點圓上的線的另外一個點的座標
     */
    private var mOuterPos: FloatArray? = null

    private var mTan: FloatArray? = null
    /**
     * 外間距爲外圓環寬度的一半
     */
    private var mOutsideMargin: Float = 0.toFloat()
    private var mWidth: Int = 0
    private var mHeight: Int = 0

    private var mRectF: RectF = RectF()
    /**
     * 內環的範圍
     */
    private var mInnerArea: RectF = RectF()
    /**
     * 結束點圓環範圍
     */
    private var mOuterLineArea: RectF = RectF()

    private var mValueAnimator: ValueAnimator? = null

    private val mTopStr = "體重"

    private val mUnit = "kg"

    private var mValueStr: String? = null

    private var mBgSweepGradient1: SweepGradient? = null
    private var mBgSweepGradient2: SweepGradient? = null
    private var mBgSweepGradient3: SweepGradient? = null
    private var mPathMeasure: PathMeasure = PathMeasure()

    private var mCenterBaseY: Int = 0
    private var mTopBaseY: Int = 0
    private var mBottomBaseY: Int = 0

    private val mBlueStart = Color.parseColor("#C2E9FB")
    private val mBlueEnd = Color.parseColor("#A1C4FD")
    private val mGreenStart = Color.parseColor("#58DBAE")
    private val mGreenEnd = Color.parseColor("#63D798")
    private val mRedStart = Color.parseColor("#F9C46F")
    private val mRedEnd = Color.parseColor("#FA8677")
    private val mBottomHighColor = Color.parseColor("#FA8677")
    private val mBottomNormalColor = Color.parseColor("#63D798")
    private val mBottomLowColor = Color.parseColor("#A1C4FD")

    init {
        mTopTextSize = spToPx(15f)
        mTopTextBottomMargin = dpToPx(3f)
        mCenterTextSize = spToPx(33f)
        mCenterUnitTextSize = spToPx(10f)
        mBottomTextSize = spToPx(16f)
        mOuterArcWidth = dpToPx(12f)
        mInnerArcWidth = dpToPx(3f)
        mEndCircleRadius = dpToPx(6f)
        mEndLineWidth = dpToPx(5f)

        mTextPaint.color = mCenterTvColor
        mTextPaint.textAlign = Paint.Align.CENTER

        mTextPaint.textSize = mCenterTextSize
        mCenterValueHeight = (mTextPaint.descent() - mTextPaint.ascent()).toInt()

        val otherAngle = 360 - mBottomBlankAngle
        mEveryAngle = (otherAngle - mOtherBlankAngle * 2) / 3f
        mStartAngle = 90 + mBottomBlankAngle / 2f
        mMaxEndAngle = 360 - mBottomBlankAngle

        mInnerPos = FloatArray(2)
        mOuterPos = FloatArray(2)
        mTan = FloatArray(2)
        mValueStr = getBigDecimalValue(mShowValue)

        mInnerArcColor = mBlueEnd
    }

    fun setWeight(value: Float, benchmarkL: Float, benchmarkH: Float) {
        if (value < 0) {
            mCenterValue = 0f
        } else if (value > 150) {
            mCenterValue = maxWeight
        } else {
            mCenterValue = value
        }
        this.mShowValue = mCenterValue
        mDrawBottom = false
        mResultAngle = calculateAngle(mCenterValue, benchmarkL, benchmarkH)
        when {
            mResultAngle <= mEveryAngle -> {
                this.mBottomText = "偏低" + (benchmarkL - mCenterValue)
                mInnerArcColor = mBlueEnd
                mBottomTextColor = mBottomLowColor
            }
            mResultAngle <= mEveryAngle * 2 + mOtherBlankAngle -> {
                this.mBottomText = "正常"
                mInnerArcColor = mGreenEnd
                mBottomTextColor = mBottomNormalColor
            }
            else -> {
                mInnerArcColor = mRedEnd
                mBottomTextColor = mBottomHighColor
                this.mBottomText = "偏高" + (mCenterValue - benchmarkH)

            }
        }
        if (mValueAnimator == null) {
            initAnimator()
        }
        if (mValueAnimator!!.isRunning) {
            mValueAnimator!!.cancel()
        }
        mValueAnimator!!.start()


    }

    /**
     * 計算結束的角度
     *
     * @param value
     * @param low
     * @param high
     * @return
     */
    private fun calculateAngle(value: Float, low: Float, high: Float): Float {
        var endAngle: Float
        endAngle = when {
            value < low -> mEveryAngle * if (value / low > 1) 1f else value / low
            value > high -> {
                val benchmark = (value - high) / (maxWeight - high) * mEveryAngle
                mEveryAngle * 2 + mOtherBlankAngle * 2 + benchmark
            }
            else -> {
                val benchmark = (value - low) / (high - low)
                mEveryAngle + mOtherBlankAngle * 1 + mEveryAngle * if (benchmark > 1) 1f else benchmark

            }
        }
        if (endAngle > mMaxEndAngle) {
            endAngle = mMaxEndAngle
        }
        return endAngle
    }

    private fun initAnimator() {
        mValueAnimator = ValueAnimator.ofFloat(0f, 1f)
        mValueAnimator!!.interpolator = OvershootInterpolator()
        mValueAnimator!!.duration = 2000

        mValueAnimator!!.addUpdateListener { animation ->
            val animatedValue = animation.animatedValue as Float
            mInnerArcAlpha = (animatedValue * 255f).toInt()
            mEndAngle = animatedValue * mResultAngle
            mShowValue = mCenterValue * animatedValue
            mValueStr = getBigDecimalValue(mShowValue)
            invalidate()
        }

        mValueAnimator!!.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator) {
                super.onAnimationEnd(animation)
                mDrawBottom = true
                invalidate()
            }
        })

    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        mHeight = kotlin.math.min(w, h)
        mWidth = mHeight
        mOutsideMargin = mOuterArcWidth / 2f

        //SweepGradient的 postions beginning with 0 and ending with 1.0.而第三段明顯超過一圈,因此繪製的時候畫布旋轉,旋轉到startAngle爲開始點
        val pos1 = floatArrayOf(0f, mEveryAngle / 360f)
        val pos2 = floatArrayOf(
            (mEveryAngle + mOtherBlankAngle) / 360f,
            (mEveryAngle * 2f + mOtherBlankAngle) / 360f
        )
        val pos3 = floatArrayOf(
            (mEveryAngle * 2f + mOtherBlankAngle * 2f) / 360f,
            (mEveryAngle * 3f + mOtherBlankAngle * 2f) / 360f
        )
        val colors1 = intArrayOf(mBlueStart, mBlueEnd)
        val colors2 = intArrayOf(mGreenStart, mGreenEnd)
        val colors3 = intArrayOf(mRedStart, mRedEnd)

        mBgSweepGradient1 = SweepGradient(w / 2f, h / 2f, colors1, pos1)
        mBgSweepGradient2 = SweepGradient(w / 2f, h / 2f, colors2, pos2)
        mBgSweepGradient3 = SweepGradient(w / 2f, h / 2f, colors3, pos3)

    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        if (width > height) {
            canvas.translate((width - height) / 2f, 0f)
        } else if (height > width) {
            canvas.translate(0f, (height - width) / 2f)
        }
        drawBgArc(canvas)
        drawCenterText(canvas)
        drawInnerAcr(canvas)
    }

    /**
     * 畫圓弧
     *
     * @param canvas
     */
    private fun drawBgArc(canvas: Canvas) {
        mRectF.set(
            mOutsideMargin,
            mOutsideMargin,
            mWidth - mOutsideMargin,
            mHeight - mOutsideMargin
        )
        mPaint.color = Color.RED
        mPaint.style = Paint.Style.STROKE
        mPaint.strokeWidth = mOuterArcWidth
        mPaint.strokeCap = Paint.Cap.ROUND

        canvas.save()
        //正常繪製,最後一段會超過一圈,可是SweepGradient的positions的範圍是【0,1】,因此旋轉繪製
        //旋轉開始位置,會使開始點的圓帽 漸變色異常,因此少旋轉mOtherBlankAngle
        canvas.rotate(mStartAngle - mOtherBlankAngle, mWidth / 2f, mHeight / 2f)
        mPaint.shader = mBgSweepGradient1
        canvas.drawArc(mRectF, mOtherBlankAngle, mEveryAngle, false, mPaint)
        mPaint.shader = mBgSweepGradient2
        canvas.drawArc(
            mRectF,
            mEveryAngle + mOtherBlankAngle + mOtherBlankAngle,
            mEveryAngle,
            false,
            mPaint
        )
        mPaint.shader = mBgSweepGradient3
        canvas.drawArc(
            mRectF,
            mEveryAngle * 2f + mOtherBlankAngle * 2f + mOtherBlankAngle,
            mEveryAngle,
            false,
            mPaint
        )
        canvas.restore()

    }

    /**
     * 中間文字
     *
     * @param canvas
     */
    private fun drawCenterText(canvas: Canvas) {
        mTextPaint.textSize = mCenterTextSize
        mTextPaint.color = mCenterTvColor
        val measureCenterText = mTextPaint.measureText(mValueStr)
        if (mCenterBaseY == 0) {
            //計算中間文字的矩形
            mRectF.set(
                mWidth / 2f - measureCenterText / 2,
                (mHeight / 2 - mCenterValueHeight / 2).toFloat(),
                mWidth / 2f + measureCenterText / 2,
                (mHeight / 2 + mCenterValueHeight / 2).toFloat()
            )
            val fontMetrics = mTextPaint.fontMetricsInt
            mCenterBaseY =
                ((mRectF.bottom + mRectF.top - fontMetrics.bottom.toFloat() - fontMetrics.top.toFloat()) / 2).toInt()
        }

        canvas.drawText(mValueStr!!, mWidth / 2f, mCenterBaseY.toFloat(), mTextPaint)
        //計算單位的區域
        mTextPaint.textSize = mCenterUnitTextSize
        canvas.drawText(
            mUnit,
            mWidth / 2f + measureCenterText / 2f + mTextPaint.measureText(mUnit) / 2f,
            mCenterBaseY.toFloat(),
            mTextPaint
        )

        //畫頂部的文字
        mTextPaint.textSize = mTopTextSize
        if (mTopBaseY == 0) {
            val topWidth = mTextPaint.measureText(mTopStr)
            val bottom = (mHeight / 2 - mCenterValueHeight / 2).toFloat()
            val top = bottom - (mOuterArcWidth + mTopTextBottomMargin + mEndCircleRadius * 2)
            mRectF.set(mWidth / 2f - topWidth / 2, top, mWidth / 2f + topWidth / 2f, bottom)
            val topFontMetrics = mTextPaint.fontMetricsInt
            mTopBaseY =
                ((mRectF.bottom + mRectF.top - topFontMetrics.bottom.toFloat() - topFontMetrics.top.toFloat()) / 2).toInt()
        }
        canvas.drawText(mTopStr, mWidth / 2f, mTopBaseY.toFloat(), mTextPaint)
        //畫底部的值
        if (mShowValue > 0 && mDrawBottom) {
            mTextPaint.textSize = mBottomTextSize
            mTextPaint.color = mBottomTextColor
            if (mBottomBaseY == 0) {
                val arcRadius = mHeight / 2f
                mBottomBaseY =
                    (mHeight / 2f + Math.cos(Math.toRadians((mBottomBlankAngle / 2f).toDouble())) * arcRadius).toInt()
            }

            canvas.drawText(mBottomText!!, mWidth / 2f, mBottomBaseY.toFloat(), mTextPaint)

        }

    }


    private fun drawInnerAcr(canvas: Canvas) {
        val radius =
            mWidth / 2f - mOuterArcWidth - mEndCircleRadius - mTopTextBottomMargin
        mInnerArea.set(
            mWidth / 2f - radius,
            mHeight / 2f - radius,
            mWidth / 2f + radius,
            mHeight / 2f + radius
        )
        //經過Path類畫一個內切圓弧路徑
        mInnerArcPath.reset()

        mInnerArcPath.addArc(
            mInnerArea, mStartAngle, if (mCenterValue == 0f) {
                360f
            } else {
                mEndAngle
            }
        )
        mPathMeasure.setPath(mInnerArcPath, false)
        //得出切點
        mPathMeasure.getPosTan(mPathMeasure.length, mInnerPos, mTan)

        mOuterLineArea.set(
            mOutsideMargin,
            mOutsideMargin,
            mWidth - mOutsideMargin,
            mHeight - mOutsideMargin
        )
        mOuterOrbitPath.reset()
        mOuterOrbitPath.addArc(
            mOuterLineArea, mStartAngle, if (mCenterValue == 0f) {
                360f
            } else {
                mEndAngle
            }
        )
        // 建立 PathMeasure
        mPathMeasure.setPath(mOuterOrbitPath, false)
        //得出直線的頂點
        mPathMeasure.getPosTan(mPathMeasure.length, mOuterPos, mTan)

        //繪製實心小圓圈
        mPathPaint.color = mInnerArcColor
        mPathPaint.style = Paint.Style.FILL
        mPathPaint.shader = null
        mPathPaint.alpha = 255
        canvas.drawCircle(mInnerPos!![0], mInnerPos!![1], mEndCircleRadius, mPathPaint)
        //圓中心點和線的頂端鏈接起來
        mPathPaint.strokeWidth = mEndLineWidth
        mPathPaint.style = Paint.Style.STROKE
        canvas.drawLine(
            mInnerPos!![0],
            mInnerPos!![1],
            mOuterPos!![0],
            mOuterPos!![1],
            mPathPaint
        )
        //有數據的時候 繪製內環
        if (mShowValue >= 0) {
            canvas.save()
            mPathPaint.alpha = mInnerArcAlpha
            mPathPaint.strokeWidth = mInnerArcWidth
            mPathPaint.style = Paint.Style.STROKE
            mInnerArcPath.reset()
            mInnerArcPath.addArc(mInnerArea, 0f, mEndAngle)
//            mOuterOrbitPath.reset()
//            mOuterOrbitPath.addArc(mOuterLineArea,0f,mEndAngle)
            canvas.rotate(mStartAngle, (mWidth / 2).toFloat(), (mHeight / 2).toFloat())
            canvas.drawPath(mInnerArcPath, mPathPaint)
//            canvas.drawPath(mOuterOrbitPath, mPathPaint)
            canvas.restore()
        }

    }

    fun cancel() {
        if (mValueAnimator != null && mValueAnimator!!.isRunning) {
            mValueAnimator!!.cancel()
        }
    }


    private fun getBigDecimalValue(value: Float): String {
        if (value <= 0) {
            return "--"
        }
        val bigDecimal = BigDecimal(value.toDouble())
        return bigDecimal.setScale(1, BigDecimal.ROUND_HALF_UP).toString()
    }

    private fun dpToPx(dp: Float): Float {
        return (dp * context.resources.displayMetrics.density)
    }

    private fun spToPx(sp: Float): Float {
        return (TypedValue.applyDimension(2, sp, context.resources.displayMetrics) + 0.5f)
    }

    companion object {

        private val TAG = "DialView"
    }
}

複製代碼

github地址app

相關文章
相關標籤/搜索