musicLineアプリ開発日記

作曲を誰でも楽しく簡単に♪

ナンバーピッカーで数字入力を良い感じに

数字入力をダイヤル式のナンバーピッカーに変えて使いやすくした話。

ナンバーピッカー


例えば

musicLineでは、拍子の指定にテキスト入力を使っていました。

テキスト入力

でも、テキスト入力だと

  • 範囲がわからない
  • キーボードが押しづらい
  • 今の値を削除する必要がある

と色々不満があります。
このテキスト入力をナンバーピッカーに変えることで不満を解消しました。

ナンバーピッカー

テキスト入力は自由度が高いことが利点ですが、入力できる範囲を明示的に示したい場合などはナンバーピッカーを使用することで使いやすくなります。


ナンバーピッカーの欠点

ナンバーピッカーは範囲がある時は有効だと書きましたが、範囲が広すぎる場合はスワイプする回数が増えるため使いづらくなります。
その場合はテキスト入力やスライダーを使用した方がいいでしょう。

musicLineでは曲のテンポを指定する方法を、スライダーとテキスト入力で併用しています。
スライダーで直感的に操作できますが、直接指定したい時はテキスト入力で行います。また、テンポは一般的には300までの範囲ですが、それ以上を超えたい特殊の場合はテキスト入力で指定できるようにしています。

スライダーとテキスト入力併用


実装

Android標準のNumberPickerをポップアップでダイアログ表示できるようにラッパークラスを作成しました。

developer.android.com

全コードを表示

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data/>

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center">

        <NumberPicker
            android:id="@+id/number_picker"
            android:layout_width="80dp"
            android:layout_height="150dp"
            android:layout_gravity="center"
            android:layout_margin="30dp"
            android:gravity="center" />
    </FrameLayout>
</layout>
class NumberPickerDialogFragment : BaseDialogFragment() {

    companion object {
        private const val BUNDLE_KEY_VALUE = "value"
        private const val BUNDLE_KEY_MIN = "min"
        private const val BUNDLE_KEY_MAX = "max"

        // 引数:初期値、最小値、最大値
        fun createInstance(value: Int, min: Int, max: Int)
        = NumberPickerDialogFragment().also { fragment ->
            fragment.arguments = Bundle().apply {
                putInt(BUNDLE_KEY_VALUE, value)
                putInt(BUNDLE_KEY_MIN, min)
                putInt(BUNDLE_KEY_MAX, max)
            }
        }
    }

    var onResult: (Int) -> Unit = {}
    private lateinit var binding: DialogNumberPickerBinding

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        binding = DataBindingUtil.inflate(
                             LayoutInflater.from(requireActivity()), 
                             R.layout.dialog_number_picker, null, false)

        binding.numberPicker.run {
            maxValue = requireArguments().getInt(BUNDLE_KEY_MAX)
            minValue = requireArguments().getInt(BUNDLE_KEY_MIN)
            value = requireArguments().getInt(BUNDLE_KEY_VALUE)

            wrapSelectorWheel = false // ループをOFFにする
        }

        return AlertDialog.Builder(requireActivity(), R.style.CustomSlimAlertDialog)
            .setView(binding.root).create()
    }

    override fun onPause() {
        super.onPause()

        // 実際、コールバックはsetResultListenerを使うべき
        onResult(binding.numberPicker.value)
    }
}
<resources>
    <style name="CustomSlimAlertDialog" parent="Theme.AppCompat.Light.Dialog.Alert">
        <item name="android:windowMinWidthMajor">0%</item>
        <item name="android:windowMinWidthMinor">0%</item>
        <item name="dialogCornerRadius">8dp</item>
    </style>
</resources>
    val numberPickerDialogFragment = NumberPickerDialogFragment
    .createInstance(4, 2, 8).apply {
        onResult = { number ->
            // ピックした数字を使う
        }
    }
    numberPickerDialogFragment.show(parentFragmentManager, "beat_picker")

ポイント

  • NumberPickerをレイアウトに貼り付け
<NumberPicker
    android:id="@+id/number_picker"
    android:layout_width="80dp"
    android:layout_height="150dp"
    ...
  • NumberPickerに範囲の最大・最小と初期値を設定
binding.numberPicker.run {
    maxValue = requireArguments().getInt(BUNDLE_KEY_MAX)
    minValue = requireArguments().getInt(BUNDLE_KEY_MIN)
    value = requireArguments().getInt(BUNDLE_KEY_VALUE)
    ...
  • ダイアログ閉じるときに数字を取得
onResult = { number ->
    // ピックした数字を使う
}


ちなみに

NumberPickerのdisplayedValuesを設定すると、選択肢に数字以外を含めることもできます。

表示を数字以外にする