這是一個很簡單的自定義Viewjava
先上效果圖android
/**
* 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
經過PathMeasure.setPath()以後調用PathMeasure.getPosTan(PathMeasure.length,pos[],tan[])獲取pos[]座標點,就能夠作不少事情了github
public boolean getPosTan(float distance, float pos[], float tan[]) {
...
}
複製代碼
好比下圖的canvas
代碼很少,就直接貼了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