2021年6月30日水曜日

Jetpack Compose : キーボードが表示されたときに scrollable な Column 内の TextField が見えるようにスクロールする

1. accompanist-insets を入れる
  1. implementation "com.google.accompanist:accompanist-insets:$accompanist_version"  
2. Activity に android:windowSoftInputMode="adjustResize" をセットする

* accompanist-insets がちゃんと動くために必要
  1. <activity  
  2.     android:name=".RelocationRequesterSampleActivity"  
  3.     android:windowSoftInputMode="adjustResize">  
3. Activity で WindowCompat.setDecorFitsSystemWindows(window, false) する

* accompanist-insets がちゃんと動くために必要
  1. class RelocationRequesterSampleActivity : ComponentActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.   
  6.         WindowCompat.setDecorFitsSystemWindows(window, false)  
  7.   
  8.         setContent {  
  9.             MaterialTheme {  
  10.                 ProvideWindowInsets {  
  11.                     RelocationRequesterSample()  
  12.                 }  
  13.             }  
  14.         }  
  15.     }  
  16. }  
4. RelocationRequester を使う
  1. @OptIn(ExperimentalComposeUiApi::class)  
  2. @Composable  
  3. fun RelocationRequesterSample() {  
  4.     Column(  
  5.         modifier = Modifier  
  6.             .fillMaxSize()  
  7.             .statusBarsPadding()  
  8.             .navigationBarsWithImePadding()  
  9.             .verticalScroll(rememberScrollState())  
  10.             .padding(24.dp)  
  11.     ) {  
  12.         Spacer(  
  13.             modifier = Modifier  
  14.                 .fillMaxSize()  
  15.                 .height(600.dp)  
  16.                 .background(color = Color.LightGray)  
  17.         )  
  18.   
  19.         Spacer(modifier = Modifier.height(24.dp))  
  20.   
  21.         val relocationRequester = remember { RelocationRequester() }  
  22.         val interactionSource = remember { MutableInteractionSource() }  
  23.         val isFocused by interactionSource.collectIsFocusedAsState()  
  24.   
  25.         val ime = LocalWindowInsets.current.ime  
  26.         if (ime.isVisible && !ime.animationInProgress && isFocused) {  
  27.             LaunchedEffect(Unit) {  
  28.                 relocationRequester.bringIntoView()  
  29.             }  
  30.         }  
  31.   
  32.         var value by remember { mutableStateOf("") }  
  33.   
  34.         OutlinedTextField(  
  35.             value = value,  
  36.             onValueChange = { value = it },  
  37.             interactionSource = interactionSource,  
  38.             modifier = Modifier.relocationRequester(relocationRequester)  
  39.         )  
  40.     }  
  41. }  
ちなみに、 relocationRequester.bringIntoView() 部分をコメントアウトするとこうなる



参考


2021年6月28日月曜日

Jetpack Compose: Text の文字サイズを dp で指定する

Text の文字サイズを dp で指定したい(fontScale に依存しないようにしたい)ときはこのようにします。
  1. val density = LocalDensity.current  
  2.   
  3. Text(  
  4.     text = "Hello",  
  5.     fontSize = with(density) { 18.dp.toSp() },  
  6. )  


2021年6月21日月曜日

Jetpack Compose で Runtime Permission をリクエストする

onStart() でパーミッションがあるかどうかチェックして、無い場合はパーミッションをリクエストする処理です。

rememberLauncherForActivityResult() を使います。
  1. enum class PermissionState {  
  2.     Checking,  
  3.     Granted,  
  4.     Denied,  
  5. }  
  6.   
  7. @Composable  
  8. private fun NeedPermissionScreen() {  
  9.     var state by remember { mutableStateOf(PermissionState.Checking) }  
  10.   
  11.     val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) {  
  12.         state = if (it) PermissionState.Granted else PermissionState.Denied  
  13.     }  
  14.   
  15.     val permission = Manifest.permission.CAMERA  
  16.   
  17.     val context = LocalContext.current  
  18.     val lifecycleObserver = remember {  
  19.         LifecycleEventObserver { _, event ->  
  20.             if (event == Lifecycle.Event.ON_START) {  
  21.                 val result = context.checkSelfPermission(permission)  
  22.                 if (result != PackageManager.PERMISSION_GRANTED) {  
  23.                     state = PermissionState.Checking  
  24.                     launcher.launch(permission)  
  25.                 } else {  
  26.                     state = PermissionState.Granted  
  27.                 }  
  28.             }  
  29.         }  
  30.     }  
  31.     val lifecycle = LocalLifecycleOwner.current.lifecycle  
  32.     DisposableEffect(lifecycle, lifecycleObserver) {  
  33.         lifecycle.addObserver(lifecycleObserver)  
  34.         onDispose {  
  35.             lifecycle.removeObserver(lifecycleObserver)  
  36.         }  
  37.     }  
  38.   
  39.     when (state) {  
  40.         PermissionState.Checking -> {  
  41.         }  
  42.         PermissionState.Granted -> {  
  43.             // TODO パーミッションが必要な機能を使う画面  
  44.         }  
  45.         PermissionState.Denied -> {  
  46.             // TODO 拒否された時の画面  
  47.         }  
  48.     }  
  49. }  


追記:
Accompanist に最近 Permission のやつが追加されました (現状 Permission 関係のすべての機能は experimental)

https://google.github.io/accompanist/permissions/

2021年6月10日木曜日

Compose の Text を長押しで文字選択できるようにしたい

SelectionContainer を使います。
  1. SelectionContainer {  
  2.     Text("This text is selectable")  
  3. }  
選択ハンドルなどの色は MaterialTheme.colors.primary が使われます。

残念ながら SelectionContainer には直接色を指定するパラメータは用意されていません。

ピンポイントで色を指定したいなら MaterialTheme を使って primary を上書きします。
  1. val original = MaterialTheme.colors  
  2. val textColor = original.primary  
  3.   
  4. MaterialTheme(colors = original.copy(primary = original.secondary)) {  
  5.     SelectionContainer {  
  6.         Text("This text is selectable", color = textColor)  
  7.     }  
  8. }  




2021年6月1日火曜日

Kotlin の sealed interface が必要になる例

(本当は Compose のコード例(State/MutableState)にしたかったんだけど、まだ Jetpack Compose は Kotlin 1.5 に対応してないので sealed interface 使えないんだよね...)


ViewModel で持ってる LiveData を Activity で observe してるとする。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     private val viewModel: MainViewModel by viewModels()  
  4.   
  5.     override fun onCreate(savedInstanceState: Bundle?) {  
  6.         super.onCreate(savedInstanceState)  
  7.         setContentView(R.layout.activity_main)  
  8.   
  9.         viewModel.state.observe(this) {  
  10.             when (it) {  
  11.                 is State.Data -> {  
  12.                     println(it.value)  
  13.                 }  
  14.                 State.Loading -> {  
  15.   
  16.                 }  
  17.             }  
  18.         }  
  19.     }  
  20. }  
  1. class MainViewModel : ViewModel() {  
  2.   
  3.     private val _state = MutableLiveData<State>()  
  4.     val state: LiveData<State>  
  5.         get() = _state  
  6.   
  7.     fun doSomething() {  
  8.         val state = _state.value  
  9.         if (state is State.Data) {  
  10.             state.mutableValue = Random.nextInt()  
  11.         }  
  12.     }  
  13. }  
  14.   
  15. sealed class State {  
  16.     object Loading : State()  
  17.   
  18.     data class Data(val id: String) : State() {  
  19.         // 変更できるのは MainViewModel からだけにしたいが、  
  20.         // private にすると MainViewModel からも見えなくなる  
  21.         var mutableValue: Int = -1  
  22.   
  23.         val value: Int  
  24.             get() = mutableValue  
  25.     }  
  26. }  
↑ State.Data が持つ mutableValue は MainViewModel からのみ変更できるようにしたい。

mutableValue に private は MainViewModel から見えなくなるのでダメ。
  1. private var mutableValue: Int = -1  
これもダメ。
  1. private sealed class State {  
  1. private data class Data(val id: String) : State() {  
Data を top level にすると private をつけても MainViewModel から見えるけど、MainActivity から見えなくなるのでダメ。
  1. sealed class State  
  2.   
  3. object Loading : State()  
  4.   
  5. // MainViewModel から mutableValue は見えるが  
  6. // Data が MainActivity からは見えなくなる  
  7. private data class Data(val id: String) : State() {  
  8.     var mutableValue: Int = -1  
  9.   
  10.     val value: Int  
  11.         get() = mutableValue  
  12. }  


ということで sealed interface を使います。
  1. class MainViewModel : ViewModel() {  
  2.   
  3.     private val _state = MutableLiveData<State>()  
  4.     val state: LiveData<State>  
  5.         get() = _state  
  6.   
  7.     fun doSomething() {  
  8.         val state = _state.value  
  9.         if (state is DataImpl) {  
  10.             state.mutableValue = Random.nextInt()  
  11.         }  
  12.     }  
  13. }  
  14.   
  15. sealed interface State  
  16.   
  17. object Loading : State  
  18.   
  19. sealed interface Data : State {  
  20.     val value: Int  
  21. }  
  22.   
  23. private class DataImpl(val id: Int) : Data {  
  24.     // private class なので変更できるのは同じファイルからだけ  
  25.     var mutableValue: Int = id  
  26.   
  27.     override val value: Int  
  28.         get() = mutableValue  
  29. }