Compose自定義View實現(xiàn)繪制Rainbow運動三環(huán)效果
本章節(jié)介紹的是一個基于Compose自定義的一個Rainbow彩虹運動三環(huán),業(yè)務(wù)上類似于iWatch上的那個運動三環(huán),不過這里實現(xiàn)的用的一個半圓去繪制,整個看起來像彩虹,三環(huán)的外兩層為卡路里跟步數(shù),最里層可設(shè)定為活動時間,站立次數(shù)。同樣地首先看一下gif動圖:

大致地介紹一下Rainbow的繪制過程,很明顯圖形分兩層,底層有個alpha為0.4f * 255的背景底,前景會依據(jù)具體的值的百分占比繪制一個角度的弧度環(huán),從外往里分三個Type的環(huán),每個環(huán)有前景跟背景,畫三次,需要每次對Canvas進行一個translate,環(huán)的繪制邏輯放在了RainbowModel里了,前景加背景所以這個一共需要被調(diào)用6次。
@Composable
fun drawCircle(type: Int,
fraction: Float,
isBg: Boolean, modifier: Modifier){
val colorResource = getColorResource(type)
val color = colorResource(id = colorResource)
Canvas(modifier = modifier.fillMaxSize()){
val contentWidth = size.width
val contentHeight = size.height
val itemWidth = contentWidth / 7.2f
val spaceWidth = itemWidth / 6.5f
val rectF = createTargetRectF(type, itemWidth, spaceWidth, contentWidth, contentHeight)
val space = if (type == RainbowConstant.TARGET_THIRD_TYPE)
spaceWidth/2.0f else spaceWidth
val sweepAngel = fraction * 180
val targetModel = createTargetModel(isBg, type, rectF, itemWidth, space, sweepAngel)
println("drawRainbow width:${rectF.width()}, height${rectF.height()}")
if (checkFractionIsSmall(fraction, type)) {
val roundRectF = createRoundRectF(type, itemWidth, spaceWidth, contentHeight)
drawRoundRect(
color = color,
topLeft = Offset(x = roundRectF.left, y = roundRectF.top),
size = Size(roundRectF.width(), roundRectF.height()),
cornerRadius = CornerRadius(spaceWidth / 2.0f, spaceWidth / 2.0f)
)
} else {
withTransform({ translate(left = rectF.left, top = rectF.top) }) {
targetModel.createComponents()
targetModel.drawComponents(this, color, isBg)
}
}
}
}這里有個邊界需要處理,當(dāng)百分比比較小的時候繪制的一個RoundRectF, 而且不需要translate。
這里前景的三次調(diào)用做了個簡易的動畫,如上面的gif動圖所示:
val animator1 = remember{ Animatable(0f, Float.VectorConverter) }
val animator2 = remember{ Animatable(0f, Float.VectorConverter) }
val animator3 = remember{ Animatable(0f, Float.VectorConverter) }
?
val tweenSpec = tween<Float>(durationMillis = 1000, delayMillis = 600, easing = FastOutSlowInEasing)
LaunchedEffect(Unit){
animator1.animateTo(targetValue = 0.5f, animationSpec = tweenSpec)
}
LaunchedEffect(Unit){
animator2.animateTo(targetValue = 0.7f, animationSpec = tweenSpec)
}
LaunchedEffect(Unit){
animator3.animateTo(targetValue = 0.8f, animationSpec = tweenSpec)
}
?
drawCircle(
type = RainbowConstant.TARGET_FIRST_TYPE,
fraction = animator1.value,
isBg = false,
modifier
)
drawCircle(
type = RainbowConstant.TARGET_SECOND_TYPE,
fraction = animator2.value,
isBg = false,
modifier
)
drawCircle(
type = RainbowConstant.TARGET_THIRD_TYPE,
fraction = animator3.value,
isBg = false,
modifier
)Rainbow環(huán)的繪制
上面是Rainbow繪制的外層框架,然后每個Rainbow環(huán)的繪制的邏輯(這里沒有用SweepGradient,Compose里對應(yīng)的為brush 參數(shù), 直接用的單一的Color值)即上面的targetModel.drawComponents(this, color, isBg) 背后的邏輯。想必讀者都繪制過RoundRectF, 這里的RountF 弧形環(huán)是如何實現(xiàn)繪制的呢?整個的邏輯在RainbowModel里,這里把小圓角視為一個近似直角的扇形,所以一共有4個小扇形,然后除去4個小扇形,中間一個大的沒有圓角的弧形,外加內(nèi)層、外層出去圓角的小弧形,所以總共7個path:
private lateinit var centerCircle: Path private lateinit var wrapperCircle: Path private lateinit var innerCircle: Path private lateinit var wrapperStartPath: Path private lateinit var wrapperEndPath: Path private lateinit var innerStartPath: Path private lateinit var innerEndPath: Path
然后稍微簡單介紹下小扇形的繪制, 內(nèi)層跟外層不太一樣,通過構(gòu)建封閉的Path,所以需要用的圓角的曲線,這里近似地用二階Bezier代替,所以需要找它的Control點,這里直接用沒有沒有圓角情況下,直徑網(wǎng)外射出去跟圓角的交點,同樣外、內(nèi)的計算稍微不太一樣:
fun createCommonPoint(rectF: RectF, sweepAngel: Float): PointF {
val radius = rectF.width() / 2
val halfCircleLength = (Math.PI * radius).toFloat()
val pathOriginal = Path()
pathOriginal.moveTo(rectF.left, (rectF.top + rectF.bottom) / 2)
pathOriginal.arcTo(rectF, 180f, 180f, false)
val pathMeasure = PathMeasure(pathOriginal, false)
val points = FloatArray(2)
val pointLength = halfCircleLength * sweepAngel / 180f
pathMeasure.getPosTan(pointLength, points, null)
return PointF(points[0], points[1])
}
?
fun createEndPoint(rectF: RectF, sweepAngel: Float): PointF {
val radius = rectF.width() / 2
val halfCircleLength = (Math.PI * radius).toFloat()
val pathOriginal = Path()
pathOriginal.moveTo(rectF.right, (rectF.top + rectF.bottom) / 2)
pathOriginal.arcTo(rectF, 0f, -180f, false)
val pathMeasure = PathMeasure(pathOriginal, false)
val points = FloatArray(2)
val pointLength = halfCircleLength * sweepAngel / 180f
pathMeasure.getPosTan(pointLength, points, null)
return PointF(points[0], points[1])
}借助PathMeasure通過計算 弧長跟半圓的一個Compare,計算弧長的endpoint, 這個點算作 小扇形的二階bezier的Control點,然后通過createQuadPath()來構(gòu)建小扇形。
fun createQuadPath(): Path {
quadPath = Path()
quadPath.apply {
moveTo(startPointF.x, startPointF.y)
quadTo(ctrlPointF.x, ctrlPointF.y, endPointF.x, endPointF.y)
lineTo(centerPointF.x, centerPointF.y)
close()
}
return quadPath
}以下是在RainbowModel里計算wrapperStartPath、wrapperEndPath、innerStartPath、innerEndPath 具體的邏輯
private fun createInnerPath() {
innerStartPath = Path()
val startQuadModel = QuadModel()
startQuadModel.centerPointF =
startQuadModel.createCommonPoint(innerStartRectF, innerFixAngel)
startQuadModel.ctrlPointF = startQuadModel.createCommonPoint(innerEndRectF, 0f)
startQuadModel.startPointF =
startQuadModel.createCommonPoint(innerEndRectF, innerFixAngel)
startQuadModel.endPointF = startQuadModel.createCommonPoint(innerStartRectF, 0f)
innerStartPath = startQuadModel.createQuadPath()
val endQuadModel = QuadModel()
endQuadModel.centerPointF =
endQuadModel.createEndPoint(innerStartRectF, 180 - sweepAngel + innerFixAngel)
endQuadModel.ctrlPointF = endQuadModel.createCommonPoint(innerEndRectF, sweepAngel)
endQuadModel.startPointF = endQuadModel.createCommonPoint(innerStartRectF, sweepAngel)
endQuadModel.endPointF =
endQuadModel.createEndPoint(innerEndRectF, 180 - sweepAngel + innerFixAngel)
innerEndPath = endQuadModel.createQuadPath()
}
?
private fun createWrapperPath() {
val startQuadModel = QuadModel()
startQuadModel.centerPointF =
startQuadModel.createCommonPoint(wrapperEndRectF, wrapperFixAngel)
startQuadModel.ctrlPointF = startQuadModel.createCommonPoint(wrapperStartRectF, 0f)
startQuadModel.startPointF = startQuadModel.createCommonPoint(wrapperEndRectF, 0f)
startQuadModel.endPointF =
startQuadModel.createCommonPoint(wrapperStartRectF, wrapperFixAngel)
wrapperStartPath = startQuadModel.createQuadPath()
val endQuadModel = QuadModel()
endQuadModel.centerPointF =
endQuadModel.createEndPoint(wrapperEndRectF, 180 - sweepAngel + wrapperFixAngel)
endQuadModel.ctrlPointF = endQuadModel.createCommonPoint(wrapperStartRectF, sweepAngel)
endQuadModel.startPointF =
endQuadModel.createEndPoint(wrapperStartRectF, 180 - sweepAngel + wrapperFixAngel)
endQuadModel.endPointF = endQuadModel.createCommonPoint(wrapperEndRectF, sweepAngel)
wrapperEndPath = endQuadModel.createQuadPath()
}以上大致是小扇形的繪制邏輯,其中關(guān)鍵的一些點在于,因為它比較小所以直接用二階貝塞爾來代替圓弧,通過PathLength里計算任一sweepAngel下的二階Bezier的Control點。然后內(nèi)層跟外層的一些計算上數(shù)據(jù)幾何上的問題的處理,逆時針、順時針的注意,筆者也是在代碼過程中慢慢調(diào)試,然后修改變量等。
然后其它三個Path相對比較簡單,不做過多介紹了。
代碼同樣在https://github.com/yinxiucheng/compose-codelabs/ 下的CustomerComposeView 的rainbow的package 下面。
以上就是Compose自定義View實現(xiàn)繪制Rainbow運動三環(huán)效果的詳細(xì)內(nèi)容,更多關(guān)于Compose Rainbow運動三環(huán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android入門之使用SimpleAdapter實現(xiàn)復(fù)雜界面布局
這篇文章主要為大家詳細(xì)介紹了Android如何使用SimpleAdapter實現(xiàn)復(fù)雜的界面布局,文中的示例代碼講解詳細(xì),對我們學(xué)習(xí)Android有一定的幫助,需要的可以參考一下2022-11-11
Android?webview攔截H5的接口請求并返回處理好的數(shù)據(jù)代碼示例
這篇文章主要給大家介紹了關(guān)于Android?webview攔截H5的接口請求并返回處理好的數(shù)據(jù)的相關(guān)資料,通過WebView的shouldInterceptRequest方法,Android可以攔截并處理WebView中的H5網(wǎng)絡(luò)請求,需要的朋友可以參考下2024-10-10
Android BottomSheet實現(xiàn)可拉伸控件
這篇文章主要為大家詳細(xì)介紹了Android BottomSheet實現(xiàn)可拉伸控件,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-11-11
詳解Android Activity中的幾種監(jiān)聽器和實現(xiàn)方式
這篇文章主要介紹了Activity中的幾種監(jiān)聽器和實現(xiàn)方式的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)使用Android,感興趣的朋友可以了解下2021-04-04
Android 6.0權(quán)限請求相關(guān)及權(quán)限分組方法
今天小編就為大家分享一篇Android 6.0權(quán)限請求相關(guān)及權(quán)限分組方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-08-08

