自定義View-餅狀圖(百分比圖)
阿新 • • 發佈:2018-11-27
按照設定的百分比陣列,設定的百分比陣列之和要等於1,如果大於1會出現覆蓋的情況。
這個效果裡面有2個關注點:
1、通過百分比陣列繪製多大的環。
2、切換比例的時候動畫效果。
3、正在變化時接收到百分比變化的處理
1、通過百分比陣列繪製多大的環
圓環一圈是360度,如果百分比是0.2,那麼圓環就繪製360 * 0.2 = 72,起始位置是在前面進度的結尾處。程式碼如下:
var startAngle = -90F for (i in 0 until mPercentages.size) { if (i > mPercentageColors.size - 1) { //如果沒有為每一段百分比設定顏色值,則使用最後一種顏色值 mPaint.color = mPercentageColors[mPercentageColors.size - 1] } else { mPaint.color = mPercentageColors[i] } val sweepAngle = mPercentages[i] * 360 canvas?.drawArc(mArcRectF, startAngle, sweepAngle, false, mPaint) startAngle += sweepAngle }
2、切換比例的時候動畫效果
1、補全陣列
2個不同的百分比陣列有可能長度不同,我們要較短那個後面補上0
/** * 補全陣列,使2個數組的長度相等 * fromValues : [0.1,0.2,0.4,0.3] * toValues: [0.1,0.2,0.7] * * 將toValues變為:[0.1,0.2,0.7,0] */ private fun completionArrays(fromValues: Array<Float>, toValues: Array<Float>): Values { return when { fromValues.size == toValues.size -> Values(fromValues, toValues) fromValues.size > toValues.size -> { //補全toValues var newValues: Array<Float> = Array(fromValues.size) { 0F } for (i in 0 until toValues.size) { newValues[i] = toValues[i] } Values(fromValues, newValues) } else -> { //fromValues var newValues: Array<Float> = Array(toValues.size) { 0F } for (i in 0 until fromValues.size) { newValues[i] = fromValues[i] } Values(newValues, toValues) } } }
Values是一個內部類:
data class Values(val fromValues: Array<Float>, val toValues: Array<Float>)
2、開啟一個動畫,根據動畫的進度計算當前百分比
/** * 啟動動畫 * */ private fun startAnimator(fromValues: Array<Float>, toValues: Array<Float>) { isChange = true val (fv, tv) = completionArrays(fromValues, toValues) val animator = ValueAnimator.ofFloat(0F, 1F) animator.duration = mAnimDuration animator.addUpdateListener { computePercentages(it.animatedValue as Float, fv, tv) postInvalidate() if (it.animatedValue as Float == 1F) { //動畫結束 isChange = false // val list = mPercentages.filter { it > 0 } // mPercentages = Array(list.size) { i -> list[i] } mNextToValues?.let { startAnimator(mPercentages, mNextToValues!!) } mNextToValues = null } } animator.start() }
/**
* 計算當前動畫百分比的值
*/
private fun computePercentages(animatedValue: Float, fromValues: Array<Float>, toValues: Array<Float>) {
var newValues = Array(fromValues.size) { 0F }
for (i in 0 until fromValues.size) {
newValues[i] = fromValues[i] - (fromValues[i] - toValues[i]) * animatedValue
}
mPercentages = newValues
}
根據當前的進度實時繪製餅狀圖。
3、正在變化時接收到百分比變化的處理
如果當前正在變化,又接收到百分比變化,則記錄此次變化,等上個動畫結束時馬上再次啟動動畫。
整體code:
PieChart.kt:
class PieChart @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) : View(context, attrs, defStyleAttr, defStyleRes) {
/**
* 百分比陣列,陣列和要=1
*/
private var mPercentages: Array<Float> = arrayOf(1.0F)
/**
* 不同百分比的顏色值
*/
private var mPercentageColors: Array<Int> = arrayOf(Color.parseColor("#fff4e0"),
Color.parseColor("#f8b500"),
Color.parseColor("#ff4d4d"),
Color.parseColor("#42d3b7"),
Color.parseColor("#334d5c"))
/**
* 圓環畫筆
*/
private var mPaint: Paint = Paint()
/**
* 圓環的大小
*/
private var mArcRectF: RectF = RectF()
/**
* 圓環寬度
*/
private var mStrokeWidth: Float = 20F
/**
* 圓環變化時是否使用動畫
*/
private var mUseAnimation: Boolean = true
/**
* 動畫時長,單位ms
*/
private var mAnimDuration: Long = 500L
/**
* 圓環正在改變時又有新的資料來記錄下來,等圓環改變結束在重新整理新的資料
*/
private var mNextToValues: Array<Float>? = null
private var isChange = false
init {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.PercentageRing, defStyleAttr, 0)
mStrokeWidth = a.getDimension(R.styleable.PercentageRing_stroke_width, 10F)
//設定圓環畫筆
mPaint.style = Paint.Style.STROKE
mPaint.strokeWidth = mStrokeWidth
mPaint.isAntiAlias = true
}
override fun onDraw(canvas: Canvas?) {
drawRing(canvas)
}
/**
* 繪製圓環
*
* @param canvas 畫布
*/
private fun drawRing(canvas: Canvas?) {
var ringWidth = width
var height = height
mArcRectF.left = mStrokeWidth / 2
mArcRectF.right = width - mStrokeWidth / 2
mArcRectF.top = mStrokeWidth / 2
mArcRectF.bottom = height - mStrokeWidth / 2
var startAngle = -90F
for (i in 0 until mPercentages.size) {
if (i > mPercentageColors.size - 1) {
//如果沒有為每一段百分比設定顏色值,則使用最後一種顏色值
mPaint.color = mPercentageColors[mPercentageColors.size - 1]
} else {
mPaint.color = mPercentageColors[i]
}
val sweepAngle = mPercentages[i] * 360
canvas?.drawArc(mArcRectF, startAngle, sweepAngle, false, mPaint)
startAngle += sweepAngle
}
}
/**
* 設定百分比資料和中間顯示文案
*
* @param percentages 百分比
*/
fun setPercentages(percentages: Array<Float>) {
if (mUseAnimation) {
if (isChange) {
mNextToValues = percentages
return
}
startAnimator(mPercentages, percentages)
} else {
mPercentages = percentages
postInvalidate()
}
}
fun setPercentages(percentages: Array<Float>, colors: Array<Int>) {
mPercentages = percentages
setColors(colors)
postInvalidate()
}
/**
* 設定圓環顏色
*
* @param colors 顏色RGB值
*/
fun setColors(colors: Array<Int>) {
mPercentageColors = colors
}
/**
* 啟動動畫
*
*/
private fun startAnimator(fromValues: Array<Float>, toValues: Array<Float>) {
isChange = true
val (fv, tv) = completionArrays(fromValues, toValues)
val animator = ValueAnimator.ofFloat(0F, 1F)
animator.duration = mAnimDuration
animator.addUpdateListener {
computePercentages(it.animatedValue as Float, fv, tv)
postInvalidate()
if (it.animatedValue as Float == 1F) {
//動畫結束
isChange = false
// val list = mPercentages.filter { it > 0 }
// mPercentages = Array(list.size) { i -> list[i] }
mNextToValues?.let { startAnimator(mPercentages, mNextToValues!!) }
mNextToValues = null
}
}
animator.start()
}
/**
* 補全陣列,使2個數組的長度相等
* fromValues : [0.1,0.2,0.4,0.3]
* toValues: [0.1,0.2,0.7]
*
* 將toValues變為:[0.1,0.2,0.7,0]
*/
private fun completionArrays(fromValues: Array<Float>, toValues: Array<Float>): Values {
return when {
fromValues.size == toValues.size -> Values(fromValues, toValues)
fromValues.size > toValues.size -> {
//補全toValues
var newValues: Array<Float> = Array(fromValues.size) { 0F }
for (i in 0 until toValues.size) {
newValues[i] = toValues[i]
}
Values(fromValues, newValues)
}
else -> {
//fromValues
var newValues: Array<Float> = Array(toValues.size) { 0F }
for (i in 0 until fromValues.size) {
newValues[i] = fromValues[i]
}
Values(newValues, toValues)
}
}
}
/**
* 計算當前動畫百分比的值
*/
private fun computePercentages(animatedValue: Float, fromValues: Array<Float>, toValues: Array<Float>) {
var newValues = Array(fromValues.size) { 0F }
for (i in 0 until fromValues.size) {
newValues[i] = fromValues[i] - (fromValues[i] - toValues[i]) * animatedValue
}
mPercentages = newValues
}
data class Values(val fromValues: Array<Float>, val toValues: Array<Float>)
companion object {
const val TAG = "PieChart"
}
}
自定義屬性 attr.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PieChart">
<attr name="stroke_width" format="dimension" />
</declare-styleable>
</resources>
使用及測試 TestActivity.kt :
class TestActivity : AppCompatActivity() {
val handler = object : Handler() {
override fun handleMessage(msg: Message?) {
pieChart.setPercentages(list[index++ % list.size])
sendEmptyMessageDelayed(0, 1000)
}
}
var list = arrayListOf<Array<Float>>(
arrayOf(0.1F, 0.2F, 0.3F, 0.4F),
arrayOf(0.2F, 0.3F, 0.2F, 0.3F),
arrayOf(0.5F, 0.5F),
arrayOf(0.1F, 0.2F, 0.3F, 0.4F),
arrayOf(0.3F, 0.5F, 0.2F),
arrayOf(0.1F, 0.8F, 0.1F)
)
var index = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_test)
handler.sendEmptyMessage(0)
}
}
佈局檔案 activity_test.xml:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PieChart
android:id="@+id/pieChart"
android:layout_width="300dp"
android:layout_height="300dp"
app:stroke_width="@dimen/dp_11"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</android.support.constraint.ConstraintLayout>