Jetpack Compose初體驗之自定義圖表

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端

開發項目的時候,不免會遇到原生控件沒法知足,須要自定義的狀況,今天經過繪製幾個圖表來練習一下Jetpack Compose 中的自定義View。android

線形圖

copmose_30.gif

繪製原理和以前xml中同樣,只不過實現的方式變了一些,比以前簡單了不少,好比下面經過path來繪製線形圖。構建好path以後,直接在Canvas中繪製就OK了。web

若是想要對圖標進行雙指縮放,能夠經過Modifier.graphicsLayer().transformable()來監聽手勢。經過rememberTransformableState來監聽手指縮放的大小而後將返回值賦值給相應的變量就能夠啦canvas

完整代碼:後端

data class Point(val X: Float = 0f, val Y: Float = 0f)

    @Composable
    fun LineChart() {
        //用來記錄縮放大小
        var scale by remember { mutableStateOf(1f) }
        val state = rememberTransformableState {
                zoomChange, panChange, rotationChange ->
            scale*=zoomChange
        }
        val point = listOf(
            Point(10f, 10f), Point(50f, 100f), Point(100f, 30f),
            Point(150f, 200f), Point(200f, 120f), Point(250f, 10f),
            Point(300f, 280f), Point(350f, 100f), Point(400f, 10f),
            Point(450f, 100f), Point(500f, 200f)
        )
        val path = Path()
        for ((index, item) in point.withIndex()) {
            if (index == 0) {
                path.moveTo(item.X*scale, item.Y)
            } else {
                path.lineTo(item.X*scale, item.Y)
            }
        }
        val point1 = listOf(
            Point(10f, 210f), Point(50f, 150f), Point(100f, 130f),
            Point(150f, 200f), Point(200f, 80f), Point(250f, 240f),
            Point(300f, 20f), Point(350f, 150f), Point(400f, 50f),
            Point(450f, 240f), Point(500f, 140f)
        )
        val path1 = Path()
        path1.moveTo(point1[0].X*scale, point1[0].Y)
        path1.cubicTo(point1[0].X*scale, point1[0].Y, point1[1].X*scale, point1[1].Y, point1[2].X*scale, point1[2].Y)
        path1.cubicTo(point1[3].X*scale, point1[3].Y, point1[4].X*scale, point1[4].Y, point1[5].X*scale, point1[5].Y)
        path1.cubicTo(point1[6].X*scale, point1[6].Y, point1[7].X*scale, point1[7].Y, point1[8].X*scale, point1[8].Y)
        path1.cubicTo(point1[7].X*scale, point1[7].Y, point1[8].X*scale, point1[8].Y, point1[9].X*scale, point1[9].Y)

        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(120.dp)
                .background(Color.White)
                //監聽手勢縮放
                .graphicsLayer(
                ).transformable(state)
        ) {
            //繪製 X軸 Y軸
            drawLine(
                start = Offset(10f, 300f),
                end = Offset(10f, 0f),
                color = Color.Black,
                strokeWidth = 2f
            )
            drawLine(
                start = Offset(10f, 300f),
                end = Offset(510f, 300f),
                color = Color.Black,
                strokeWidth = 2f
            )
            //繪製path
            drawPath(
                path = path,
                color = Color.Blue,
                style = Stroke(width = 2f)
            )
            drawPath(
                path = path1,
                color = Color.Green,
                style = Stroke(width = 2f)
            )
        }
    }

複製代碼

柱狀圖

copmose_31.gif

下面來繪製柱狀圖,繪製很簡單,直接根據座標繪製矩形就能夠了。Jetpack Compose中的繪製矩形的API跟以前XML中的API不大同樣,須要提供繪製的左上角和矩形的大小就能夠繪製了,看一下構造函數就知道了。markdown

而後給柱子加上點擊事件,Jetpack Compose中監聽點擊的屏幕位置座標使用Modifier中的pointerInput方法,而後判斷點擊的座標是否在矩形的範圍以內便可,下面代碼中只判斷了X軸 的座標,也能夠在加上Y軸的判斷。ide

最後再給柱形圖加上動畫,動畫使用animateFloatAsState方法,值設置爲0到1表明當前繪製高度的百分比,而後繪製的時候給高度添加該百分比的值就OK了。函數

完整代碼:post

private fun identifyClickItem(points: List<Point>, x: Float, y: Float): Int {
        for ((index, point) in points.withIndex()) {
            if (x > point.X+20 && x < point.X + 20+40) {
                return index
            }
        }
        return -1
    }

    @Composable
    fun BarChart() {
        val point = listOf(
            Point(10f, 10f), Point(90f, 100f), Point(170f, 30f),
            Point(250f, 200f), Point(330f, 120f), Point(410f, 10f),
            Point(490f, 280f), Point(570f, 100f), Point(650f, 10f),
            Point(730f, 100f), Point(810f, 200f)
        )
        var start by remember { mutableStateOf(false) }
        val heightPre by animateFloatAsState(
            targetValue = if (start) 1f else 0f,
            animationSpec = FloatTweenSpec(duration = 1000)
        )
        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(300.dp)
                .background(Color.White)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            val i = identifyClickItem(point, it.x, it.y)
                            Log.d("pointerInput", "onTap: ${it.x} ${it.y} item:$i")
                            Toast
                                .makeText(this@FourActivity, "onTap: $i", Toast.LENGTH_SHORT)
                                .show()
                        }
                    )
                }
        ) {
            //繪製 X軸 Y軸
            drawLine(
                start = Offset(10f, 600f),
                end = Offset(10f, 0f),
                color = Color.Black,
                strokeWidth = 2f
            )
            drawLine(
                start = Offset(10f, 600f),
                end = Offset(850f, 600f),
                color = Color.Black,
                strokeWidth = 2f
            )
            start = true
            for (p in point) {
                drawRect(
                    color = Color.Blue,
                    topLeft = Offset(p.X + 20, 600 - (600 - p.Y) * heightPre),
                    size = Size(40f, (600 - p.Y) * heightPre)
                )
            }
        }
    }
複製代碼

餅圖

copmose_32.gif

最後繪製一個餅圖,餅圖的實現方式能夠經過繪製drawPathdrawArc兩種方式實現,drawArc的方式簡單一點。動畫

給餅圖中的每一塊添加點擊事件,點擊事件也是在Modifier的pointerInput方法中監聽點擊的座標。Math.atan2()返回從原點(0,0) 到 (x,y)的線與x軸正方向的弧度值,而後通Math.toDegrees()方法把弧度轉化爲角度,最後經過角度獲取點擊的區域。

完整代碼:

private fun getPositionFromAngle(angles:List<Float>,touchAngle:Double):Int{
        var totalAngle = 0f
        for ((i, angle) in angles.withIndex()) {
            totalAngle +=angle
            if(touchAngle<=totalAngle){
                return i
            }
        }
        return -1
    }
    @Composable
    fun PieChart() {
        val point = listOf(10f, 40f, 20f, 80f, 100f, 60f)
        val color = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
        val sum = point.sum()
        var startAngle = 0f
        val radius = 200f
        val rect = Rect(Offset(-radius, -radius), Size(2 * radius, 2 * radius))
        val path = Path()
        val angles = mutableListOf<Float>()
        val regions = mutableListOf<Region>()
        var start by remember { mutableStateOf(false) }
        val sweepPre by animateFloatAsState(
            targetValue = if (start) 1f else 0f,
            animationSpec = FloatTweenSpec(duration = 1000)
        )
        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Color.White)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            Log.d(
                                "pointerInput",
                                "onTap: ${it.x - radius.toInt()} ${it.y - radius.toInt()} ${regions}"
                            )
                            var x = it.x - radius
                            var y = it.y - radius
                            var touchAngle = Math.toDegrees(Math.atan2(y.toDouble(),x.toDouble()))
                            //座標1,2象限返回-180~0 3,4象限返回0~180
                            if(x<0&&y<0 || x>0&&y<0){//1,2象限
                                touchAngle += 360;
                            }
                            val position = getPositionFromAngle(touchAngle = touchAngle,angles = angles)
                            Toast
                                .makeText(
                                    this@FourActivity,
                                    "onTap: $position",
                                    Toast.LENGTH_SHORT
                                )
                                .show()
                        }
                    )
                }
        ) {
            translate(radius, radius) {
                start = true
                for ((i, p) in point.withIndex()) {
                    var sweepAngle = p / sum * 360f
                    println("sweepAngle: $sweepAngle p:$p sum:$sum")
                    path.moveTo(0f, 0f)
                    path.arcTo(rect = rect, startAngle, sweepAngle*sweepPre, false)
                    angles.add(sweepAngle)
                    drawPath(path = path, color = color[i])
                    path.reset()

// drawArc(color = color[i],
// startAngle = startAngle,
// sweepAngle = sweepAngle,
// useCenter = true,
// topLeft = Offset(-radius,-radius),
// size = Size(2*radius,2*radius)
// )

                    startAngle += sweepAngle
                }
            }
        }
    }
複製代碼

Jetpack Compose 剛出來有一些功能還不完善,能夠在drawIntoCanvas的做用域中使用使用原來的canvas,按照原來的方式來繪製。drawIntoCanvas做用域內的對象是一個canvas,經過it.nativeCanvas方法能夠返回一個原生Android中的canvas對象。咱們就能夠經過它來按照原來的方式繪製了。

好比上面的餅圖的點擊事件,原來咱們能夠經過Path和Region這兩個類結合,計算出每一塊的繪製區域。可是在使用的時候發現Jetpack Compose的UI包中沒有對應的Region類,只有對應的Path類,想要使用上面的功能就只能使用原來的Path類和Region來計算了。使用方式以下:

@Composable
    fun PieChart1(){
        val point = listOf(10f, 40f, 20f, 80f, 100f, 60f)
        val colors = listOf(Color.Blue, Color.Yellow, Color.Green, Color.Gray, Color.Red, Color.Cyan)
        val sum = point.sum()
        var startAngle = 0f
        val radius = 200f
        val path = android.graphics.Path()
        val rect = android.graphics.RectF(-radius,-radius,radius,radius)
        val regions = mutableListOf<Region>()
        val paint = Paint()
        paint.isAntiAlias = true
        paint.style = Paint.Style.FILL
        Canvas(
            modifier = Modifier
                .fillMaxWidth()
                .height(200.dp)
                .background(Color.White)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            Log.d(
                                "pointerInput",
                                "onTap: ${it.x - radius.toInt()} ${it.y - radius.toInt()} ${regions.toString()}"
                            )
                            val x = it.x - radius
                            val y = it.y - radius
                            var position = -1
                            for ((i, region) in regions.withIndex()) {
                                if(region.contains(x.toInt(),y.toInt())){
                                    position = i
                                }
                            }
                            Toast
                                .makeText(
                                    this@FourActivity,
                                    "onTap: $position",
                                    Toast.LENGTH_SHORT
                                )
                                .show()
                        }
                    )
                }
        ) {
            translate(radius, radius) {
                drawIntoCanvas {
                    for ((i, p) in point.withIndex()) {
                        var sweepAngle = p / sum * 360f
                        println("sweepAngle: $sweepAngle p:$p sum:$sum")
                        path.moveTo(0f, 0f)
                        path.arcTo(rect,startAngle,sweepAngle)
                        //計算繪製區域並保存
                        val r = RectF()
                        path.computeBounds(r,true)
                        val region = Region()
                        region.setPath(path, Region(r.left.toInt(),r.top.toInt(),r.right.toInt(),r.bottom.toInt()))
                        regions.add(region)

                        paint.color = colors[i].toArgb()
                        it.nativeCanvas.drawPath(path,paint)
                        path.reset()
                        startAngle += sweepAngle
                    }
                }
            }
        }
    }
複製代碼

運行效果跟跟前面繪製的餅圖效果同樣。

總結:Jetpack Compose中自定義View的API比原來的方式簡潔了很多,並且噹噹前API沒法知足需求的時候,也能夠很方便的使用原來的API進行繪製,體驗很不錯。

相關文章
相關標籤/搜索