2021年4月28日水曜日

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

TextAlign.Center を指定した TextStyle を渡します。
  1. BasicTextField(  
  2.     ...,  
  3.     textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),  
  4.     ...  
  5. )  
  6.     
  7. BasicTextField(  
  8.     ...,  
  9.     textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),  
  10.     ...  
  11. )  
  12.   
  13. BasicTextField(  
  14.     ...,  
  15.     textStyle = MaterialTheme.typography.body1.copy(textAlign = TextAlign.Center),  
  16.     ...  
  17. )  
MDC の TextField と OutlinedTextField は innerTextField の位置が動かせないので、幅が innerTextField より大きい場合は左に寄った innerTextField の中で中央揃えになってしまいます。そのうち設定できるようになるのかもしれません。
  1. @Composable  
  2. fun CenteringTextField() {  
  3.     Column(  
  4.         modifier = Modifier  
  5.             .fillMaxSize()  
  6.             .padding(16.dp)  
  7.     ) {  
  8.         var text1 by remember { mutableStateOf("") }  
  9.         TextField(  
  10.             value = text1,  
  11.             onValueChange = { text1 = it },  
  12.             textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),  
  13.         )  
  14.   
  15.         Spacer(modifier = Modifier.height(32.dp))  
  16.   
  17.         var text2 by remember { mutableStateOf("") }  
  18.         OutlinedTextField(  
  19.             value = text2,  
  20.             onValueChange = { text2 = it },  
  21.             textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center)  
  22.         )  
  23.   
  24.         Spacer(modifier = Modifier.height(32.dp))  
  25.   
  26.         var text3 by remember { mutableStateOf("") }  
  27.         BasicTextField(  
  28.             value = text3,  
  29.             onValueChange = { text3 = it },  
  30.             textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),  
  31.             decorationBox = {  
  32.                 Box(  
  33.                     contentAlignment = Alignment.Center,  
  34.                     modifier = Modifier  
  35.                         .background(Color.LightGray)  
  36.                         .padding(16.dp)  
  37.                 ) {  
  38.                     it()  
  39.                 }  
  40.             }  
  41.         )  
  42.   
  43.         Spacer(modifier = Modifier.height(32.dp))  
  44.   
  45.         var text4 by remember { mutableStateOf("") }  
  46.         BasicTextField(  
  47.             value = text4,  
  48.             onValueChange = { text4 = it },  
  49.             textStyle = TextStyle.Default.copy(textAlign = TextAlign.Center),  
  50.             decorationBox = {  
  51.                 Box(  
  52.                     contentAlignment = Alignment.Center,  
  53.                     modifier = Modifier  
  54.                         .background(Color.LightGray)  
  55.                         .padding(16.dp)  
  56.                 ) {  
  57.                     it()  
  58.                 }  
  59.             },  
  60.             modifier = Modifier.fillMaxWidth()  
  61.         )  
  62.     }  
  63. }  

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 の指定にそって配置される。
  1. Box(  
  2.     Modifier  
  3.         .size(50.dp)  
  4.         .background(Color.LightGray)  
  5. ) {  
  6.     GrayText(  
  7.         "Hello Android",  
  8.         modifier = Modifier  
  9.             .width(100.dp)  
  10.     )  
  11. }  
  12.   
  13. Spacer(Modifier.height(24.dp))  
  14.   
  15. Box(  
  16.     Modifier  
  17.         .size(50.dp)  
  18.         .background(Color.LightGray)  
  19. ) {  
  20.     GrayText(  
  21.         "Hello Android",  
  22.         modifier = Modifier  
  23.             .requiredWidth(100.dp)  
  24.     )  
  25. }  
  26.   
  27. Spacer(Modifier.height(24.dp))  
  28.   
  29. Box(  
  30.     Modifier  
  31.         .size(50.dp)  
  32.         .background(Color.LightGray)  
  33. ) {  
  34.     GrayText(  
  35.         "Hello Android",  
  36.         modifier = Modifier  
  37.             .wrapContentWidth()  
  38.     )  
  39. }  
  40.   
  41. Spacer(Modifier.height(24.dp))  
  42.   
  43. Box(  
  44.     Modifier  
  45.         .size(50.dp)  
  46.         .background(Color.LightGray)  
  47. ) {  
  48.     GrayText(  
  49.         "Hello Android",  
  50.         modifier = Modifier  
  51.             .wrapContentWidth(align = Alignment.Start, unbounded = true)  
  52.     )  
  53. }  
  54.   
  55. Spacer(Modifier.height(24.dp))  
  56.   
  57. Box(  
  58.     Modifier  
  59.         .size(50.dp)  
  60.         .background(Color.LightGray)  
  61. ) {  
  62.     GrayText(  
  63.         "Hello Android",  
  64.         modifier = Modifier  
  65.             .wrapContentWidth(align = Alignment.CenterHorizontally, unbounded = true)  
  66.     )  
  67. }  
  68.   
  69. Spacer(Modifier.height(24.dp))  
  70.   
  71. Box(  
  72.     Modifier  
  73.         .size(50.dp)  
  74.         .background(Color.LightGray)  
  75. ) {  
  76.     GrayText(  
  77.         "Hello Android",  
  78.         modifier = Modifier  
  79.             .wrapContentWidth(align = Alignment.End, unbounded = true)  
  80.     )  
  81. }  




2021年4月15日木曜日

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

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

バケツリレー問題

例えば深いところの Composable で Context が必要だとします。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         val context: Context = this  
  7.   
  8.         setContent {  
  9.             MyAppTheme {  
  10.                 MyApp(context)  
  11.             }  
  12.         }  
  13.     }  
  14. }  
  15.   
  16. @Composable  
  17. fun MyApp(context:Context) {  
  18.     TopScreen(context)  
  19. }  
  20.   
  21. @Composable  
  22. fun TopScreen(context:Context) {  
  23.     MyList(context)  
  24. }  
  25.   
  26. @Composable  
  27. fun MyList(context:Context) {  
  28.     // use context  
  29. }  
この場合 Context を必要としているのは MyList ですが、MyList に Context を渡すために

MyApp -> TopScreen -> MyList

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

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

Composition Local

Composition Local を使うと、深いところの Composable にデータを渡すことができます。
上のコードを Composition Local を使って書き換えるとこのようになります。
後ほどそれぞれを詳しく説明します。
  1. private val MyLocalContext = staticCompositionLocalOf<Context> {   
  2.     error("No current Context")   
  3. }  
  4.   
  5. class MainActivity : AppCompatActivity() {  
  6.   
  7.     override fun onCreate(savedInstanceState: Bundle?) {  
  8.         super.onCreate(savedInstanceState)  
  9.   
  10.         val context: Context = this  
  11.   
  12.         setContent {  
  13.             MyAppTheme {  
  14.                 CompositionLocalProvider(MyLocalContext provides context) {  
  15.                     MyApp()  
  16.                 }  
  17.             }  
  18.         }  
  19.     }  
  20. }  
  21.   
  22. @Composable  
  23. fun MyApp() {  
  24.     TopScreen()  
  25. }  
  26.   
  27. @Composable  
  28. fun TopScreen() {  
  29.     MyList()  
  30. }  
  31.   
  32. @Composable  
  33. fun MyList() {  
  34.     val context:Context = MyLocalContext.current  
  35.     // use context  
  36. }  
実は Context を受け渡す Composition Local は Jetpack Compose の方で用意されています。それが LocalContext です。
LocalContext を使って書き換えるとこのようになります。
Context が欲しい Composable で LocalContext.current から Context のインスタンスが取得できます。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         setContent {  
  7.             MyAppTheme {  
  8.                 MyApp()  
  9.             }  
  10.         }  
  11.     }  
  12. }  
  13.   
  14. @Composable  
  15. fun MyApp() {  
  16.     TopScreen()  
  17. }  
  18.   
  19. @Composable  
  20. fun TopScreen() {  
  21.     MyList()  
  22. }  
  23.   
  24. @Composable  
  25. fun MyList() {  
  26.     val context:Context = LocalContext.current  
  27.     // use context  
  28. }  


用意されている Composition Local

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


独自の Composition Local を作る

まず、受け渡したい値のホルダーになる ProvidableCompositionLocal を作ります。作るには staticCompositionLocalOf() か compositionLocalOf() を使います。
  1. private val MyLocalColor: ProvidableCompositionLocal<Color> = compositionLocalOf<Color> {  
  2.     error("No current color")  
  3. }  
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<*> なので、複数渡すこともできます。
  1. CompositionLocalProvider(MyLocalColor provides Color.Black) {  
  2.     MyApp()  
  3. }  
こうすると、CompositionLocalProvider の content lambda 内の Composable では ProvidableCompositionLocal.current で指定された値を取得することができます。
  1. @Composable  
  2. fun MyApp() {  
  3.     TopScreen()  
  4. }  
  5.   
  6. @Composable  
  7. fun TopScreen() {  
  8.     MyList()  
  9. }  
  10.   
  11. @Composable  
  12. fun MyList() {  
  13.     val color:Color = MyLocalColor.current // Color.Black  
  14. }  


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


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

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

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


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


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

これにより Surface の color を指定することで、内部に置かれた LocalContentColor を使っている Text などの色が自動で変わるようになっています。
  1. @Composable  
  2. fun Surface(  
  3.     ...  
  4.     color: Color = MaterialTheme.colors.surface,  
  5.     contentColor: Color = contentColorFor(color),  
  6.     ...  
  7.     content: @Composable () -> Unit  
  8. ) {  
  9.     ...  
  10.     CompositionLocalProvider(  
  11.         LocalContentColor provides contentColor,  
  12.         LocalAbsoluteElevation provides absoluteElevation  
  13.     ) {  
  14.         Box(  
  15.             ...  
  16.         ) {  
  17.             content()  
  18.         }  
  19.     }  
  20. }  



2021年4月2日金曜日

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

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

上が Icon + clickable で 下が IconButton です。
  1. Column(modifier = Modifier.padding(16.dp)) {  
  2.     Icon(  
  3.         imageVector = Icons.Filled.Favorite,  
  4.         contentDescription = "favorite",  
  5.         modifier = Modifier  
  6.             .clickable { }  
  7.             .padding(12.dp)  
  8.             .size(24.dp)  
  9.     )  
  10.   
  11.     Spacer(modifier = Modifier.height(48.dp))  
  12.   
  13.     IconButton(onClick = { /*TODO*/ }) {  
  14.         Icon(  
  15.             imageVector = Icons.Filled.Favorite,  
  16.             contentDescription = "favorite",  
  17.             modifier = Modifier  
  18.                 .size(24.dp)  
  19.         )  
  20.     }  
  21. }  
IconButton は ripple がいい感じです。まるいし。



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

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

例えば #232323 のグレーに透明度15%の白を重ねたときの色(不透明のグレー)を作りたい場合は、このようになる。
  1. val gray = Color(0xFF232323)  
  2. val white150 = Color.White.copy(alpha = 0.15f)  
  3.   
  4. val compositeColor = white150.compositeOver(gray)  
Card の背景は不透明じゃないと影が変になる。デフォルトでは Card の backgroundColor は MaterialTheme の surface なので、MaterialTheme の surface に半透明の色を指定しているときは、こんな感じで compositeOver で合成した色を backgroundColor に指定するとよい。
  1. Card(  
  2.     backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background),  
  3. ) {  
  4. }  

以下のコードでは、MaterialTheme の surface に透明度15%の白を指定しているので、左では影が変になっている。右は compositeOver を使って作った不透明の色(ベースの色が不透明のグレーだから不透明になる)を backgroundColor に指定しているので、意図通りの表示になっている。
  1. MaterialTheme(  
  2.     colors = lightColors(  
  3.         background = Color(0xFF232323),  
  4.         surface = Color.White.copy(alpha = 0.15f)  
  5.     )  
  6. ) {  
  7.     Surface(color = MaterialTheme.colors.background) {  
  8.         Row(  
  9.             modifier = Modifier  
  10.                 .padding(top = 8.dp)  
  11.                 .fillMaxSize()  
  12.         ) {  
  13.             Card(  
  14.                 modifier = Modifier  
  15.                     .padding(start = 8.dp, bottom = 8.dp)  
  16.                     .size(136.dp)  
  17.             ) {  
  18.                // 左のカード  
  19.             }  
  20.   
  21.             Card(  
  22.                 backgroundColor = MaterialTheme.colors.surface.compositeOver(MaterialTheme.colors.background),  
  23.                 modifier = Modifier  
  24.                     .padding(start = 8.dp, bottom = 8.dp)  
  25.                     .size(136.dp)  
  26.             ) {  
  27.                // 右のカード  
  28.             }  
  29.         }  
  30.     }  
  31. }  



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

Card の bottom に padding を入れていないとこのように影が切れてしまう。
  1. Row(  
  2.     modifier = Modifier  
  3.         .horizontalScroll(rememberScrollState())  
  4.         .padding(top = 16.dp)  
  5. ) {  
  6.     repeat(10) {  
  7.         Card(  
  8.             modifier = Modifier  
  9.                 .padding(  
  10.                     start = 8.dp,  
  11.                     // bottom padding がない  
  12.                 )  
  13.                 .size(136.dp)  
  14.         ) {  
  15.   
  16.         }  
  17.     }  
  18. }  


このように bottom に padding を入れると切れない。
(top はこのコードのように padding を入れていなくても影が出る)
  1. Row(  
  2.     modifier = Modifier  
  3.         .horizontalScroll(rememberScrollState())  
  4.         .padding(top = 16.dp)  
  5. ) {  
  6.     repeat(10) {  
  7.         Card(  
  8.             modifier = Modifier  
  9.                 .padding(  
  10.                     start = 8.dp,  
  11.                     bottom = 8.dp, // 追加  
  12.                 )  
  13.                 .size(136.dp)  
  14.         ) {  
  15.   
  16.         }  
  17.     }  
  18. }  



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

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