blob: a91e0d2220fca32b326b14ca53e3847b116df49a [file]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.compose.foundation.demos.text
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.RadioButton
import androidx.compose.material.Slider
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.text.style.LineHeightStyle.Trim
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import kotlin.math.round
private val HintStyle = TextStyle(fontSize = 14.sp)
private fun Float.format(digits: Int = 2) = "%.${digits}f".format(this)
private val FontSize = 60.sp
@Preview
@Composable
fun TextLineHeightDemo() {
Column(
Modifier.verticalScroll(rememberScrollState())
.background(TextMetricColors.Default.background)
) {
var lineHeightSp = remember { mutableFloatStateOf(60f) }
var lineHeightEm = remember { mutableFloatStateOf(1f) }
var lineHeightEnabled = remember { mutableStateOf(false) }
val lineHeightStyleEnabled = remember { mutableStateOf(false) }
var lineHeightAlignment = remember {
mutableStateOf(LineHeightStyle.Default.alignment)
}
var lineHeightTrim = remember { mutableStateOf(LineHeightStyle.Default.trim) }
val includeFontPadding = remember { mutableStateOf(false) }
val applyMaxLines = remember { mutableStateOf(false) }
val ellipsize = remember { mutableStateOf(false) }
val useSizedSpan = remember { mutableStateOf(false) }
val singleLine = remember { mutableStateOf(false) }
val useTallScript = remember { mutableStateOf(false) }
Column(Modifier.padding(16.dp)) {
LineHeightConfiguration(lineHeightSp, lineHeightEm, lineHeightEnabled)
StringConfiguration(useSizedSpan, singleLine, useTallScript)
FontPaddingAndMaxLinesConfiguration(includeFontPadding, applyMaxLines, ellipsize)
LineHeightStyleConfiguration(
lineHeightStyleEnabled,
lineHeightTrim,
lineHeightAlignment
)
Spacer(Modifier.padding(16.dp))
TextWithLineHeight(
lineHeightEnabled.value,
lineHeightSp.floatValue,
lineHeightEm.floatValue,
if (lineHeightStyleEnabled.value) {
LineHeightStyle(
alignment = lineHeightAlignment.value,
trim = lineHeightTrim.value
)
} else null,
includeFontPadding.value,
applyMaxLines.value,
ellipsize.value,
useSizedSpan.value,
singleLine.value,
useTallScript.value
)
}
}
}
@Composable
private fun LineHeightConfiguration(
lineHeightSp: MutableState<Float>,
lineHeightEm: MutableState<Float>,
lineHeightEnabled: MutableState<Boolean>
) {
Column {
val density = LocalDensity.current
val lineHeightInPx = with(density) { lineHeightSp.value.sp.toPx() }
Text(
"Line height: ${lineHeightSp.value.format()}.sp [$lineHeightInPx px, $density]",
style = HintStyle
)
Row {
Checkbox(
checked = lineHeightEnabled.value,
onCheckedChange = { lineHeightEnabled.value = it }
)
SnappingSlider(
value = lineHeightSp.value,
onValueChange = {
lineHeightSp.value = it
lineHeightEm.value = 0f
lineHeightEnabled.value = true
},
steps = 11,
valueRange = 0f..120f
)
}
val fontSizeInPx = with(density) { FontSize.toPx() }
val lineHeightEmInPx = lineHeightEm.value * fontSizeInPx
Text(
"Line height: ${lineHeightEm.value.format()}.em [$lineHeightEmInPx px]",
style = HintStyle
)
Row {
Checkbox(
checked = lineHeightEnabled.value,
onCheckedChange = { lineHeightEnabled.value = it }
)
SnappingSlider(
value = lineHeightEm.value,
onValueChange = {
lineHeightEm.value = it
lineHeightSp.value = 0f
lineHeightEnabled.value = true
},
steps = 5,
valueRange = 0f..3f
)
}
}
}
@Composable
private fun LineHeightStyleConfiguration(
lineHeightStyleEnabled: MutableState<Boolean>,
lineHeightTrim: MutableState<Trim>,
lineHeightAlignment: MutableState<LineHeightStyle.Alignment>
) {
Column(Modifier.horizontalScroll(rememberScrollState())) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = lineHeightStyleEnabled.value,
onCheckedChange = { lineHeightStyleEnabled.value = it }
)
Text("LineHeightStyle", style = HintStyle)
}
Column(Modifier.padding(horizontal = 16.dp)) {
LineHeightTrimOptions(lineHeightTrim, lineHeightStyleEnabled.value)
LineHeightAlignmentOptions(lineHeightAlignment, lineHeightStyleEnabled.value)
}
}
}
@Composable
private fun LineHeightAlignmentOptions(
lineHeightAlignment: MutableState<LineHeightStyle.Alignment>,
enabled: Boolean
) {
val options = listOf(
LineHeightStyle.Alignment.Proportional,
LineHeightStyle.Alignment.Top,
LineHeightStyle.Alignment.Center,
LineHeightStyle.Alignment.Bottom
)
Row(
modifier = Modifier.selectableGroup(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "alignment:", style = HintStyle)
options.forEach { option ->
Row(
Modifier
.height(56.dp)
.selectable(
selected = (option == lineHeightAlignment.value),
onClick = { lineHeightAlignment.value = option },
role = Role.RadioButton,
enabled = enabled
),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (option == lineHeightAlignment.value),
onClick = null,
enabled = enabled
)
Text(text = option.toString().split(".").last(), style = HintStyle)
}
}
}
}
@Composable
private fun LineHeightTrimOptions(
lineHeightTrim: MutableState<Trim>,
enabled: Boolean
) {
val options = listOf(
Trim.Both,
Trim.None,
Trim.FirstLineTop,
Trim.LastLineBottom
)
Row(
modifier = Modifier.selectableGroup(),
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "trim:", style = HintStyle)
options.forEach { option ->
Row(
Modifier
.height(56.dp)
.selectable(
selected = (option == lineHeightTrim.value),
onClick = { lineHeightTrim.value = option },
role = Role.RadioButton,
enabled = enabled
),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = (option == lineHeightTrim.value),
onClick = null,
enabled = enabled
)
Text(text = option.toString().split(".").last(), style = HintStyle)
}
}
}
}
@Composable
private fun StringConfiguration(
useSizeSpan: MutableState<Boolean>,
singleLine: MutableState<Boolean>,
useTallScript: MutableState<Boolean>
) {
Column(Modifier.horizontalScroll(rememberScrollState())) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = useSizeSpan.value,
onCheckedChange = { useSizeSpan.value = it }
)
Text("Size Span", style = HintStyle)
Checkbox(
checked = singleLine.value,
onCheckedChange = { singleLine.value = it }
)
Text("Single Line", style = HintStyle)
Checkbox(
checked = useTallScript.value,
onCheckedChange = { useTallScript.value = it }
)
Text("Tall script", style = HintStyle)
}
}
}
@Composable
private fun FontPaddingAndMaxLinesConfiguration(
includeFontPadding: MutableState<Boolean>,
applyMaxLines: MutableState<Boolean>,
ellipsize: MutableState<Boolean>,
) {
Column(Modifier.horizontalScroll(rememberScrollState())) {
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = includeFontPadding.value,
onCheckedChange = { includeFontPadding.value = it }
)
Text("IncludeFontPadding", style = HintStyle)
Checkbox(
checked = applyMaxLines.value,
onCheckedChange = { applyMaxLines.value = it }
)
Text("maxLines", style = HintStyle)
Checkbox(
checked = ellipsize.value,
onCheckedChange = { ellipsize.value = it }
)
Text("ellipsize", style = HintStyle)
}
}
}
@Composable
private fun SnappingSlider(
value: Float,
onValueChange: (Float) -> Unit,
modifier: Modifier = Modifier,
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
steps: Int = 0,
snap: Boolean = true,
enabled: Boolean = true
) {
var lastValue by remember(value) { mutableFloatStateOf(value) }
val increment = valueRange.endInclusive / (steps + 1).toFloat()
val snapValue = round(value / increment / 2f) * increment
Slider(
modifier = modifier,
value = lastValue,
onValueChangeFinished = {
if (snap) {
if (lastValue != snapValue) {
lastValue = snapValue
}
}
},
onValueChange = onValueChange,
valueRange = valueRange,
steps = steps,
enabled = enabled
)
}
@Suppress("DEPRECATION")
@Composable
private fun TextWithLineHeight(
lineHeightEnabled: Boolean,
lineHeightSp: Float,
lineHeightEm: Float,
lineHeightStyle: LineHeightStyle?,
includeFontPadding: Boolean,
applyMaxLines: Boolean,
ellipsize: Boolean,
useSizeSpan: Boolean,
singleLine: Boolean,
useTallScript: Boolean
) {
val width = with(LocalDensity.current) { (FontSize.toPx() * 5).toDp() }
var string = if (singleLine) {
if (useTallScript) "ဪไ၇ဤန်" else "Abyfhpq"
} else {
if (useTallScript) "ဪไ၇ဤနဩဦဤနိမြသကိမ့်ဪไန််" else "ABCDEfgHIjKgpvylzgpvykwi"
}
if (applyMaxLines) {
string = string.repeat(4)
}
var text = AnnotatedString(string)
if (useSizeSpan) {
text = if (singleLine) {
buildAnnotatedString {
append(text)
addStyle(style = SpanStyle(fontSize = FontSize * 1.5), start = 1, end = 2)
addStyle(style = SpanStyle(fontSize = FontSize * 1.5), start = 3, end = 4)
}
} else {
buildAnnotatedString {
append(text)
addStyle(style = SpanStyle(fontSize = FontSize * 1.5), start = 1, end = 2)
addStyle(style = SpanStyle(fontSize = FontSize * 1.5), start = 10, end = 12)
addStyle(style = SpanStyle(fontSize = FontSize * 1.5), start = 18, end = 19)
}
}
}
val maxLines = if (applyMaxLines) 3 else Int.MAX_VALUE
val style = TextStyle(
fontSize = FontSize,
color = TextMetricColors.Default.text,
lineHeightStyle = lineHeightStyle,
lineHeight = if (lineHeightEnabled) {
if (lineHeightSp > 0) lineHeightSp.sp else lineHeightEm.em
} else {
TextUnit.Unspecified
},
platformStyle = PlatformTextStyle(includeFontPadding = includeFontPadding)
)
val overflow = if (ellipsize) TextOverflow.Ellipsis else TextOverflow.Clip
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
Column(Modifier.width(width)) {
TextWithMetrics(
text = text,
style = style,
maxLines = maxLines,
overflow = overflow,
softWrap = !singleLine
)
}
Spacer(Modifier.padding(16.dp))
Column(Modifier.width(width)) {
var textFieldValue by remember(text) { mutableStateOf(TextFieldValue(text)) }
TextFieldWithMetrics(
value = textFieldValue,
onValueChange = {
textFieldValue = it
},
style = style,
maxLines = maxLines,
softWrap = !singleLine
)
}
Spacer(Modifier.padding(16.dp))
}
}