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進(jìn)行一個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使用PullToRefresh實現(xiàn)上拉加載和下拉刷新效果的代碼
這篇文章主要介紹了Android使用PullToRefresh實現(xiàn)上拉加載和下拉刷新效果 的相關(guān)資料,需要的朋友可以參考下2016-07-07詳解android studio游戲搖桿開發(fā)教程,仿王者榮耀搖桿
這篇文章主要介紹了android studio游戲搖桿開發(fā)教程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05AndroidStudio代碼達(dá)到指定字符長度時自動換行實例
這篇文章主要介紹了AndroidStudio代碼達(dá)到指定字符長度時自動換行實例,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03Android 控件(button)對齊方法實現(xiàn)詳解
horizontal是讓所有的子元素按水平方向從左到右排列,vertical是讓所有的子元素按豎直方向從上到下排列,下面為大家介紹下控件(button)的對齊方法2013-06-06Android scrollToTop實現(xiàn)點擊回到頂部(兼容PullTorefreshScrollview)
當(dāng)頁面滑動到底部,出現(xiàn)回到頂部的按鈕相信對大家來說并不陌生,下面這篇文章主要介紹了關(guān)于Android scrollToTop實現(xiàn)點擊回到頂部,并兼容PullTorefreshScrollview的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒。2017-03-03