Skip to content

Instantly share code, notes, and snippets.

@wm3
Last active October 15, 2022 14:54
Show Gist options
  • Save wm3/ad2d1a5c34cedf5721febd8c31af094b to your computer and use it in GitHub Desktop.
Save wm3/ad2d1a5c34cedf5721febd8c31af094b to your computer and use it in GitHub Desktop.
Jetpack Compose のレイアウトまとめ1: 基本的な仕組み
import android.graphics.Paint
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.layout
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.PlatformTextStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.*
import java.text.DecimalFormat
import kotlin.math.roundToInt
// ----------------------------------------------------------------
// padding
// ----------------------------------------------------------------
@Preview
@Composable
fun Sample1Padding() {
Box(Modifier.size(120.dp).background(Color.White).inspect(Color.Black)) {
Box(modifier = Modifier.padding(8.dp).size(80.dp).inspect(Color.Blue))
Box(modifier = Modifier.size(80.dp).padding(8.dp).inspect(Color.Red))
}
}
@Preview
@Composable
fun Sample1NestedComponent() {
@Composable
fun SizeModifier(size: Dp, content: @Composable () -> Unit) {
Box(Modifier.size(size).inspect(Color.Blue), propagateMinConstraints = true) { content() }
}
@Composable
fun PaddingModifier(padding: Dp, content: @Composable () -> Unit) {
Box(
Modifier.padding(padding).inspect(Color.Red),
propagateMinConstraints = true
) { content() }
}
Box(Modifier.background(Color.White)) {
SizeModifier(80.dp) {
PaddingModifier(8.dp) {
Text("hello")
}
}
}
}
// ----------------------------------------------------------------
// constraints
// ----------------------------------------------------------------
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsSize() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.before().size(40.dp).after())
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsFillMaxSize() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.before().fillMaxSize().after())
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsSizeIn() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.before().sizeIn(20.dp, 20.dp, 60.dp, 60.dp).after())
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsPadding() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.sizeIn(40.dp, 40.dp).before().padding(8.dp).after())
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsBox() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.size(80.dp).before()) { Box(Modifier.after()) }
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsWrapContentSize() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.size(80.dp).before().wrapContentSize().after())
}
@Preview
@Composable
@Suppress("RemoveRedundantQualifierName")
fun Sample2ConstraintsPropagateMinConstraints() = Sample2ConstraintsTemplate { before, after ->
Box(Modifier.sizeIn(40.dp, 40.dp).before(), propagateMinConstraints = true) {
Box(Modifier.after())
}
}
@Composable
private fun Sample2ConstraintsTemplate(
content: @Composable (before: Modifier.() -> Modifier, after: Modifier.() -> Modifier) -> Unit,
) {
Row(
Modifier
.size(280.dp, 120.dp)
.background(Color.White)
.wrapContentSize(Alignment.Center),
Arrangement.spacedBy(16.dp),
Alignment.CenterVertically,
) {
Box(Modifier.sizeIn(maxWidth = 80.dp, maxHeight = 80.dp), Alignment.CenterStart) {
content(
before = { inspectConstraints(Color.Blue) },
after = { this }
)
}
Text("→", fontSize = 32.sp)
Box(Modifier.sizeIn(maxWidth = 80.dp, maxHeight = 80.dp), Alignment.CenterStart) {
content(
before = { this },
after = { inspectConstraints(Color.Red) }
)
}
}
}
// ----------------------------------------------------------------
// size
// ----------------------------------------------------------------
@Preview
@Composable
private fun Sample3ColumnSize() {
@OptIn(ExperimentalTextApi::class)
ProvideTextStyle(
TextStyle(
fontSize = 20.sp,
lineHeight = 36.sp,
platformStyle = PlatformTextStyle(includeFontPadding = false),
lineHeightStyle = LineHeightStyle(
LineHeightStyle.Alignment.Center,
LineHeightStyle.Trim.None
)
)
) {
Row(
Modifier.background(Color.White).padding(16.dp),
Arrangement.spacedBy(16.dp),
Alignment.CenterVertically,
) {
Box(Modifier.size(90.dp, 140.dp)) {
Column {
val modifier = Modifier
.inspectConstraints(Color.Blue, false)
.inspect(Color.Red)
Text(text = "Dolphin", modifier = modifier)
Text(text = "Electric Eel", modifier = modifier)
}
}
Text("→", fontSize = 32.sp)
Box(Modifier.size(90.dp, 140.dp)) {
Column(Modifier.inspect(Color.Red)) {
Text(text = "Dolphin")
Text(text = "Electric Eel")
}
}
}
}
}
// ----------------------------------------------------------------
// utilities
// ----------------------------------------------------------------
data class InspectConstraintsState(
val widthIn: ClosedRange<Float>,
val heightIn: ClosedRange<Float>,
val density: Float,
) {
constructor(constraints: Constraints, density: Float) : this(
constraints.minWidth.toFloat()..constraints.maxWidth.toFloat(),
constraints.minHeight.toFloat()..constraints.maxHeight.toFloat(),
density,
)
private val format = DecimalFormat("0.##")
private fun Float.toDpText() = format.format(this / density)
val constraintsText: String
get() = "[${widthIn.start.toDpText()}-${widthIn.endInclusive.toDpText()}]" +
"x[${heightIn.start.toDpText()}-${heightIn.endInclusive.toDpText()}]"
}
fun Modifier.inspectConstraints(color: Color, fullSize: Boolean = true): Modifier = composed {
var state by remember { mutableStateOf<InspectConstraintsState?>(null) }
val density = LocalDensity.current.density
val lineHeight = 9f * density
this
.layout { measurable, constraints ->
val res = measurable.measure(constraints)
state = InspectConstraintsState(constraints, density)
layout(
if (fullSize) constraints.maxWidth else res.width,
if (fullSize) constraints.maxHeight else res.height,
) {
res.placeRelative(0, 0)
}
}
.drawBehind {
val s = state ?: return@drawBehind
val path = Path().apply {
addRect(Rect(0f, 0f, s.widthIn.endInclusive, s.heightIn.endInclusive))
moveTo(0f, 0f)
lineTo(s.widthIn.start, 0f)
lineTo(s.widthIn.start, s.heightIn.start)
lineTo(0f, s.heightIn.start)
lineTo(0f, 0f)
}
drawPath(path, color = color.copy(alpha = 0.3f))
drawRect(
color = color,
size = Size(s.widthIn.start, s.heightIn.start),
style = Stroke(s.density)
)
drawRect(
color = color,
size = Size(s.widthIn.endInclusive, s.heightIn.endInclusive),
style = Stroke(s.density)
)
}
.drawWithContent {
drawContent()
val s = state ?: return@drawWithContent
drawText(
s.constraintsText,
Offset(lineHeight / 4, s.heightIn.start + lineHeight),
color,
lineHeight
)
}
}
private fun Modifier.inspect(color: Color): Modifier = composed {
var state by remember { mutableStateOf<String?>(null) }
val density = LocalDensity.current.density
val lineHeight = 9.dp.value * density
this
.onPlaced {
val size = it.size.toSize() / density
state = "${size.width.roundToInt()}x${size.height.roundToInt()}"
}
.border(1.dp, color)
.drawWithContent {
drawContent()
drawText(
state ?: return@drawWithContent,
Offset(lineHeight / 4, size.height - lineHeight / 4),
color,
lineHeight
)
}
}
private fun DrawScope.drawText(text: String, offset: Offset, color: Color, lineHeight: Float) {
val paint = Paint().also {
it.textSize = lineHeight
it.color = color.toArgb()
}
drawIntoCanvas {
it.nativeCanvas.drawText(text, offset.x, offset.y, paint)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment