2021年6月30日水曜日

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

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

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

* accompanist-insets がちゃんと動くために必要 class RelocationRequesterSampleActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { MaterialTheme { ProvideWindowInsets { RelocationRequesterSample() } } } } } 4. RelocationRequester を使う @OptIn(ExperimentalComposeUiApi::class) @Composable fun RelocationRequesterSample() { Column( modifier = Modifier .fillMaxSize() .statusBarsPadding() .navigationBarsWithImePadding() .verticalScroll(rememberScrollState()) .padding(24.dp) ) { Spacer( modifier = Modifier .fillMaxSize() .height(600.dp) .background(color = Color.LightGray) ) Spacer(modifier = Modifier.height(24.dp)) val relocationRequester = remember { RelocationRequester() } val interactionSource = remember { MutableInteractionSource() } val isFocused by interactionSource.collectIsFocusedAsState() val ime = LocalWindowInsets.current.ime if (ime.isVisible && !ime.animationInProgress && isFocused) { LaunchedEffect(Unit) { relocationRequester.bringIntoView() } } var value by remember { mutableStateOf("") } OutlinedTextField( value = value, onValueChange = { value = it }, interactionSource = interactionSource, modifier = Modifier.relocationRequester(relocationRequester) ) } }
ちなみに、 relocationRequester.bringIntoView() 部分をコメントアウトするとこうなる



参考


2021年6月28日月曜日

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

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

2021年6月21日月曜日

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

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

rememberLauncherForActivityResult() を使います。 enum class PermissionState { Checking, Granted, Denied, } @Composable private fun NeedPermissionScreen() { var state by remember { mutableStateOf(PermissionState.Checking) } val launcher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { state = if (it) PermissionState.Granted else PermissionState.Denied } val permission = Manifest.permission.CAMERA val context = LocalContext.current val lifecycleObserver = remember { LifecycleEventObserver { _, event -> if (event == Lifecycle.Event.ON_START) { val result = context.checkSelfPermission(permission) if (result != PackageManager.PERMISSION_GRANTED) { state = PermissionState.Checking launcher.launch(permission) } else { state = PermissionState.Granted } } } } val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle, lifecycleObserver) { lifecycle.addObserver(lifecycleObserver) onDispose { lifecycle.removeObserver(lifecycleObserver) } } when (state) { PermissionState.Checking -> { } PermissionState.Granted -> { // TODO パーミッションが必要な機能を使う画面 } PermissionState.Denied -> { // TODO 拒否された時の画面 } } }

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

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

2021年6月10日木曜日

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

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

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

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




2021年6月1日火曜日

Kotlin の sealed interface が必要になる例

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


ViewModel で持ってる LiveData を Activity で observe してるとする。 class MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) viewModel.state.observe(this) { when (it) { is State.Data -> { println(it.value) } State.Loading -> { } } } } } class MainViewModel : ViewModel() { private val _state = MutableLiveData<State>() val state: LiveData<State> get() = _state fun doSomething() { val state = _state.value if (state is State.Data) { state.mutableValue = Random.nextInt() } } } sealed class State { object Loading : State() data class Data(val id: String) : State() { // 変更できるのは MainViewModel からだけにしたいが、 // private にすると MainViewModel からも見えなくなる var mutableValue: Int = -1 val value: Int get() = mutableValue } } ↑ State.Data が持つ mutableValue は MainViewModel からのみ変更できるようにしたい。

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

ということで sealed interface を使います。 class MainViewModel : ViewModel() { private val _state = MutableLiveData<State>() val state: LiveData<State> get() = _state fun doSomething() { val state = _state.value if (state is DataImpl) { state.mutableValue = Random.nextInt() } } } sealed interface State object Loading : State sealed interface Data : State { val value: Int } private class DataImpl(val id: Int) : Data { // private class なので変更できるのは同じファイルからだけ var mutableValue: Int = id override val value: Int get() = mutableValue }