2021年4月28日水曜日

Jetpack Compose の BasicTextField の文字位置を中央揃え (centering) にする

TextAlign.Center を指定した TextStyle を渡します。 BasicTextField( ..., textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center), ... ) BasicTextField( ..., textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), ... ) BasicTextField( ..., textStyle = MaterialTheme.typography.body1.copy(textAlign = TextAlign.Center), ... ) MDC の TextField と OutlinedTextField は innerTextField の位置が動かせないので、幅が innerTextField より大きい場合は左に寄った innerTextField の中で中央揃えになってしまいます。そのうち設定できるようになるのかもしれません。
@Composable fun CenteringTextField() { Column( modifier = Modifier .fillMaxSize() .padding(16.dp) ) { var text1 by remember { mutableStateOf("") } TextField( value = text1, onValueChange = { text1 = it }, textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center), ) Spacer(modifier = Modifier.height(32.dp)) var text2 by remember { mutableStateOf("") } OutlinedTextField( value = text2, onValueChange = { text2 = it }, textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center) ) Spacer(modifier = Modifier.height(32.dp)) var text3 by remember { mutableStateOf("") } BasicTextField( value = text3, onValueChange = { text3 = it }, textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center), decorationBox = { Box( contentAlignment = Alignment.Center, modifier = Modifier .background(Color.LightGray) .padding(16.dp) ) { it() } } ) Spacer(modifier = Modifier.height(32.dp)) var text4 by remember { mutableStateOf("") } BasicTextField( value = text4, onValueChange = { text4 = it }, textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center), decorationBox = { Box( contentAlignment = Alignment.Center, modifier = Modifier .background(Color.LightGray) .padding(16.dp) ) { it() } }, modifier = Modifier.fillMaxWidth() ) } }

2021年4月22日木曜日

Modifier の width と requiredWidth と wrapContentWidth

↑の3番目について

wrapContentWith() を指定すると、そのコンテンツの計測したサイズが指定された minWidth よりも小さい場合、minWidth の幅の中に align(デフォルトでは Alignment.CenterHorizontal) で配置される。
↑の3番目では、"Hello" の横幅が指定された minWidth の 70.dp よりも小さいので、70.dp の幅の真ん中に Text が配置されている。

50.dp の Box の中に Text を配置している。

1番目は Text に width として 50.dp よりも大きい 100.dp を指定しているが、parent の maxWidth である 50.dp が利用される。

2番目は requiredWidth で 100.dp を指定しているので、parent の maxWidth によらずそれが利用される。

3番目は wrapContentWidth() を指定していて、かつ "Hello World" の幅が 50.dp よりも大きいが unbounded が false(デフォルトは false) なので parent の maxWidth が利用される。

4〜6番目は wrapContentWidth() を指定していて、かつ "Hello World" の幅が 50.dp よりも大きく unbounded が true なので "Hello World" の幅が利用され、align の指定にそって配置される。 Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .width(100.dp) ) } Spacer(Modifier.height(24.dp)) Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .requiredWidth(100.dp) ) } Spacer(Modifier.height(24.dp)) Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .wrapContentWidth() ) } Spacer(Modifier.height(24.dp)) Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .wrapContentWidth(align = Alignment.Start, unbounded = true) ) } Spacer(Modifier.height(24.dp)) Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .wrapContentWidth(align = Alignment.CenterHorizontally, unbounded = true) ) } Spacer(Modifier.height(24.dp)) Box( Modifier .size(50.dp) .background(Color.LightGray) ) { GrayText( "Hello Android", modifier = Modifier .wrapContentWidth(align = Alignment.End, unbounded = true) ) }



2021年4月15日木曜日

Jetpack Compose の Composition Local ってなんなの?

Jetpack Compose には Composition Local という仕組みが用意されています。
これを使うとバケツリレー問題に対応することができます。

バケツリレー問題

例えば深いところの Composable で Context が必要だとします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val context: Context = this setContent { MyAppTheme { MyApp(context) } } } } @Composable fun MyApp(context:Context) { TopScreen(context) } @Composable fun TopScreen(context:Context) { MyList(context) } @Composable fun MyList(context:Context) { // use context } この場合 Context を必要としているのは MyList ですが、MyList に Context を渡すために

MyApp -> TopScreen -> MyList

と Context をバケツリレーしています。

しかしこの方法では、途中の MyApp と TopScreen では Context を使わないにもかかわらず、受け渡しのためだけに引数に Context が必要になってしまいます。 その Composable で使わないものが引数にあると、何をその Compose で必要としているのかがわかりににくくなってしまいます。

Composition Local

Composition Local を使うと、深いところの Composable にデータを渡すことができます。
上のコードを Composition Local を使って書き換えるとこのようになります。
後ほどそれぞれを詳しく説明します。 private val MyLocalContext = staticCompositionLocalOf<Context> { error("No current Context") } class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val context: Context = this setContent { MyAppTheme { CompositionLocalProvider(MyLocalContext provides context) { MyApp() } } } } } @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val context:Context = MyLocalContext.current // use context } 実は Context を受け渡す Composition Local は Jetpack Compose の方で用意されています。それが LocalContext です。
LocalContext を使って書き換えるとこのようになります。
Context が欲しい Composable で LocalContext.current から Context のインスタンスが取得できます。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MyAppTheme { MyApp() } } } } @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val context:Context = LocalContext.current // use context }

用意されている Composition Local

LocalContext 以外にも LocalDensity, LocalConfiguration, LocalLifecycleOwner, LocalContentColor などが用意されています。


独自の Composition Local を作る

まず、受け渡したい値のホルダーになる ProvidableCompositionLocal を作ります。作るには staticCompositionLocalOf() か compositionLocalOf() を使います。 private val MyLocalColor: ProvidableCompositionLocal<Color> = compositionLocalOf<Color> { error("No current color") } staticCompositionLocalOf() および compositionLocalOf() に渡す lambda はデフォルト値のファクトリーです。値が与えられる前に取得しようとすると、このファクトリーが呼ばれます。

staticCompositionLocalOf() だと StaticProvidableCompositionLocal が生成され、compositionLocalOf() だと DynamicProvidableCompositionLocal が生成されます。

staticCompositionLocalOf() (StaticProvidableCompositionLocal) は滅多に変わらない値に使います。

例えば LocalContext や LocalLifecycleOwner は staticCompositionLocalOf() を使っていますが、LocalConfiguration は compositionLocalOf() を使っています。



ProvidableCompositionLocal の provides() または providesDefault() で 値を指定して ProvidedValue を取得します。
provides() および providesDefault() には infix がついているので、MyLocalColor provides Color.Black のように記述できます。

次に、取得した ProvidedValue を CompositionLocalProvider Composable に渡します。
CompositionLocalProvider の第1引数は vararg values: ProvidedValue<*> なので、複数渡すこともできます。 CompositionLocalProvider(MyLocalColor provides Color.Black) { MyApp() } こうすると、CompositionLocalProvider の content lambda 内の Composable では ProvidableCompositionLocal.current で指定された値を取得することができます。 @Composable fun MyApp() { TopScreen() } @Composable fun TopScreen() { MyList() } @Composable fun MyList() { val color:Color = MyLocalColor.current // Color.Black }

provides() で指定された値を上書きできます。 CompositionLocalProvider(MyLocalColor provides Color.Black) { Column { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black CompositionLocalProvider(MyLocalColor provides Color.Red) { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Red } } } providesDefault() ではすでに指定された値がある場合は上書きしません。 CompositionLocalProvider(MyLocalColor provides Color.Black) { Column { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black CompositionLocalProvider(MyLocalColor providesDefault Color.Red) { Text("Hello", color = MyLocalColor.current) // 文字色は Color.Black のまま } } }

用意されている Composable での Composition Local の利用

Jetpack Compose で用意されている Composable では Composition Local がたくさん使われています。
ここでは一部を紹介します。

例えば string resource id を指定して文字列を取得する stringResource() では内部で Resources インスタンスを取得する resources() を使っています。この resources() では LocalContext から Context を取得しています。 @Composable @ReadOnlyComposable private fun resources(): Resources { LocalConfiguration.current return LocalContext.current.resources }

Text Composable ではデフォルトのスタイルとして LocalTextStyle から TextStyle を取得しています。また、文字色の指定がないときは LocalContentColor と LocalContentAlpha が使われます。 @Composable fun Text( text: AnnotatedString, ... style: TextStyle = LocalTextStyle.current ) { val textColor = color.takeOrElse { style.color.takeOrElse { LocalContentColor.current.copy(alpha = LocalContentAlpha.current) } } ... } よって CompositionLocalProvider で LocalTextStyle を上書きすると、Text のデフォルトスタイルが変わります。 CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h3) { Text("Hello") // h3 }

Surface では LocalContentColor を contentColor で上書きしています。
contentColor はデフォルトでは color (背景色) から導出 (color が primary なら onPrimary, color が secondary から onSecondary など) しています。

これにより Surface の color を指定することで、内部に置かれた LocalContentColor を使っている Text などの色が自動で変わるようになっています。 @Composable fun Surface( ... color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), ... content: @Composable () -> Unit ) { ... CompositionLocalProvider( LocalContentColor provides contentColor, LocalAbsoluteElevation provides absoluteElevation ) { Box( ... ) { content() } } }


2021年4月2日金曜日

Compose メモ : Icon + clickable より IconButton を使う

IconButton だと ripple がいい感じになります。 accesibility の処理も入っているし、ボタンの大きさが 48dp になるような指定も入っています。

上が Icon + clickable で 下が IconButton です。 Column(modifier = Modifier.padding(16.dp)) { Icon( imageVector = Icons.Filled.Favorite, contentDescription = "favorite", modifier = Modifier .clickable { } .padding(12.dp) .size(24.dp) ) Spacer(modifier = Modifier.height(48.dp)) IconButton(onClick = { /*TODO*/ }) { Icon( imageVector = Icons.Filled.Favorite, contentDescription = "favorite", modifier = Modifier .size(24.dp) ) } } IconButton は ripple がいい感じです。まるいし。



Compose メモ : 2つの色を重ねた色を作る

fun Color.compositeOver(background: Color): Color を使う。

例えば #232323 のグレーに透明度15%の白を重ねたときの色(不透明のグレー)を作りたい場合は、このようになる。 val gray = Color(0xFF232323) val white150 = Color.White.copy(alpha = 0.15f) val compositeColor = white150.compositeOver(gray) Card の背景は不透明じゃないと影が変になる。デフォルトでは Card の backgroundColor は MaterialTheme の surface なので、MaterialTheme の surface に半透明の色を指定しているときは、こんな感じで compositeOver で合成した色を backgroundColor に指定するとよい。 Card( backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background), ) { }
以下のコードでは、MaterialTheme の surface に透明度15%の白を指定しているので、左では影が変になっている。右は compositeOver を使って作った不透明の色(ベースの色が不透明のグレーだから不透明になる)を backgroundColor に指定しているので、意図通りの表示になっている。 MaterialTheme( colors = lightColors( background = Color(0xFF232323), surface = Color.White.copy(alpha = 0.15f) ) ) { Surface(color = MaterialTheme.colors.background) { Row( modifier = Modifier .padding(top = 8.dp) .fillMaxSize() ) { Card( modifier = Modifier .padding(start = 8.dp, bottom = 8.dp) .size(136.dp) ) { // 左のカード } Card( backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background), modifier = Modifier .padding(start = 8.dp, bottom = 8.dp) .size(136.dp) ) { // 右のカード } } } }



Compose メモ : Card を使う時は bottom に padding を入れないと影が切れる

Card の bottom に padding を入れていないとこのように影が切れてしまう。 Row( modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 16.dp) ) { repeat(10) { Card( modifier = Modifier .padding( start = 8.dp, // bottom padding がない ) .size(136.dp) ) { } } }


このように bottom に padding を入れると切れない。
(top はこのコードのように padding を入れていなくても影が出る) Row( modifier = Modifier .horizontalScroll(rememberScrollState()) .padding(top = 16.dp) ) { repeat(10) { Card( modifier = Modifier .padding( start = 8.dp, bottom = 8.dp, // 追加 ) .size(136.dp) ) { } } }



Compose メモ : baseline を基準とした余白

Modifier .paddingFromBaseline(top = 24.dp, bottom = 16.dp)