本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!前端
開發項目的時候,不免會遇到原生控件沒法知足,須要自定義的狀況,今天經過繪製幾個圖表來練習一下Jetpack Compose 中的自定義View。android
繪製原理和以前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)
)
}
}
複製代碼
下面來繪製柱狀圖,繪製很簡單,直接根據座標繪製矩形就能夠了。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)
)
}
}
}
複製代碼
最後繪製一個餅圖,餅圖的實現方式能夠經過繪製drawPath
和drawArc
兩種方式實現,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進行繪製,體驗很不錯。