2021年5月28日金曜日

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

通知が有効になっているかどうか調べる処理 (NotificationManagerCompat.from(context).areNotificationsEnabled()) を毎 onResume で行い場合はこんな感じになる。 @Composable fun SampleScreen( notificationEnabled: Boolean, onCheckNotification: () -> Unit ) { val lifecycleObserver = remember(onCheckNotification) { LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { onCheckNotification() } } } val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { lifecycle.addObserver(lifecycleObserver) onDispose { lifecycle.removeObserver(lifecycleObserver) } } if (!notificationEnabled) { Text("通知設定がオフです") } } state hoisting だとこんな感じだけど、引数 ViewModel にするならこんな感じ。 @Composable fun SampleScreen( viewModel: SampleViewModel = viewModel(), ) { val lifecycleObserver = remember(viewModel) { LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_RESUME) { viewModel.updateNotificationEnabledState() } } } val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { lifecycle.addObserver(lifecycleObserver) onDispose { lifecycle.removeObserver(lifecycleObserver) } } if (!viewModel.notificationEnabled.value) { Text("通知設定がオフです") } }


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

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

Modifier.alpha() を使う

val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled Column( modifier = Modifier.padding(16.dp).alpha(alpha) ) { Image(Icons.Default.Android, contentDescription = null) Icon(Icons.Default.Home, contentDescription = null) Text("Android") }

LocalContentAlpha を指定する

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

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

val alpha = if (enabled) LocalContentAlpha.current else ContentAlpha.disabled Column( modifier = Modifier.padding(16.dp) ) { Image( Icons.Default.Android, contentDescription = null, alpha = alpha ) Icon( Icons.Default.Home, contentDescription = null, tint = MaterialTheme.colors.primary.copy(alpha = alpha) ) Text( "Android", color = MaterialTheme.colors.primary.copy(alpha = alpha) ) }



2021年5月27日木曜日

Jetpack Compose で AutoSizeableTextView 的なのを作る

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

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





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

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


2021年5月26日水曜日

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

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


2021年5月14日金曜日

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

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


inline class のときのコード @Serializable inline class ItemId(val value: String) @Serializable data class Item(val id: ItemId, val name: String) fun main() { val item = Item(ItemId("1"), "Android") val json = Json.encodeToString(item) println(json) println(Json.decodeFromString<Item>(json)) } value class のときのコード @Serializable @JvmInline value class ItemId(val value: String) @Serializable data class Item(val id: ItemId, val name: String) fun main() { val item = Item(ItemId("1"), "Android") val json = Json.encodeToString(item) println(json) println(Json.decodeFromString<Item>(json)) }

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() とかも呼び出せます。 @Composable fun CircleProgress( progress: Int, modifier: Modifier, colorProgress: Color = MaterialTheme.colors.primary, colorBackground: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f) .compositeOver(MaterialTheme.colors.surface), strokeWidth: Dp = 8.dp, ) { Canvas(modifier = modifier) { val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round) val diameter = min(size.width, size.height) - stroke.width val topLeft = Offset((size.width - diameter) / 2, (size.height - diameter) / 2) val circleSize = Size(diameter, diameter) drawArc( color = colorBackground, startAngle = -90f, sweepAngle = 360f, useCenter = false, style = stroke, topLeft = topLeft, size = circleSize, ) drawArc( color = colorProgress, startAngle = -90f, sweepAngle = 360f / 100 * progress, useCenter = false, style = stroke, topLeft = topLeft, size = circleSize, ) } } @Preview @Composable fun CircleProgressPreview() { var progress by remember { mutableStateOf(0) } val animateProgress by animateIntAsState(targetValue = progress, animationSpec = tween()) Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp) ) { Button(onClick = { progress = Random.nextInt(0, 101) }) { Text("Change Progress") } Spacer(modifier = Modifier.height(16.dp)) CircleProgress( progress = animateProgress, modifier = Modifier.size(120.dp) ) } } 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。 @Composable fun IconToggleButton( imageVector: ImageVector, contentDescription: String?, checked: Boolean, onCheckedChange: (Boolean) -> Unit, enabled: Boolean = true ) { CompositionLocalProvider( LocalContentColor provides contentColor(enabled = enabled, checked = checked), ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .toggleable( value = checked, onValueChange = onCheckedChange, role = Role.RadioButton, ) .size(48.dp) ) { Icon( imageVector = imageVector, contentDescription = contentDescription, modifier = Modifier.size(24.dp) ) } } }

枠線などの描画は Modifier.drawWithContent() でやったが、これが面倒だった〜(特にRTL対応)。 private fun Modifier.drawToggleButtonFrame( ... ): Modifier = this.drawWithContent { ... // draw checked border drawPath( path = ..., color = checkedBorderColor, style = Stroke(strokeWidth), ) drawContent() }

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

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



Modifier.triStateToggleable() を使って独自の3状態ボタンも作ることができます。 var state by remember { mutableStateOf(ToggleableState.On) } Box( contentAlignment = Alignment.Center, modifier = Modifier.triStateToggleable( state = state, onClick = { state = when (state) { ToggleableState.On -> ToggleableState.Off ToggleableState.Off -> ToggleableState.Indeterminate ToggleableState.Indeterminate -> ToggleableState.On } }, role = Role.Checkbox, interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple( bounded = false, radius = 24.dp ) ) ) { Icon( imageVector = when (state) { ToggleableState.On -> Icons.Default.Favorite ToggleableState.Off -> Icons.Default.FavoriteBorder ToggleableState.Indeterminate -> Icons.Default.FavoriteBorder }, contentDescription = when (state) { ToggleableState.On -> "favorite on" ToggleableState.Off -> "favorite on" ToggleableState.Indeterminate -> "favorite indeterminate" }, tint = when (state) { ToggleableState.On -> MaterialTheme.colors.primary ToggleableState.Off -> MaterialTheme.colors.primary ToggleableState.Indeterminate -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled) } ) }



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

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



2021年5月2日日曜日

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

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

よくない例 @Composable fun DogList(list: List<Dog>) { LazyColumn { items(list.size + 1) { if (it == 0) { Header() } else { DogListItem(list[it - 1]) } } } }

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

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



2021年5月1日土曜日

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

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

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

@Composable fun Capsule() { Text( text = "Android", modifier = Modifier .padding(16.dp) .background( color = Color.LightGray, shape = RoundedCornerShape(50) ) .padding(vertical = 8.dp, horizontal = 16.dp) ) }