田辺  明美

田辺 明美

1677754504

Jetpack Compose でタブ レイアウトを作成する方法

このチュートリアルでは、Jetpack Compose でタブ レイアウトを作成する方法を学習します。簡単なタブの作成方法を学びます。スワイプを有効にしてタブを作成する方法を学ぶ

私たちは皆それをやった。

複雑なアプリケーションでコンテンツを整理するための古き良きタブのようなものはありません。では、Jetpack Compose でタブ レイアウトを作成するにはどうすればよいでしょうか?

このチュートリアルでは、すべての基本について説明しますが、より高度な内容もいくつか示します。

シンプルなタブの作成方法

タブ レイアウトを作成するには、TabRowから始める必要があります。これは、タブを保持するコンテナ要素になります。

@Composable
@UiComposable
fun TabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    indicator: @Composable @UiComposable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable @UiComposable () -> Unit = @Composable {
            TabRowDefaults.Divider()
        },
    tabs: @Composable @UiComposable () -> Unit
): Unit
  • selectedTabIndex は、現在選択されているタブのインデックスを示します
  • インジケータは、現在どのタブが選択されているかを示す UI を表します
  • ディバイダは、インディケータの下の TabRow の下部に描画されるコンポーザブルです
  • タブのスタイルをカスタマイズする必要がない場合は、 TabRow に使用されるデフォルト値と実装が含まれているため、 TabRowDefaults を 使用できます(ディバイダー内で使用されていることがわかります)。

例で TabRow の使用法を見てみましょう。3 つのタブを持つ単純なレイアウトを作成します。

  1. だいたい
  2. 設定
@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings")

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
        }
    }
}

注意すべき点がいくつかあります。

  • TabRow コンポーザブルは、それ自体の内部にタブ コンポーザブルを保持します
  • TabRow コンポーザブルの後に、各タブがクリックされたときに何が起こるかを処理する when 句があります (特定のケースでは、異なる画面を開いています)。
  • どのタブが選択されているかを追跡するために、tabIndex という変数を使用しています。

初め

かなり当たり障りのないものですよね?

Tab コンポーザブルの icon 属性を使用して、アイコンでスパイスを効かせましょう。

@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings")

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                        }
                    }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
        }
    }
}

1-1

見栄えは良くなりましたが、疑問が生じます: 画面に表示できるよりも多くのタブがある場合はどうなるでしょうか?

幸いなことに、答えは簡単です。

TabRow をスクロール可能にするオプションがあります。TabRow 要素を使用する代わりに、ScrollableTabRow コンポーザブルを使用できます。

@Composable
@UiComposable
fun ScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
    indicator: @Composable @UiComposable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable @UiComposable () -> Unit = @Composable {
            TabRowDefaults.Divider()
        },
    tabs: @Composable @UiComposable () -> Unit
): Unit

したがって、上記の例を変換すると、次のようになります。

@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings", "More", "Something", "Everything")

    Column(modifier = Modifier.fillMaxWidth()) {
        ScrollableTabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                            3 -> Icon(imageVector = Icons.Default.Lock, contentDescription = null)
                            4 -> Icon(imageVector = Icons.Default.HeartBroken, contentDescription = null)
                            5 -> Icon(imageVector = Icons.Default.Star, contentDescription = null)
                        }
                    }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
            3 -> MoreScreen()
            4 -> SomethingScreen()
            5 -> EverythingScreen()
        }
    }
}

2

スワイプを有効にしてタブを作成する方法

スクロール可能なタブは便利ですが、タブ間のスワイプはさらに優れています。ほとんどのユーザーは、各タブをクリックするよりも、タブ間をスワイプする方が直感的だと感じるでしょう。ドキュメントを見ると、いくつかのオプションがあることがわかります。

  1. スワイプ可能な修飾子
  2. detectDragGesturesモディファイ
  3. ドラッグ可能な修飾子

これらのすべてが目標を達成するのに役立つわけではなく、それぞれに独自の理由があります。自分で何かをする「面倒」を経験したくない場合は、使用できるページャーと呼ばれる Accompanist のライブラリがあります。スワイプに反応する行/列を水平または垂直に作成する機能を追加できます。

実装する手順は既に説明されており、以下のリソースを使用してその方法を学習できます。

あなたが私のようで、自分のために何かをするのが好きで、手を汚すのが好きなら、読み進めてください。

オプション 1:Swipeableモディファイア

スワイプ可能な修飾子について最初に知っておくべきことは、 @ ExperimentalMaterialApiで注釈が付けられていることです。これは、この API が Jetpack Compose のバージョン間で変更される可能性があり、安定していないことを意味します。

それとは別に、スワイプ可能な修飾子が使用するメカニズムを調べる必要があります。3 つのビルディング ブロックがあります。

  1. スワイプ可能な状態 – 現在の状態を示し、進行中のスワイプまたはスワイプ関連のアニメーションに関するデータを保持します。
  2. Anchors – スワイプ アクションを最小値から最大値に制限する値のマップ (Float ベース)。アンカー ポイントをスワイプ可能な状態にマップします。
  3. しきい値 – 2 つの既知のアンカー間の差を示す値。
@ExperimentalMaterialApi
fun <T : Any?> Modifier.swipeable(
    state: SwipeableState<T>,
    anchors: Map<Float, T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    thresholds: (from, to) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
    velocityThreshold: Dp = VelocityThreshold
): Modifier

この API は実験的なものですが、私たちが探しているスワイプ ジェスチャに使用するためのものではありません。

あなたはできる。この修飾子は、ユーザーがオン/オフの位置の間でドラッグできるスイッチ ボタンに使用します (例として)。しかし、この例のアンカーは何でしょうか? しきい値をどのように定義しますか? ユーザーが実行するスワイプは、2 点間で制限することはできません。したがって、これは手放して、detectDragGestures に進みます。

オプション 2:detectDragGesturesモディファイア

名前が示すように、この修飾子はドラッグ ジェスチャを検出します。これは、スワイプによく似ています。

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit

ご覧のとおり、onDrag コールバックには 2 つの引数があります。

  1. changePointerInputChangeタイプの、ドラッグ時のポインターの変化を示す
  2. dragAmountOffsetx、y 値でドラッグされた量を示す型

このコールバックは、次の場合に呼び出されます。

「… ポインタが下に来るのを待って、任意の方向でタッチストップしてから、onDrag各ドラッグイベントを呼び出します。」

ドラッグ可能な修飾子の代わりにこの修飾子を使用する利点は、x 座標と y 座標の両方の変更に関する情報を提供することです。

それの欠点は、スワイプのためのスムーズでエレガントなソリューションを提供しないことです. これは、onDrag コールバックがトリガーされる回数が原因です。

ユーザーがスワイプ ジェスチャを実行すると、onDrag コールバックが複数回トリガーされます。これにより、いつ「ドラッグ」ジェスチャが完全に終了したかを判別するのが難しくなります。

これを試してみると、スワイプ ジェスチャごとに onDrag コールバックが 3 回トリガーされることがわかりました。これは私たちのユースケースには適していないので、ドラッグ可能な修飾子を調べてみましょう。

オプション 3:Draggableモディファイア

このモディファイヤは、前のモディファイヤの簡素化されたバージョンと考えてください。これは、ユーザーが 1 つの方向 (垂直/水平) でのみドラッグ ジェスチャを実行したときの UI の変化を測定します。水平方向のスワイプのみを対象としているため、これは適切なオプションです。

fun Modifier.draggable(
    state: DraggableState,
    orientation: Orientation,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = false,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
    reverseDirection: Boolean = false
): Modifier

ここでも、他の 2 つの修飾子との類似点はありません。注意すべき点を指摘します。

  • state– スワイプ可能な修飾子の状態と同様に、ここでのみドラッグ モーションについて説明します。
  • onDragStarted– ドラッグ モーションが開始されたときにトリガーされるコールバック。
  • onDragStopped– ドラッグ モーションが終了したときにトリガーされるコールバック。

とは異なりdetectDragGestures、ここではonDragStoppedすべてのスワイプ ジェスチャに対して 1 回呼び出されるため、この修飾子が最適な候補になります。

この例のスワイプジェスチャ検出器としての実装は非常に堅牢であるため、いくつかの前提条件から始めましょう。

  1. ビューモデルクラスで現在表示されているタブのインデックスを保存します
  2. このインデックスは、MutableLiveData値が変更されたときにコンポーザブルを再構成できるようにするためのものです。
  3. 各画面は、draggable修飾子をそのレイアウトに追加します
  4. メソッドを使用するため、runtime-livedata ライブラリを追加する必要がありますobserveAsState

#4 から始めます。

アプリケーションの build.gradle ファイルに移動し、次の依存関係を追加します。

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

は、$compose_version使用している Jetpack Compose のバージョンです。

また、ソリューションはどちらの場合でも機能し、余分なボイラー プレートを作成する必要がないため、前の例を 6 つではなく 3 つの画面を保持するように最小化しました。

以下はビューモデルです。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val _tabIndex: MutableLiveData<Int> = MutableLiveData(0)
    val tabIndex: LiveData<Int> = _tabIndex
    val tabs = listOf("Home", "About", "Settings")

    fun updateTabIndexBasedOnSwipe(isSwipeToTheLeft: Boolean) {
        _tabIndex.value = when (isSwipeToTheLeft) {
            true -> Math.floorMod(_tabIndex.value!!.plus(1), tabs.size)
            false -> Math.floorMod(_tabIndex.value!!.minus(1), tabs.size)
        }
    }

    fun updateTabIndex(i: Int) {
        _tabIndex.value = i
    }

}
  • tabIndex 現在選択されているインデックスの保持を担当します。
  • index 公開された tabIndex です。
  • tabs タブ名のリストです。
  • このメソッドは、updateTabIndexBasedOnSwipe スワイプが発生したときにトリガーされ、tabIndex の移動先の計算を実行します。

各画面は同じレイアウトで構成されています。

@Composable
fun AboutScreen(viewModel: MainViewModel) {

    var isSwipeToTheLeft by remember { mutableStateOf(false) }
    val dragState = rememberDraggableState(onDelta = { delta ->
        isSwipeToTheLeft = delta > 0
    })

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = dragState,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe(isSwipeToTheLeft = isSwipeToTheLeft)
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = "About",
                textAlign = TextAlign.Center,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}
  • isSwipeToTheLeft スワイプの方向を示すブール値です。
  • dragState 実行中のドラッグの状態を保持し、デルタに従って isSwipeToTheLeft を更新します。
  • コールバックonDragStopped が呼び出されると、公開されたビューモデル メソッド updateTabIndexBasedOnSwipe が呼び出されます。

そして最後に、私たちのTabLayout:

@Composable
fun TabLayout(viewModel: MainViewModel) {
    val tabIndex = viewModel.tabIndex.observeAsState()
    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex.value!!) {
            viewModel.tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex.value!! == index,
                    onClick = { viewModel.updateTabIndex(index) },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                        }
                    }
                )
            }
        }

        when (tabIndex.value) {
            0 -> HomeScreen(viewModel = viewModel)
            1 -> AboutScreen(viewModel = viewModel)
            2 -> SettingsScreen(viewModel = viewModel)
        }
    }
}
  • タブが選択されると、 with で現在選択されているタブが更新されることに注意してviewModelくださいupdateTabIndex。

すべてをまとめると、次のようになります。

2-1

私たちが達成したことについて一言。お気付きかもしれませんが、画面ごとにボイラープレートを追加しているため、繰り返しが発生します。各画面はドラッグの状態を保存しています。

draggableStateそれを改善するために、次のようにビュー モデルに移動できます。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val _tabIndex: MutableLiveData<Int> = MutableLiveData(0)
    val tabIndex: LiveData<Int> = _tabIndex
    val tabs = listOf("Home", "About", "Settings")

    var isSwipeToTheLeft: Boolean = false
    private val draggableState = DraggableState { delta ->
        isSwipeToTheLeft= delta > 0
    }

    private val _dragState = MutableLiveData<DraggableState>(draggableState)
    val dragState: LiveData<DraggableState> = _dragState

    fun updateTabIndexBasedOnSwipe() {
        _tabIndex.value = when (isSwipeToTheLeft) {
            true -> Math.floorMod(_tabIndex.value!!.plus(1), tabs.size)
            false -> Math.floorMod(_tabIndex.value!!.minus(1), tabs.size)
        }
    }

    fun updateTabIndex(i: Int) {
        _tabIndex.value = i
    }

}

各画面が次のようになるため、定型文が少し減ります。

@Composable
fun AboutScreen(viewModel: MainViewModel) {

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = viewModel.dragState.value!!,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe()
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = "About",
                textAlign = TextAlign.Center,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}

この記事が、Jetpack Compose で独自のタブ UI を作成するために必要なツールを提供したことを願っています。

上記の例は、ここにあります。

また、私が書いた他の記事を読みたい場合は、こちらからチェックできます。

参考文献:

ソース: https://www.freecodecamp.org

#jetpack

What is GEEK

Buddha Community

Jetpack Compose でタブ レイアウトを作成する方法
Autumn  Blick

Autumn Blick

1593253920

Jetpack Compose: Twitter UI

He used Flutter (which is an amazing tool btw for building cross platform apps) — with just one day of work, 1500 lines of code. That’s beyond impressive (specially the fact that Flutter can be hosted on CodePen as well).

So with similar constraints, I wanted to try out Jetpack Compose. I followed the CodePen example (as closely as I could) and this is the result:

Complete source code:

The App

There are three screens in this app

Home Screen

Profile Screen

Compose Screen

App State

Before we get to the screens — take a look at app state model, which will be used for navigation and theming. I also added some helpers for navigating to individual screens & for checking theme.

Models

There are two models — both data classes. The Tweet model is annotated with _@Model _as we update this model from our composed functions to update view state. User stays the same, hence it’s not annotated.

#kotlin #android #jetpack-compose #android-app-development #jetpack

Jeromy  Lowe

Jeromy Lowe

1598743860

JetPack Compose 🏹 — State Management

State Management in Android is a complex concept and to know the reason you have to first understand the architectural design of Android and to learn why it’s the key requirement to manage state. The Marcelo Benites article Managing State in Android defines the best description of the state:

The state is an object that is connected/subscribed to one or more widgets, contains data, and eager to update the widgets from that data. If there’s any change happens in data, it notifies all widgets to whom it’s connected. The values of the state are changed at runtime.

#jetpack-compose #jetpack #state #android #kotlin

田辺  明美

田辺 明美

1677754504

Jetpack Compose でタブ レイアウトを作成する方法

このチュートリアルでは、Jetpack Compose でタブ レイアウトを作成する方法を学習します。簡単なタブの作成方法を学びます。スワイプを有効にしてタブを作成する方法を学ぶ

私たちは皆それをやった。

複雑なアプリケーションでコンテンツを整理するための古き良きタブのようなものはありません。では、Jetpack Compose でタブ レイアウトを作成するにはどうすればよいでしょうか?

このチュートリアルでは、すべての基本について説明しますが、より高度な内容もいくつか示します。

シンプルなタブの作成方法

タブ レイアウトを作成するには、TabRowから始める必要があります。これは、タブを保持するコンテナ要素になります。

@Composable
@UiComposable
fun TabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    indicator: @Composable @UiComposable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable @UiComposable () -> Unit = @Composable {
            TabRowDefaults.Divider()
        },
    tabs: @Composable @UiComposable () -> Unit
): Unit
  • selectedTabIndex は、現在選択されているタブのインデックスを示します
  • インジケータは、現在どのタブが選択されているかを示す UI を表します
  • ディバイダは、インディケータの下の TabRow の下部に描画されるコンポーザブルです
  • タブのスタイルをカスタマイズする必要がない場合は、 TabRow に使用されるデフォルト値と実装が含まれているため、 TabRowDefaults を 使用できます(ディバイダー内で使用されていることがわかります)。

例で TabRow の使用法を見てみましょう。3 つのタブを持つ単純なレイアウトを作成します。

  1. だいたい
  2. 設定
@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings")

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
        }
    }
}

注意すべき点がいくつかあります。

  • TabRow コンポーザブルは、それ自体の内部にタブ コンポーザブルを保持します
  • TabRow コンポーザブルの後に、各タブがクリックされたときに何が起こるかを処理する when 句があります (特定のケースでは、異なる画面を開いています)。
  • どのタブが選択されているかを追跡するために、tabIndex という変数を使用しています。

初め

かなり当たり障りのないものですよね?

Tab コンポーザブルの icon 属性を使用して、アイコンでスパイスを効かせましょう。

@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings")

    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                        }
                    }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
        }
    }
}

1-1

見栄えは良くなりましたが、疑問が生じます: 画面に表示できるよりも多くのタブがある場合はどうなるでしょうか?

幸いなことに、答えは簡単です。

TabRow をスクロール可能にするオプションがあります。TabRow 要素を使用する代わりに、ScrollableTabRow コンポーザブルを使用できます。

@Composable
@UiComposable
fun ScrollableTabRow(
    selectedTabIndex: Int,
    modifier: Modifier = Modifier,
    backgroundColor: Color = MaterialTheme.colors.primarySurface,
    contentColor: Color = contentColorFor(backgroundColor),
    edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
    indicator: @Composable @UiComposable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
            TabRowDefaults.Indicator(
                Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
            )
        },
    divider: @Composable @UiComposable () -> Unit = @Composable {
            TabRowDefaults.Divider()
        },
    tabs: @Composable @UiComposable () -> Unit
): Unit

したがって、上記の例を変換すると、次のようになります。

@Composable
fun TabScreen() {
    var tabIndex by remember { mutableStateOf(0) }

    val tabs = listOf("Home", "About", "Settings", "More", "Something", "Everything")

    Column(modifier = Modifier.fillMaxWidth()) {
        ScrollableTabRow(selectedTabIndex = tabIndex) {
            tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex == index,
                    onClick = { tabIndex = index },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                            3 -> Icon(imageVector = Icons.Default.Lock, contentDescription = null)
                            4 -> Icon(imageVector = Icons.Default.HeartBroken, contentDescription = null)
                            5 -> Icon(imageVector = Icons.Default.Star, contentDescription = null)
                        }
                    }
                )
            }
        }
        when (tabIndex) {
            0 -> HomeScreen()
            1 -> AboutScreen()
            2 -> SettingsScreen()
            3 -> MoreScreen()
            4 -> SomethingScreen()
            5 -> EverythingScreen()
        }
    }
}

2

スワイプを有効にしてタブを作成する方法

スクロール可能なタブは便利ですが、タブ間のスワイプはさらに優れています。ほとんどのユーザーは、各タブをクリックするよりも、タブ間をスワイプする方が直感的だと感じるでしょう。ドキュメントを見ると、いくつかのオプションがあることがわかります。

  1. スワイプ可能な修飾子
  2. detectDragGesturesモディファイ
  3. ドラッグ可能な修飾子

これらのすべてが目標を達成するのに役立つわけではなく、それぞれに独自の理由があります。自分で何かをする「面倒」を経験したくない場合は、使用できるページャーと呼ばれる Accompanist のライブラリがあります。スワイプに反応する行/列を水平または垂直に作成する機能を追加できます。

実装する手順は既に説明されており、以下のリソースを使用してその方法を学習できます。

あなたが私のようで、自分のために何かをするのが好きで、手を汚すのが好きなら、読み進めてください。

オプション 1:Swipeableモディファイア

スワイプ可能な修飾子について最初に知っておくべきことは、 @ ExperimentalMaterialApiで注釈が付けられていることです。これは、この API が Jetpack Compose のバージョン間で変更される可能性があり、安定していないことを意味します。

それとは別に、スワイプ可能な修飾子が使用するメカニズムを調べる必要があります。3 つのビルディング ブロックがあります。

  1. スワイプ可能な状態 – 現在の状態を示し、進行中のスワイプまたはスワイプ関連のアニメーションに関するデータを保持します。
  2. Anchors – スワイプ アクションを最小値から最大値に制限する値のマップ (Float ベース)。アンカー ポイントをスワイプ可能な状態にマップします。
  3. しきい値 – 2 つの既知のアンカー間の差を示す値。
@ExperimentalMaterialApi
fun <T : Any?> Modifier.swipeable(
    state: SwipeableState<T>,
    anchors: Map<Float, T>,
    orientation: Orientation,
    enabled: Boolean = true,
    reverseDirection: Boolean = false,
    interactionSource: MutableInteractionSource? = null,
    thresholds: (from, to) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) },
    resistance: ResistanceConfig? = resistanceConfig(anchors.keys),
    velocityThreshold: Dp = VelocityThreshold
): Modifier

この API は実験的なものですが、私たちが探しているスワイプ ジェスチャに使用するためのものではありません。

あなたはできる。この修飾子は、ユーザーがオン/オフの位置の間でドラッグできるスイッチ ボタンに使用します (例として)。しかし、この例のアンカーは何でしょうか? しきい値をどのように定義しますか? ユーザーが実行するスワイプは、2 点間で制限することはできません。したがって、これは手放して、detectDragGestures に進みます。

オプション 2:detectDragGesturesモディファイア

名前が示すように、この修飾子はドラッグ ジェスチャを検出します。これは、スワイプによく似ています。

suspend fun PointerInputScope.detectDragGestures(
    onDragStart: (Offset) -> Unit = { },
    onDragEnd: () -> Unit = { },
    onDragCancel: () -> Unit = { },
    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit

ご覧のとおり、onDrag コールバックには 2 つの引数があります。

  1. changePointerInputChangeタイプの、ドラッグ時のポインターの変化を示す
  2. dragAmountOffsetx、y 値でドラッグされた量を示す型

このコールバックは、次の場合に呼び出されます。

「… ポインタが下に来るのを待って、任意の方向でタッチストップしてから、onDrag各ドラッグイベントを呼び出します。」

ドラッグ可能な修飾子の代わりにこの修飾子を使用する利点は、x 座標と y 座標の両方の変更に関する情報を提供することです。

それの欠点は、スワイプのためのスムーズでエレガントなソリューションを提供しないことです. これは、onDrag コールバックがトリガーされる回数が原因です。

ユーザーがスワイプ ジェスチャを実行すると、onDrag コールバックが複数回トリガーされます。これにより、いつ「ドラッグ」ジェスチャが完全に終了したかを判別するのが難しくなります。

これを試してみると、スワイプ ジェスチャごとに onDrag コールバックが 3 回トリガーされることがわかりました。これは私たちのユースケースには適していないので、ドラッグ可能な修飾子を調べてみましょう。

オプション 3:Draggableモディファイア

このモディファイヤは、前のモディファイヤの簡素化されたバージョンと考えてください。これは、ユーザーが 1 つの方向 (垂直/水平) でのみドラッグ ジェスチャを実行したときの UI の変化を測定します。水平方向のスワイプのみを対象としているため、これは適切なオプションです。

fun Modifier.draggable(
    state: DraggableState,
    orientation: Orientation,
    enabled: Boolean = true,
    interactionSource: MutableInteractionSource? = null,
    startDragImmediately: Boolean = false,
    onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {},
    onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {},
    reverseDirection: Boolean = false
): Modifier

ここでも、他の 2 つの修飾子との類似点はありません。注意すべき点を指摘します。

  • state– スワイプ可能な修飾子の状態と同様に、ここでのみドラッグ モーションについて説明します。
  • onDragStarted– ドラッグ モーションが開始されたときにトリガーされるコールバック。
  • onDragStopped– ドラッグ モーションが終了したときにトリガーされるコールバック。

とは異なりdetectDragGestures、ここではonDragStoppedすべてのスワイプ ジェスチャに対して 1 回呼び出されるため、この修飾子が最適な候補になります。

この例のスワイプジェスチャ検出器としての実装は非常に堅牢であるため、いくつかの前提条件から始めましょう。

  1. ビューモデルクラスで現在表示されているタブのインデックスを保存します
  2. このインデックスは、MutableLiveData値が変更されたときにコンポーザブルを再構成できるようにするためのものです。
  3. 各画面は、draggable修飾子をそのレイアウトに追加します
  4. メソッドを使用するため、runtime-livedata ライブラリを追加する必要がありますobserveAsState

#4 から始めます。

アプリケーションの build.gradle ファイルに移動し、次の依存関係を追加します。

implementation "androidx.compose.runtime:runtime-livedata:$compose_version"

は、$compose_version使用している Jetpack Compose のバージョンです。

また、ソリューションはどちらの場合でも機能し、余分なボイラー プレートを作成する必要がないため、前の例を 6 つではなく 3 つの画面を保持するように最小化しました。

以下はビューモデルです。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val _tabIndex: MutableLiveData<Int> = MutableLiveData(0)
    val tabIndex: LiveData<Int> = _tabIndex
    val tabs = listOf("Home", "About", "Settings")

    fun updateTabIndexBasedOnSwipe(isSwipeToTheLeft: Boolean) {
        _tabIndex.value = when (isSwipeToTheLeft) {
            true -> Math.floorMod(_tabIndex.value!!.plus(1), tabs.size)
            false -> Math.floorMod(_tabIndex.value!!.minus(1), tabs.size)
        }
    }

    fun updateTabIndex(i: Int) {
        _tabIndex.value = i
    }

}
  • tabIndex 現在選択されているインデックスの保持を担当します。
  • index 公開された tabIndex です。
  • tabs タブ名のリストです。
  • このメソッドは、updateTabIndexBasedOnSwipe スワイプが発生したときにトリガーされ、tabIndex の移動先の計算を実行します。

各画面は同じレイアウトで構成されています。

@Composable
fun AboutScreen(viewModel: MainViewModel) {

    var isSwipeToTheLeft by remember { mutableStateOf(false) }
    val dragState = rememberDraggableState(onDelta = { delta ->
        isSwipeToTheLeft = delta > 0
    })

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = dragState,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe(isSwipeToTheLeft = isSwipeToTheLeft)
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = "About",
                textAlign = TextAlign.Center,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}
  • isSwipeToTheLeft スワイプの方向を示すブール値です。
  • dragState 実行中のドラッグの状態を保持し、デルタに従って isSwipeToTheLeft を更新します。
  • コールバックonDragStopped が呼び出されると、公開されたビューモデル メソッド updateTabIndexBasedOnSwipe が呼び出されます。

そして最後に、私たちのTabLayout:

@Composable
fun TabLayout(viewModel: MainViewModel) {
    val tabIndex = viewModel.tabIndex.observeAsState()
    Column(modifier = Modifier.fillMaxWidth()) {
        TabRow(selectedTabIndex = tabIndex.value!!) {
            viewModel.tabs.forEachIndexed { index, title ->
                Tab(text = { Text(title) },
                    selected = tabIndex.value!! == index,
                    onClick = { viewModel.updateTabIndex(index) },
                    icon = {
                        when (index) {
                            0 -> Icon(imageVector = Icons.Default.Home, contentDescription = null)
                            1 -> Icon(imageVector = Icons.Default.Info, contentDescription = null)
                            2 -> Icon(imageVector = Icons.Default.Settings, contentDescription = null)
                        }
                    }
                )
            }
        }

        when (tabIndex.value) {
            0 -> HomeScreen(viewModel = viewModel)
            1 -> AboutScreen(viewModel = viewModel)
            2 -> SettingsScreen(viewModel = viewModel)
        }
    }
}
  • タブが選択されると、 with で現在選択されているタブが更新されることに注意してviewModelくださいupdateTabIndex。

すべてをまとめると、次のようになります。

2-1

私たちが達成したことについて一言。お気付きかもしれませんが、画面ごとにボイラープレートを追加しているため、繰り返しが発生します。各画面はドラッグの状態を保存しています。

draggableStateそれを改善するために、次のようにビュー モデルに移動できます。

class MainViewModel(application: Application) : AndroidViewModel(application) {

    private val _tabIndex: MutableLiveData<Int> = MutableLiveData(0)
    val tabIndex: LiveData<Int> = _tabIndex
    val tabs = listOf("Home", "About", "Settings")

    var isSwipeToTheLeft: Boolean = false
    private val draggableState = DraggableState { delta ->
        isSwipeToTheLeft= delta > 0
    }

    private val _dragState = MutableLiveData<DraggableState>(draggableState)
    val dragState: LiveData<DraggableState> = _dragState

    fun updateTabIndexBasedOnSwipe() {
        _tabIndex.value = when (isSwipeToTheLeft) {
            true -> Math.floorMod(_tabIndex.value!!.plus(1), tabs.size)
            false -> Math.floorMod(_tabIndex.value!!.minus(1), tabs.size)
        }
    }

    fun updateTabIndex(i: Int) {
        _tabIndex.value = i
    }

}

各画面が次のようになるため、定型文が少し減ります。

@Composable
fun AboutScreen(viewModel: MainViewModel) {

    Column(modifier = Modifier.fillMaxSize().draggable(
        state = viewModel.dragState.value!!,
        orientation = Orientation.Horizontal,
        onDragStarted = {  },
        onDragStopped = {
            viewModel.updateTabIndexBasedOnSwipe()
        }),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center) {
        Row(modifier = Modifier.align(Alignment.CenterHorizontally)) {
            Text(
                text = "About",
                textAlign = TextAlign.Center,
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}

この記事が、Jetpack Compose で独自のタブ UI を作成するために必要なツールを提供したことを願っています。

上記の例は、ここにあります。

また、私が書いた他の記事を読みたい場合は、こちらからチェックできます。

参考文献:

ソース: https://www.freecodecamp.org

#jetpack

Buliding Todo App in Jetpack Compose

Learn how to built a todo app in jetpack compose.u will also learn about state management in compose and how to create an inline editor.
https://youtu.be/Y5_kanaupnM

#jetpack #compose #statemanagement #inlineeditor

Steven Parker

Steven Parker

1571916292

Saying Hello to Jetpack Compose

#Android #Jetpack #Jetpack-compass