2023年8月2日 星期三

[Android] Jetpack Compose Text With Underline

 參考這篇文章後的補充。

取得TextLayoutResult的方法有兩種。

第一種:

    val textLayoutResultState = remember { mutableStateOf<textlayoutresult>(null) }
    Text(
        text = text,
        style = style,
        color = color,
        textAlign = textAlign,
        onTextLayout = {
            textLayoutResultState.value = it
        }
    )

第二種(style就包含fontSize和color):

    val textMeasurer = rememberTextMeasurer()
    val textResult = textMeasurer.measure(
           text = text,
           style = style
    )

取得TextLayoutResult後就可以讀取每個字空間大小

    text.indices.forEach { index ->
        val rect = textLayoutResultState.value!!.getBoundingBox(index)
        rectList.add(rect)
    }

如果要找特定字串的話可使用這方法

    val findText = "find some text"
    val startIndex = text.indexOf(findText)
    val endIndex = startIndex.plus(findText.length) - 1
    (startIndex..endIndex).forEach { index ->
        val rect = textLayoutResultState.value!!.getBoundingBox(index)
        rectList.add(rect)
    }

其中wave path的方法補充(對Path的擴展):

    const val TWO_PI = 2 * Math.PI.toFloat()
    private fun Path.buildWaveLinePath(bound: Rect, waveLength: Float, animProgress: Float): Path {
        asAndroidPath().rewind()
        var pointX = bound.left
        while (pointX < bound.right) {
            val offsetY =
                bound.bottom + sin(((pointX - bound.left) / waveLength)
                	* TWO_PI + (TWO_PI * animProgress)) * 10
            if (pointX == bound.left) {
                moveTo(bound.left, offsetY)
            }
            lineTo(pointX, offsetY)
            pointX += 1F
        }
        return this
}

使用wave path的方法:

    val infiniteTransition = rememberInfiniteTransition(label = "")
    val animProgress = infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(500, easing = LinearEasing)
        ), 
        label = ""
    )
    val start = rectList.first()!!.topLeft
    val end = rectList.last()!!.bottomRight
    val rect = Rect(start, end)
    val path = Path().buildWaveLinePath(rect, 50f, animProgress.value)
    val pathStyle = Stroke(
        width = 5f,
        pathEffect = PathEffect.cornerPathEffect(radius = 9.dp.toPx())
    )
    
    drawPath(
        color = Color.Yellow,
        path = path,
        style = pathStyle
    )    

2023年8月1日 星期二

[Android] Create Outlined Text Using Jetpack Compose

在這篇文章中,介紹了兩種方式製作OutlinedText。

第一種:使用drawStyle(限制andriodx.core:core-ktx版本需要1.4.0-alpha01以上才有)

Text(
    text = "Sample",
    style = TextStyle.Default.copy(
        fontSize = 64.sp,
        drawStyle = Stroke(
            miter = 10f,
            width = 5f,
            join = StrokeJoin.Round
        )
    )
)

缺點:沒有繪製文字,只有文字外框


第二種:使用Canvas

// Creating a outline text
@Composable
fun OutLineText() {

    // Create a Paint that has black stroke
    val textPaintStroke = Paint().asFrameworkPaint().apply {
        isAntiAlias = true
        style = android.graphics.Paint.Style.STROKE
        textSize = 100f
        color = android.graphics.Color.CYAN
        strokeWidth = 12f
        strokeMiter = 10f
        strokeJoin = android.graphics.Paint.Join.ROUND
    }

    // Create a Paint that has white fill
    val textPaint = Paint().asFrameworkPaint().apply {
        isAntiAlias = true
        style = android.graphics.Paint.Style.FILL
        textSize = 100f
        color = android.graphics.Color.WHITE
    }

    // Create a canvas, draw the black stroke and
    // override it with the white fill
    Canvas(
        modifier = Modifier.fillMaxSize(),
        onDraw = {
            drawIntoCanvas {
                it.nativeCanvas.drawText(
                        "Hello World",
                        200f,
                        200.dp.toPx(),
                        textPaintStroke
                    )

                    it.nativeCanvas.drawText(
                        "Hello World",
                        200f,
                        200.dp.toPx(),
                        textPaint
                    )
                }
            }
        )
}

缺點: 這方式能繪出文字及顏色,

            但因為使用了Canvas,所以是以畫面有效區域內的座標為位置,

            無法使用Arrangement.Center,快速置中,

            必須自行算出中點扣除文字長度一半後的座標點位。

            (無法將文字當物件使用)


將上兩種方法重新整合後,出現第三種方法

@Composable
fun OutLineText(
    text: String,
    modifier: Modifier = Modifier,
    fontSize: TextUnit,
    color: Color = Color.White,
    textAlign: TextAlign = TextAlign.Start,
    outlinedBrush: Brush = Brush.horizontalGradient(
        listOf(
            Color.Blue,
            Color.Magenta,
            Color.Red,
            Color.Yellow
        )
    ),
) {
    val textLayoutResultState = remember { mutableStateOf(null) }
    Text(
        text = text,
        fontSize = fontSize,
        color = color,
        textAlign = textAlign,
        onTextLayout = { textLayoutResultState.value = it },
        modifier = modifier
            .drawWithCache {
                onDrawBehind {
                    drawIntoCanvas {
                        drawText(
                            textLayoutResultState.value!!,
                            outlinedBrush,
                            drawStyle = Stroke(
                                miter = 20f,
                                width = 15f,
                                join = StrokeJoin.Round
                            ),
                        )
                    }
                }
            }
            .width(IntrinsicSize.Max)
    )
}

draw方法有兩種,drawWithContent和drawBehind,

drawWithContent多了一個drawContent()的方法,

讓你決定文字本體的繪製順序,

則drawBehind會繪製有在文字本體之下,背景之上,


drawWithCache提供兩個方法,

onDrawWithContent和onDrawBehind,

使用方式相同,只是結果會存在Cache中,

UI重新繪製時先檢查Cache是否有相同的結果,

如果有,就不重新計算,進而增加效能。,


想了解draw方法可以參考這篇

更進階的用法可參考這篇