Last active
October 15, 2022 14:54
-
-
Save wm3/ad2d1a5c34cedf5721febd8c31af094b to your computer and use it in GitHub Desktop.
Jetpack Compose のレイアウトまとめ1: 基本的な仕組み
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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