1677754504
このチュートリアルでは、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
例で TabRow の使用法を見てみましょう。3 つのタブを持つ単純なレイアウトを作成します。
@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()
}
}
}
注意すべき点がいくつかあります。
かなり当たり障りのないものですよね?
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()
}
}
}
見栄えは良くなりましたが、疑問が生じます: 画面に表示できるよりも多くのタブがある場合はどうなるでしょうか?
幸いなことに、答えは簡単です。
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()
}
}
}
スクロール可能なタブは便利ですが、タブ間のスワイプはさらに優れています。ほとんどのユーザーは、各タブをクリックするよりも、タブ間をスワイプする方が直感的だと感じるでしょう。ドキュメントを見ると、いくつかのオプションがあることがわかります。
これらのすべてが目標を達成するのに役立つわけではなく、それぞれに独自の理由があります。自分で何かをする「面倒」を経験したくない場合は、使用できるページャーと呼ばれる Accompanist のライブラリがあります。スワイプに反応する行/列を水平または垂直に作成する機能を追加できます。
実装する手順は既に説明されており、以下のリソースを使用してその方法を学習できます。
あなたが私のようで、自分のために何かをするのが好きで、手を汚すのが好きなら、読み進めてください。
スワイプ可能な修飾子について最初に知っておくべきことは、 @ ExperimentalMaterialApiで注釈が付けられていることです。これは、この API が Jetpack Compose のバージョン間で変更される可能性があり、安定していないことを意味します。
それとは別に、スワイプ可能な修飾子が使用するメカニズムを調べる必要があります。3 つのビルディング ブロックがあります。
@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 に進みます。
名前が示すように、この修飾子はドラッグ ジェスチャを検出します。これは、スワイプによく似ています。
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit
ご覧のとおり、onDrag コールバックには 2 つの引数があります。
このコールバックは、次の場合に呼び出されます。
「… ポインタが下に来るのを待って、任意の方向でタッチストップしてから、onDrag各ドラッグイベントを呼び出します。」
ドラッグ可能な修飾子の代わりにこの修飾子を使用する利点は、x 座標と y 座標の両方の変更に関する情報を提供することです。
それの欠点は、スワイプのためのスムーズでエレガントなソリューションを提供しないことです. これは、onDrag コールバックがトリガーされる回数が原因です。
ユーザーがスワイプ ジェスチャを実行すると、onDrag コールバックが複数回トリガーされます。これにより、いつ「ドラッグ」ジェスチャが完全に終了したかを判別するのが難しくなります。
これを試してみると、スワイプ ジェスチャごとに onDrag コールバックが 3 回トリガーされることがわかりました。これは私たちのユースケースには適していないので、ドラッグ可能な修飾子を調べてみましょう。
このモディファイヤは、前のモディファイヤの簡素化されたバージョンと考えてください。これは、ユーザーが 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 つの修飾子との類似点はありません。注意すべき点を指摘します。
とは異なりdetectDragGestures、ここではonDragStoppedすべてのスワイプ ジェスチャに対して 1 回呼び出されるため、この修飾子が最適な候補になります。
この例のスワイプジェスチャ検出器としての実装は非常に堅牢であるため、いくつかの前提条件から始めましょう。
#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
}
}
各画面は同じレイアウトで構成されています。
@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
)
}
}
}
そして最後に、私たちの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)
}
}
}
すべてをまとめると、次のようになります。
私たちが達成したことについて一言。お気付きかもしれませんが、画面ごとにボイラープレートを追加しているため、繰り返しが発生します。各画面はドラッグの状態を保存しています。
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 を作成するために必要なツールを提供したことを願っています。
上記の例は、ここにあります。
また、私が書いた他の記事を読みたい場合は、こちらからチェックできます。
参考文献:
#jetpack
1593253920
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:
There are three screens in this app
Home Screen
Profile Screen
Compose Screen
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.
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
1598743860
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 でタブ レイアウトを作成するにはどうすればよいでしょうか?
このチュートリアルでは、すべての基本について説明しますが、より高度な内容もいくつか示します。
タブ レイアウトを作成するには、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
例で TabRow の使用法を見てみましょう。3 つのタブを持つ単純なレイアウトを作成します。
@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()
}
}
}
注意すべき点がいくつかあります。
かなり当たり障りのないものですよね?
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()
}
}
}
見栄えは良くなりましたが、疑問が生じます: 画面に表示できるよりも多くのタブがある場合はどうなるでしょうか?
幸いなことに、答えは簡単です。
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()
}
}
}
スクロール可能なタブは便利ですが、タブ間のスワイプはさらに優れています。ほとんどのユーザーは、各タブをクリックするよりも、タブ間をスワイプする方が直感的だと感じるでしょう。ドキュメントを見ると、いくつかのオプションがあることがわかります。
これらのすべてが目標を達成するのに役立つわけではなく、それぞれに独自の理由があります。自分で何かをする「面倒」を経験したくない場合は、使用できるページャーと呼ばれる Accompanist のライブラリがあります。スワイプに反応する行/列を水平または垂直に作成する機能を追加できます。
実装する手順は既に説明されており、以下のリソースを使用してその方法を学習できます。
あなたが私のようで、自分のために何かをするのが好きで、手を汚すのが好きなら、読み進めてください。
スワイプ可能な修飾子について最初に知っておくべきことは、 @ ExperimentalMaterialApiで注釈が付けられていることです。これは、この API が Jetpack Compose のバージョン間で変更される可能性があり、安定していないことを意味します。
それとは別に、スワイプ可能な修飾子が使用するメカニズムを調べる必要があります。3 つのビルディング ブロックがあります。
@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 に進みます。
名前が示すように、この修飾子はドラッグ ジェスチャを検出します。これは、スワイプによく似ています。
suspend fun PointerInputScope.detectDragGestures(
onDragStart: (Offset) -> Unit = { },
onDragEnd: () -> Unit = { },
onDragCancel: () -> Unit = { },
onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit
): Unit
ご覧のとおり、onDrag コールバックには 2 つの引数があります。
このコールバックは、次の場合に呼び出されます。
「… ポインタが下に来るのを待って、任意の方向でタッチストップしてから、onDrag各ドラッグイベントを呼び出します。」
ドラッグ可能な修飾子の代わりにこの修飾子を使用する利点は、x 座標と y 座標の両方の変更に関する情報を提供することです。
それの欠点は、スワイプのためのスムーズでエレガントなソリューションを提供しないことです. これは、onDrag コールバックがトリガーされる回数が原因です。
ユーザーがスワイプ ジェスチャを実行すると、onDrag コールバックが複数回トリガーされます。これにより、いつ「ドラッグ」ジェスチャが完全に終了したかを判別するのが難しくなります。
これを試してみると、スワイプ ジェスチャごとに onDrag コールバックが 3 回トリガーされることがわかりました。これは私たちのユースケースには適していないので、ドラッグ可能な修飾子を調べてみましょう。
このモディファイヤは、前のモディファイヤの簡素化されたバージョンと考えてください。これは、ユーザーが 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 つの修飾子との類似点はありません。注意すべき点を指摘します。
とは異なりdetectDragGestures、ここではonDragStoppedすべてのスワイプ ジェスチャに対して 1 回呼び出されるため、この修飾子が最適な候補になります。
この例のスワイプジェスチャ検出器としての実装は非常に堅牢であるため、いくつかの前提条件から始めましょう。
#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
}
}
各画面は同じレイアウトで構成されています。
@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
)
}
}
}
そして最後に、私たちの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)
}
}
}
すべてをまとめると、次のようになります。
私たちが達成したことについて一言。お気付きかもしれませんが、画面ごとにボイラープレートを追加しているため、繰り返しが発生します。各画面はドラッグの状態を保存しています。
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 を作成するために必要なツールを提供したことを願っています。
上記の例は、ここにあります。
また、私が書いた他の記事を読みたい場合は、こちらからチェックできます。
参考文献:
#jetpack
1624790696
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
1571916292
#Android #Jetpack #Jetpack-compass