【Jetpack Compose】TextField の フォーカス と IME 開閉 と カーソル位置

 

TextField を使うときのあれこれ、Jetpack Compose ではどう書くのか。

【Jetpack Compose】TextField の フォーカス と IME 開閉 と カーソル位置

 

キーボードの開閉

それらしいクラスやメソッドがあります。フォーカスが当たっていれば使えます。


val keyboardController = LocalSoftwareKeyboardController.current
keyboardController?.show()
keyboardController?.hide()

フォーカスを当てたり外したりすることのみでも、IMEを開閉できるので今回は無視します。

 

フォーカスを当てる

FocusRequester を使います。


val focusRequester = remember { FocusRequester() }

focusRequester を TextFiled に仕込みます。


TextField(
  modifier = Modifier.focusRequester(focusRequester)
)

それを Button クリックでフォーカスします。


Button(
  onClick = { focusRequester.requestFocus() }
) {
  Text("SHOW IME")
}

フォーカスと同時にIMEも開きます。

compose 時に当てたいときは、


LaunchedEffect(Unit) {
  focusRequester.requestFocus()
}

 

フォーカスを外す

同様に、FocusRequester でやれると思ったら、できません。

LocalFocusManager を使います。


val focusManager = LocalFocusManager.current

フォーカスを外してくれます。

同様に、ボタンに仕込みます。


Button(
  onClick = { focusManager.clearFocus() }
) {
  Text("HIDE IME")
}

これも、フォーカスを外すと同時にIMEが閉じます。

 

カーソルの位置

文字の入った TextField にフォーカスしてIMEが開いたときは、編集です。

文字の最後尾にカーソルがあったほうがいい気がします。

TextFieldValue を使います。


TextFieldValue(
  text = text,
  selection = TextRange(text.length)
)

selection がカーソルの位置です。

text の長さを数えて置きます。日本語でもいけます。

 

まとめ

以下で検証してみました。


@Composable
fun SampleScreen() {

  var text by remember { mutableStateOf("あいうえお") }

  val focusManager = LocalFocusManager.current
  val focusRequester = remember { FocusRequester() }

  Column(
    Modifier.fillMaxSize(),
    Arrangement.Center,
    Alignment.CenterHorizontally
  ) {

    Text(text)
    Spacer(Modifier.height(16.dp))
    Row {
      Button(
        onClick = { focusRequester.requestFocus() }
      ) {
        Text("SHOW IME")
      }
      Spacer(Modifier.width(24.dp))
      Button(
        onClick = { focusManager.clearFocus() }
      ) {
        Text("HIDE IME")
      }
    }
    Spacer(Modifier.height(16.dp))
    CustomTextField(
      text = text,
      focusRequester = focusRequester,
      onChange = { changed ->
        text = changed
      }
    )

  }
}

@Composable
fun CustomTextField(
  text: String,
  focusRequester: FocusRequester,
  onChange: (String) -> Unit
) {

  var textFieldValue by remember {
    mutableStateOf(
      TextFieldValue(
        text = text,
        selection = TextRange(text.length)
      )
    )
  }

  TextField(
    value = textFieldValue,
    onValueChange = { changed ->
      textFieldValue = changed
      onChange(changed.text)
    },
    modifier = Modifier.focusRequester(focusRequester)
  )

}

【Jetpack Compose】TextField の フォーカス と IME 開閉 と カーソル位置

Compose の SideEffect や coroutineScope など、非同期処理系は悩ましくなりそうです。


【Jetpack Compose】dp / px / sp の相互変換

dp と px と sp の相互変換。

Compose ではどうやるかという話。

【Jetpack Compose】dp / px / sp の相互変換
👉 How to convert Dp to pixels in Android Jetpack Compose? - Stack Overflow hatena-bookmark


import androidx.compose.ui.platform.LocalDensity

val pxValue = with(LocalDensity.current) { 16.dp.toPx() }

// or

val pxValue = LocalDensity.current.run { 16.dp.toPx() }

Kotlin スコープ系の関数で取得できる、というのだが気持ちが悪い。

これは、以下でも同じ。


val pxValue = 16.dp.value * LocalDensity.current.density

少し、型を表示しながら少し試してみる。


with(LocalDensity.current) {
  val dp1: Dp = 1.dp
  val dp1ToPx: Float = dp1.toPx()
  val dp1ToSp: TextUnit = dp1.toSp()
}


with(LocalDensity.current) {
  val sp1: TextUnit = 1.sp
  val sp1ToDp: Dp = sp1.toDp()
  val sp1ToPx: Float = sp1.toPx()
}

Compose 側で、dp/px/sp の値の単位を以下に揃えようとしてるような雰囲気に見える。


dp → Dp
px → Float
sp → TextUnit

 

拡張関数にしてみる

そもそも、意味的に以下のような決まりがありました。


px = sp * scale
px = dp * density
→ sp * scale = dp * density

なので、単位を、それぞれの内部の value: Float に揃えて変換すると、


@Composable
internal fun Float.dpValueToPxValue(): Float {
  return this * LocalDensity.current.density
}

@Composable
internal fun Float.dpValueToSpValue(): Float {
  return this * LocalDensity.current.density / LocalDensity.current.fontScale
}

@Composable
internal fun Float.pxValueToDpValue(): Float {
  return this / LocalDensity.current.density
}

@Composable
internal fun Float.pxValueToSpValue(): Float {
  return  this / LocalDensity.current.fontScale
}

@Composable
internal fun Float.spValueToDpValue(): Float {
  return this * LocalDensity.current.fontScale / LocalDensity.current.density
}

@Composable
internal fun Float.spValueToPxValue(): Float {
  return this * LocalDensity.current.fontScale
}

さらに、Compose の意向に添わせると、

どうなんですかね、ここらへん。


ビルド時間を計測・比較したいときの gradle(w) オプション

Terminal 開いてから。


./gradlew --profile --offline --rerun-tasks --max-workers=4 assembleDebug

👉 Compose におけるデベロッパーのエルゴノミクス  |  Jetpack Compose  |  Android Developers hatena-bookmark

結果がHTMLで出力されます。

Profile report Profiled build: assembleDebug

各オプションの意味については以下。

--profile
Generates a high-level performance report in the $buildDir/reports/profile directory. --scan is preferred.

--offline
Specifies that the build should operate without accessing network resources.

--rerun-tasks
You can force Gradle to execute all tasks ignoring up-to-date checks

--max-workers
Sets maximum number of workers that Gradle may use. Default is number of processors.

👉 Command-Line Interface hatena-bookmark

実行前にクリーンするのがいいそうです。


// On Mac or Linux, run the Gradle wrapper using "./gradlew".
gradlew clean

gradlew --profile --offline --rerun-tasks assembleFlavorDebug

👉 ビルドのプロファイリングを行う  |  Android デベロッパー  |  Android Developers hatena-bookmark


./gradlew clean && ./gradlew --profile --offline --rerun-tasks --max-workers=4 assembleDebug

10回やってみてどうなるか。

ある平均的なぶれのないビルド時間になりますかね。