2021年5月28日金曜日

Jetpack Compose : Lifecycle.Event で処理をトリガーする

通知が有効になっているかどうか調べる処理 (NotificationManagerCompat.from(context).areNotificationsEnabled()) を毎 onResume で行い場合はこんな感じになる。
  1. @Composable  
  2. fun SampleScreen(  
  3.     notificationEnabled: Boolean,  
  4.     onCheckNotification: () -> Unit  
  5. ) {  
  6.     val lifecycleObserver = remember(onCheckNotification) {  
  7.         LifecycleEventObserver { _, event ->  
  8.             if (event == Lifecycle.Event.ON_RESUME) {  
  9.                 onCheckNotification()  
  10.             }  
  11.         }  
  12.     }  
  13.     val lifecycle = LocalLifecycleOwner.current.lifecycle  
  14.     DisposableEffect(lifecycle) {  
  15.         lifecycle.addObserver(lifecycleObserver)  
  16.         onDispose {  
  17.             lifecycle.removeObserver(lifecycleObserver)  
  18.         }  
  19.     }  
  20.   
  21.     if (!notificationEnabled) {  
  22.         Text("通知設定がオフです")  
  23.     }  
  24. }  
state hoisting だとこんな感じだけど、引数 ViewModel にするならこんな感じ。
  1. @Composable  
  2. fun SampleScreen(  
  3.     viewModel: SampleViewModel = viewModel(),  
  4. ) {  
  5.     val lifecycleObserver = remember(viewModel) {  
  6.         LifecycleEventObserver { _, event ->  
  7.             if (event == Lifecycle.Event.ON_RESUME) {  
  8.                 viewModel.updateNotificationEnabledState()  
  9.             }  
  10.         }  
  11.     }  
  12.     val lifecycle = LocalLifecycleOwner.current.lifecycle  
  13.     DisposableEffect(lifecycle) {  
  14.         lifecycle.addObserver(lifecycleObserver)  
  15.         onDispose {  
  16.             lifecycle.removeObserver(lifecycleObserver)  
  17.         }  
  18.     }  
  19.   
  20.     if (!viewModel.notificationEnabled.value) {  
  21.         Text("通知設定がオフです")  
  22.     }  
  23. }  



Jetpack Compose : disabled のとき Image や Text の色を薄くしたい

Checkbox とか Switch とか enabled 設定が用意されている Composable では enabled に false を設定すると色が薄くなるよう実装されています。
  1. @Composable  
  2. fun Checkbox(  
  3.     ...  
  4.     enabled: Boolean = true,  
  5.     ...  
  6. ) {  
  7.     ...  
  8. }  
Text とか Image には enabled 設定は用意されていないので自分でがんばる必要があります。 方法としては
  • Modifier.alpha() を使う
  • LocalContentAlpha を指定する
  • Text の color に指定する色の alpha を変える
  • Image の alpha パラメータを指定する
などがあります。

Modifier.alpha() を使う

  1. val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled  
  2.   
  3. Column(  
  4.     modifier = Modifier.padding(16.dp).alpha(alpha)  
  5. ) {  
  6.     Image(Icons.Default.Android, contentDescription = null)  
  7.     Icon(Icons.Default.Home, contentDescription = null)  
  8.     Text("Android")  
  9. }  

LocalContentAlpha を指定する

LocalContentAlpha はデフォルトのときの文字色と Icon の tint color に使われていますが、Image では使われていません。
  1. val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled  
  2.   
  3. CompositionLocalProvider(LocalContentAlpha provides alpha) {  
  4.     Column(  
  5.         modifier = Modifier.padding(16.dp)  
  6.     ) {  
  7.         Image(Icons.Default.Android, contentDescription = null)  
  8.         Icon(Icons.Default.Home, contentDescription = null)  
  9.         Text("Android")  
  10.     }  
  11. }  

Text, Icon の color, tint に指定する色の alpha を変える & Image の alpha パラメータを指定する

  1. val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled  
  2.   
  3. Column(  
  4.     modifier = Modifier.padding(16.dp)  
  5. ) {  
  6.     Image(  
  7.         Icons.Default.Android,   
  8.         contentDescription = null,  
  9.         alpha = alpha  
  10.     )  
  11.     Icon(  
  12.         Icons.Default.Home,   
  13.         contentDescription = null,  
  14.         tint = MaterialTheme.colors.primary.copy(alpha = alpha)  
  15.     )  
  16.     Text(  
  17.         "Android",  
  18.         color = MaterialTheme.colors.primary.copy(alpha = alpha)  
  19.     )  
  20. }  



2021年5月27日木曜日

Jetpack Compose で AutoSizeableTextView 的なのを作る

残念ながらオフィシャルでのサポートは(まだ)ない。

Text の onTextLayout で TextLayoutResult の hasVisualOverflow (didOverflowWidth および didOverflowHeight もある) が true だったら remember {} してる文字サイズを変更するという方法でそれっぽいのは作れるが、State が変わる回数が多い(最終的な textSize と maxTextSize の差が大きい)と文字サイズが変わるアニメーションみたいになってしまうのがつらい。
  1. @Composable  
  2. fun AutoSizeableText(  
  3.     text: String,  
  4.     maxTextSize: Int = 16,  
  5.     minTextSize: Int = 14,  
  6.     modifier: Modifier  
  7. ) {  
  8.     
  9.   var textSize by remember(text) { mutableStateOf(maxTextSize) }  
  10.   
  11.   Text(  
  12.       text = text,  
  13.       fontSize = textSize.sp,  
  14.       maxLines = 1,  
  15.       overflow = TextOverflow.Ellipsis,  
  16.       modifier = modifier,  
  17.       onTextLayout = {  
  18.           if (it.hasVisualOverflow && textSize > minTextSize) {  
  19.               textSize -= 1  
  20.           }  
  21.       }  
  22.   )  
  23. }  
↑だと1文字増減したときに maxTextSize からやり直しになるので、現在の文字サイズを覚えておいてそこから +/- するようにしたのが ↓
  1. @Composable  
  2. fun AutoSizeableText(  
  3.     text: String,  
  4.     maxTextSize: Int = 16,  
  5.     minTextSize: Int = 14,  
  6.     modifier: Modifier  
  7. ) {  
  8.   
  9.     var textSize by remember { mutableStateOf(maxTextSize) }  
  10.     val checked = remember(text) { mutableMapOf<Int, Boolean?>() }  
  11.     var overflow by remember { mutableStateOf(TextOverflow.Clip) }  
  12.   
  13.     Text(  
  14.         text = text,  
  15.         fontSize = textSize.sp,  
  16.         maxLines = 1,  
  17.         overflow = overflow,  
  18.         modifier = modifier,  
  19.         onTextLayout = {  
  20.             if (it.hasVisualOverflow) {  
  21.                 checked[textSize] = true  
  22.                 if (textSize > minTextSize) {  
  23.                     textSize -= 1  
  24.                 } else {  
  25.                     overflow = TextOverflow.Ellipsis  
  26.                 }  
  27.             } else {  
  28.                 checked[textSize] = false  
  29.                 if (textSize < maxTextSize) {  
  30.                     if (checked[textSize + 1] == null) {  
  31.                         textSize += 1  
  32.                     }  
  33.                 }  
  34.             }  
  35.         }  
  36.     )  
  37. }  
それでも State が変わる回数が多い(maxTextSize と minTextSize の差が大きくて、一度に長い文字をペーストするとか)と文字サイズが変わるアニメーションみたいになってしまうのがつらいんですよね〜。





Jetpack Compose で Spannable 的なことをしたいときは AnnotatedString を作る

buildAnnotatedString 関数が用意されている。
  1. Text(  
  2.    text = buildAnnotatedString {  
  3.        append("By clicking blow you agree to our ")  
  4.   
  5.        withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {  
  6.            append("Terms of Use")  
  7.        }  
  8.   
  9.        append(" and consent \nto our ")  
  10.          
  11.        withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {  
  12.            append("Privacy Policy")  
  13.        }  
  14.          
  15.        append(".")  
  16.    },  
  17. )  
SpanStyleParagraphStyle を指定する。
  1. class SpanStyle(  
  2.     val color: Color = Color.Unspecified,  
  3.     val fontSize: TextUnit = TextUnit.Unspecified,  
  4.     val fontWeight: FontWeight? = null,  
  5.     val fontStyle: FontStyle? = null,  
  6.     val fontSynthesis: FontSynthesis? = null,  
  7.     val fontFamily: FontFamily? = null,  
  8.     val fontFeatureSettings: String? = null,  
  9.     val letterSpacing: TextUnit = TextUnit.Unspecified,  
  10.     val baselineShift: BaselineShift? = null,  
  11.     val textGeometricTransform: TextGeometricTransform? = null,  
  12.     val localeList: LocaleList? = null,  
  13.     val background: Color = Color.Unspecified,  
  14.     val textDecoration: TextDecoration? = null,  
  15.     val shadow: Shadow? = null  
  16. ) {  
  17.   ...  
  18. }  
  1. class ParagraphStyle constructor(  
  2.     val textAlign: TextAlign? = null,  
  3.     val textDirection: TextDirection? = null,  
  4.     val lineHeight: TextUnit = TextUnit.Unspecified,  
  5.     val textIndent: TextIndent? = null  
  6. ) {  
  7.   ...  
  8. }  



2021年5月26日水曜日

Jetpack Compose でフォーカスをクリアする

フォーカスをクリアするときは LocalFocusManager Composition Local から取得した FocusManager を使う
  1. val focusManager = LocalFocusManager.current  
  2.   
  3.   Button(onClick = { focusManager.clearFocus() }) {  
  4.     Text("Button")  
  5. }  



2021年5月14日金曜日

inline class および value class で kotlinx.serialization (JSON) が動く組み合わせ

inline class および value class で kotlinx.serialization (JSON) が動く組み合わせを調べてみた


inline class のときのコード
  1. @Serializable  
  2. inline class ItemId(val value: String)  
  3.   
  4. @Serializable  
  5. data class Item(val id: ItemId, val name: String)  
  6.   
  7. fun main() {  
  8.     val item = Item(ItemId("1"), "Android")  
  9.     val json = Json.encodeToString(item)  
  10.     println(json)  
  11.     println(Json.decodeFromString<Item>(json))  
  12. }  
value class のときのコード
  1. @Serializable  
  2. @JvmInline  
  3. value class ItemId(val value: String)  
  4.   
  5. @Serializable  
  6. data class Item(val id: ItemId, val name: String)  
  7.   
  8. fun main() {  
  9.     val item = Item(ItemId("1"), "Android")  
  10.     val json = Json.encodeToString(item)  
  11.     println(json)  
  12.     println(Json.decodeFromString<Item>(json))  
  13. }  

Kotlin: 14.32, kotlinx.serialization: 1.1.0 + inline class

ビルドエラーになる

e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation

Kotlin: 1.4.32, kotlinx.serialization: 1.2.1 + inline class

ビルドエラーになる

e: org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during file facade code generation

Kotlin: 1.4.32, kotlinx.serialization: 1.1.0 + value class

@JvmInline が無いのでビルドエラーになる

Kotlin: 1.4.32, kotlinx.serialization: 1.2.1 + value class

@JvmInline が無いのでビルドエラーになる

Kotlin: 1.5.0, kotlinx.serialization: 1.1.0 + inline class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.1.0 + value class

動く

Kotlin: 1.5.0, kotlinx.serialization: 1.2.1 + inline class

動く


Kotlin を 1.5.0 にすれば kotlinx.serialization を 1.2 にしなくても inline class と value class 両方で動いた。


2021年5月9日日曜日

Jetpack Compose : Canvas Composable を使う

View の onDraw() で描画して Custom View を作る、というのを Compose でやりたいときは Canvas Composable を使います。

Canvas に渡す onDraw lamnda は Receiver が DrawScope になっています。
DrawScope からは描画エリアの大きさとして size: Size が取れます。

また、DrawScope は Density を継承しているので、Dp.toPx() とかも呼び出せます。
  1. @Composable  
  2. fun CircleProgress(  
  3.     progress: Int,  
  4.     modifier: Modifier,  
  5.     colorProgress: Color = MaterialTheme.colors.primary,  
  6.     colorBackground: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)  
  7.         .compositeOver(MaterialTheme.colors.surface),  
  8.     strokeWidth: Dp = 8.dp,  
  9. ) {  
  10.     Canvas(modifier = modifier) {  
  11.         val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)  
  12.   
  13.         val diameter = min(size.width, size.height) - stroke.width  
  14.         val topLeft = Offset((size.width - diameter) / 2, (size.height - diameter) / 2)  
  15.         val circleSize = Size(diameter, diameter)  
  16.   
  17.         drawArc(  
  18.             color = colorBackground,  
  19.             startAngle = -90f,  
  20.             sweepAngle = 360f,  
  21.             useCenter = false,  
  22.             style = stroke,  
  23.             topLeft = topLeft,  
  24.             size = circleSize,  
  25.         )  
  26.   
  27.         drawArc(  
  28.             color = colorProgress,  
  29.             startAngle = -90f,  
  30.             sweepAngle = 360f / 100 * progress,  
  31.             useCenter = false,  
  32.             style = stroke,  
  33.             topLeft = topLeft,  
  34.             size = circleSize,  
  35.         )  
  36.     }  
  37. }  
  38.   
  39. @Preview  
  40. @Composable  
  41. fun CircleProgressPreview() {  
  42.     var progress by remember { mutableStateOf(0) }  
  43.     val animateProgress by animateIntAsState(targetValue = progress, animationSpec = tween())  
  44.   
  45.     Column(  
  46.         horizontalAlignment = Alignment.CenterHorizontally,  
  47.         modifier = Modifier.padding(16.dp)  
  48.     ) {  
  49.         Button(onClick = {  
  50.             progress = Random.nextInt(0101)  
  51.         }) {  
  52.             Text("Change Progress")  
  53.         }  
  54.   
  55.         Spacer(modifier = Modifier.height(16.dp))  
  56.   
  57.         CircleProgress(  
  58.             progress = animateProgress,  
  59.             modifier = Modifier.size(120.dp)  
  60.         )  
  61.     }  
  62. }  
animate**AsState などでアニメーションも簡単にできます。



Jetpack Compose で Material Design の ToggleButton を作ってみた

Material Design の ToogleButton (https://material.io/components/buttons#toggle-button) を Jetpack Compose で作ってみた。

https://github.com/yanzm/ComposeToggleButton

こんな感じのやつ。


選択の処理は Modifier.toggleable() を使えば OK。
  1. @Composable  
  2. fun IconToggleButton(  
  3.     imageVector: ImageVector,  
  4.     contentDescription: String?,  
  5.     checked: Boolean,  
  6.     onCheckedChange: (Boolean) -> Unit,  
  7.     enabled: Boolean = true  
  8. ) {  
  9.     CompositionLocalProvider(  
  10.         LocalContentColor provides contentColor(enabled = enabled, checked = checked),  
  11.     ) {  
  12.         Box(  
  13.             contentAlignment = Alignment.Center,  
  14.             modifier = Modifier  
  15.                 .toggleable(  
  16.                     value = checked,  
  17.                     onValueChange = onCheckedChange,  
  18.                     role = Role.RadioButton,  
  19.                 )  
  20.                 .size(48.dp)  
  21.         ) {  
  22.             Icon(  
  23.                 imageVector = imageVector,  
  24.                 contentDescription = contentDescription,  
  25.                 modifier = Modifier.size(24.dp)  
  26.             )  
  27.         }  
  28.     }  
  29. }  


枠線などの描画は Modifier.drawWithContent() でやったが、これが面倒だった〜(特にRTL対応)。
  1. private fun Modifier.drawToggleButtonFrame(  
  2.     ...  
  3. ): Modifier = this.drawWithContent {  
  4.   
  5.     ...  
  6.   
  7.     // draw checked border  
  8.     drawPath(  
  9.         path = ...,  
  10.         color = checkedBorderColor,  
  11.         style = Stroke(strokeWidth),  
  12.     )  
  13.   
  14.     drawContent()  
  15. }  


Jetpack Compose : Modifier.triStateToggleable() で3状態ボタンを作る

Modifier.triStateToggleable() を使った3状態チェックボックスとして TriStateCheckbox が用意されています。
  1. var state by remember { mutableStateOf(ToggleableState.On) }  
  2.   
  3. TriStateCheckbox(state = state, onClick = {  
  4.     state = when (state) {  
  5.         ToggleableState.On -> ToggleableState.Indeterminate  
  6.         ToggleableState.Indeterminate -> ToggleableState.Off  
  7.         ToggleableState.Off -> ToggleableState.On  
  8.     }  
  9. })  



Modifier.triStateToggleable() を使って独自の3状態ボタンも作ることができます。
  1. var state by remember { mutableStateOf(ToggleableState.On) }  
  2.   
  3. Box(  
  4.     contentAlignment = Alignment.Center,  
  5.     modifier = Modifier.triStateToggleable(  
  6.         state = state,  
  7.         onClick = {  
  8.             state = when (state) {  
  9.                 ToggleableState.On -> ToggleableState.Off  
  10.                 ToggleableState.Off -> ToggleableState.Indeterminate  
  11.                 ToggleableState.Indeterminate -> ToggleableState.On  
  12.             }  
  13.         },  
  14.         role = Role.Checkbox,  
  15.         interactionSource = remember { MutableInteractionSource() },  
  16.         indication = rememberRipple(  
  17.             bounded = false,  
  18.             radius = 24.dp  
  19.         )  
  20.     )  
  21. ) {  
  22.     Icon(  
  23.         imageVector = when (state) {  
  24.             ToggleableState.On -> Icons.Default.Favorite  
  25.             ToggleableState.Off -> Icons.Default.FavoriteBorder  
  26.             ToggleableState.Indeterminate -> Icons.Default.FavoriteBorder  
  27.         },  
  28.         contentDescription = when (state) {  
  29.             ToggleableState.On -> "favorite on"  
  30.             ToggleableState.Off -> "favorite on"  
  31.             ToggleableState.Indeterminate -> "favorite indeterminate"  
  32.         },  
  33.         tint = when (state) {  
  34.             ToggleableState.On -> MaterialTheme.colors.primary  
  35.             ToggleableState.Off -> MaterialTheme.colors.primary  
  36.             ToggleableState.Indeterminate -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)  
  37.         }  
  38.     )  
  39. }  



Jetpack Compose : Modifier.toggleable() で独自チェックボックスを作る

Modifier.toggleable() を使います。
  1. var checked by remember { mutableStateOf(false) }  
  2.   
  3. Box(  
  4.     contentAlignment = Alignment.Center,  
  5.     modifier = Modifier  
  6.         .toggleable(  
  7.             value = checked,  
  8.             onValueChange = { checked = it }  
  9.         )  
  10.         .size(48.dp)  
  11. ) {  
  12.     Icon(  
  13.         imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder,  
  14.         contentDescription = if (checked) "favorite on" else "favorite off",  
  15.     )  
  16. }  
Box + Modifier.toggleable() 部分は IconToggleButton として用意されているので、それを使うこともできます。
  1. var checked by remember { mutableStateOf(false) }  
  2.   
  3. IconToggleButton(  
  4.     checked = checked,  
  5.     onCheckedChange = { checked = it },  
  6. ) {  
  7.     Icon(  
  8.         imageVector = if (checked) Icons.Default.Favorite else Icons.Default.FavoriteBorder,  
  9.         contentDescription = if (checked) "favorite on" else "favorite off",  
  10.     )  
  11. }  



2021年5月2日日曜日

LazyColumn (LazyRow) の item 指定は index で頑張らなくていい

とある発表資料で見かけたのですが、LazyColumn (LazyRow)ではこういう index で頑張る方法は必要ありません。
RecyclerView がこういう頑張りをしないといけなかったので、こうやってしまう気持ちはわかります。

よくない例
  1. @Composable  
  2. fun DogList(list: List<Dog>) {  
  3.     LazyColumn {  
  4.         items(list.size + 1) {  
  5.             if (it == 0) {  
  6.                 Header()  
  7.             } else {  
  8.                 DogListItem(list[it - 1])  
  9.             }  
  10.         }  
  11.     }  
  12. }  


どうするのが良いかというと、素直に Header() と list で item/items を分ければいいんです。items() には数字ではなく List<T> をとる拡張関数が用意されています。

よい例
  1. @Composable  
  2. fun DogList(list: List<Dog>) {  
  3.     LazyColumn {  
  4.         item { Header() }  
  5.         items(list) { dog ->  
  6.             DogListItem(dog)  
  7.         }  
  8.     }  
  9. }  
また、itemsIndexed() を使うと index もとれるので、例えば dog.name の1文字目が変わったら区切りヘッダーを入れるというのもこんな感じで簡単に書けます(list は dog.name で sort されている前提)。
  1. @Composable  
  2. fun DogList(list: List<Dog>) {  
  3.     LazyColumn {  
  4.         itemsIndexed(list) { index, dog ->  
  5.             if (index == 0 || list[index - 1].name[0] != dog.name[0]) {  
  6.                 NameDivider(dog.name[0])  
  7.             }  
  8.             DogListItem(dog)  
  9.         }  
  10.     }  
  11. }  




2021年5月1日土曜日

Jetpack Compose : 角丸をパーセントで指定する

RoundedCornerShape() には Int または Float でパーセントを指定することができます。

RoundedCornerShape(50) // 50dp ではなく、50% ということ

  1. @Composable  
  2. fun Capsule() {  
  3.     Text(  
  4.         text = "Android",  
  5.         modifier = Modifier  
  6.             .padding(16.dp)  
  7.             .background(  
  8.                 color = Color.LightGray,  
  9.                 shape = RoundedCornerShape(50)  
  10.             )  
  11.             .padding(vertical = 8.dp, horizontal = 16.dp)  
  12.     )  
  13. }