【Jetpack Compose】脱 Tv Lazy Layout --- 移行前後の差分編
tv-foundation 1.0.0-alpha11 のリリース
2024年07月10日に Android TV 向けのライブラリである tv-foundation 1.0.0-alpha11 がリリースされました。
リリースノート に記載されている内容は以下の通りです。[1]
Version 1.0.0-alpha11
androidx.tv:tv-foundation:1.0.0-alpha11
is released. Version 1.0.0-alpha11 contains these commits.API Changes
- Tv Lazy Layouts have been deprecated from tv-foundation library. Refer to this ticket to learn how to migrate away from the tv lazy layouts. (I0855f, b/332674072)
PlatformImeOptions
is now a concrete class instead of an interface. (If40a4)
Tv Lazy Layout が非推奨になるから、Lazy Layout を使うようにしてね とのことです。
私の開発している Android TV 向けアプリも先日 Tv Lazy Layout から Lazy Layout への移行対応を行いました。
基本的には マイグレーションガイド に従っていけばよかったのですが、いくつかハマったポイントがあったので記事にまとめていこうと思います。
内容が少し多くなりそうなので、いくつかの記事に分けて記載していこうと思います。
パート | 内容 |
---|---|
Part.1 | 移行前後の差分編 |
Part.2 | アニメーション編 |
Part.3 | テスト編 |
本記事では、私の経験したハマりポイントを記載する前段階として、改めて移行前後でどのような差分が入るのかをまとめます。
Tv Lazy Layout と Lazy Layout におけるスクロール処理
Tv Lazy Layout から Lazy Layout へ移行する際に注意すべき差分は「フォーカスに伴うスクロールロジック」くらいです。
マイグレーションガイド にもフォーカスに伴うスクロールロジックをカスタマイズしていなければ単に Tv
のプレフィックスを取るだけで良いと記載されています。
【Tv Lazy Layout】 PivotOffsets
PivotOffsets
は アイテムがフォーカスされた際にどれだけスクロールさせるか を決めるためのクラスです。
PivotOffsets.kt (tv-foundation 1.0.0-alpha10)
/**
* Holds the offsets needed for scrolling-with-offset.
*
* @property parentFraction defines the offset of the starting edge of the child
* element from the starting edge of the parent element. This value should be between 0 and 1.
* @property childFraction defines the offset of the starting edge of the child from
* the pivot defined by parentFraction. This value should be between 0 and 1.
*/
@Immutable
class PivotOffsets constructor(
@FloatRange(
from = 0.0,
to = 1.0,
fromInclusive = true,
toInclusive = true
) val parentFraction: Float = 0.3f,
@FloatRange(
from = 0.0,
to = 1.0,
fromInclusive = true,
toInclusive = true
) val childFraction: Float = 0f,
) {
....
}
PivotOffsets
は TvLazyColumn
特有の引数であり、LazyColumn
では引数で受け取れません。[2]
デフォルト引数では、parentFraction = 0.3f
, childFraction = 0f
が設定されます。
これは以下画像のように フォーカスされた Component の最上部がリストの高さの 30% 地点に揃えられるようにスクロールさせる ということを意味しています。
スクロールの基準位置 | 実際のスクロールアニメーション |
---|---|
前述の通り、デフォルト引数 (parentFraction = 0.3f
, childFraction = 0f
) のままであれば、Lazy Layout への移行時に必要になる対応は特にありません。
しかし、これらの値を変更している場合は後述の CompositionLocal で BringIntoViewSpec
を配布する実装を行う必要があります。
【Lazy Layout】 BringIntoViewSpec
マイグレーションガイド に突如出現しました。
Lazy Layout では PivotOffset
を引数に受け取らないため、BringIntoViewSpec
インスタンスを CompositionLocal で配布する方式になるようです。
呼び出し元の差分としては以下のようになるイメージです。
(createBringIntoViewSpec
に関しては マイグレーションガイド を参考にして実装してください)
-TvLazyColumn(
- pivotOffsets = PivotOffsets(parentFraction = 1f),
-) {
- ....
-}
+val bringIntoViewSpec = remember { createBringIntoViewSpec(parentFraction = 1f) }
+
+CompositionLocalProvider(
+ LocalBringIntoViewSpec provides bringIntoViewSpec
+) {
+ LazyColumn {
+ ....
+ }
+}
馴染みのない BringIntoViewSpec
の登場は少し戸惑いますね🙄
Tv Lazy Layout の実装詳細を少し追ってみると、実は引数に渡した PivotOffsets
が BringIntoViewSpec
インスタンスの生成に利用されていたことが分かります。
ScrollableWithPivot.kt (tv-foundation 1.0.0-alpha10)
@OptIn(ExperimentalFoundationApi::class)
@ExperimentalTvFoundationApi
fun Modifier.scrollableWithPivot(
state: ScrollableState,
orientation: Orientation,
pivotOffsets: PivotOffsets,
enabled: Boolean = true,
reverseDirection: Boolean = false
): Modifier = this then Modifier.inspectable(debugInspectorInfo { .... }) {
Modifier.scrollable(
state = state,
orientation = orientation,
enabled = enabled,
reverseDirection = reverseDirection,
overscrollEffect = null,
bringIntoViewSpec = TvBringIntoViewSpec(pivotOffsets, enabled)
)
}
@OptIn(ExperimentalFoundationApi::class)
private class TvBringIntoViewSpec(
val pivotOffsets: PivotOffsets,
val userScrollEnabled: Boolean
) : BringIntoViewSpec {
// アイテムにフォーカスが当たった際に生じるスクロールのアニメーションを設定する
override val scrollAnimationSpec: AnimationSpec<Float> = tween<Float>(
durationMillis = 125,
easing = CubicBezierEasing(0.25f, 0.1f, .25f, 1f)
)
// アイテムにフォーカスが当たった際に生じるスクロール量を返す
override fun calculateScrollDistance(offset: Float, size: Float, containerSize: Float): Float {
if (!userScrollEnabled) return 0f
val leadingEdgeOfItemRequestingFocus = offset
val trailingEdgeOfItemRequestingFocus = offset + size
val sizeOfItemRequestingFocus =
abs(trailingEdgeOfItemRequestingFocus - leadingEdgeOfItemRequestingFocus)
val childSmallerThanParent = sizeOfItemRequestingFocus <= containerSize
val initialTargetForLeadingEdge =
pivotOffsets.parentFraction * containerSize -
(pivotOffsets.childFraction * sizeOfItemRequestingFocus)
val spaceAvailableToShowItem = containerSize - initialTargetForLeadingEdge
val targetForLeadingEdge =
if (childSmallerThanParent && spaceAvailableToShowItem < sizeOfItemRequestingFocus) {
containerSize - sizeOfItemRequestingFocus
} else {
initialTargetForLeadingEdge
}
return leadingEdgeOfItemRequestingFocus - targetForLeadingEdge
}
override fun hashCode(): Int { .... }
override fun equals(other: Any?): Boolean { .... }
}
つまり、公開されているインタフェースは PivotOffsets
から BringIntoViewSpec
に変更されたものの、内部のスクロールロジックは Tv Lazy Layout と Lazy Layout で同じようです。
スクロール処理の差分まとめ
以上の差分を改めてまとめてみます。
- Before
-
PivotOffets
を Tv Lazy Layout の引数に渡す- Tv Lazy Layout の内部実装によって
BringIntoViewSpec
が生成される - スクロール処理を行う ModifierNode は
ModifierNodeElement
から渡されたBringIntoViewSpec
を参照しながら処理を実行する
- Tv Lazy Layout の内部実装によって
-
- After
- Lazy Layout の実行側で
BringIntoViewSpec
を生成し、配布する- CompositionLocal を利用して配布を行う
- スクロール処理を行う ModifierNode は CompositionLocal にアクセスしながら処理を実行する
- Lazy Layout の実行側で
まとめ
tv-foundation 1.0.0-alpha11 にて Tv Lazy Layout が非推奨になり、Lazy Layout への移行が必要になりました。
Tv Lazy Layout のデフォルト引数で受け取っている PivotOffsets
を使用している場合は単に Tv
のプレフィックスを取るだけで、他に必要な作業はありません。
PivotOffsets
にデフォルト引数とは異なる値を設定している場合は「CompositionLocal を利用して BringIntoViewSpec
を配布させる」という対応が必要になります。
以降の記事で、「Tv Lazy Layout から Lazy Layout への移行で私が遭遇したハマりポイント」に関して記載していこうと思います。
Discussion