MotionLayoutとViewPager 2でアニメーションリストを構築する方法


こんにちは!これは私の最初のポストです.コメントを残したり、サポートを表示するには、ポストに反応します.より多くのポストは、彼らの方法にあります!
私たちは皆、さまざまなウェブサイトやブログでクリエイティブデザイナーによって作られた美しいデザインのコンセプトを見ていますが、私たちのほとんどは、それらを構築しようとする機会はありませんでした.あなたはそれが実際にそのようなレイアウトを構築するようなものだろうか?それは現代のフレームワークで超簡単です、あるいは、それはあまりに多くの面倒ですか?さて、このブログの目的は、その謎を解決し、それらの複雑なデザインのいずれかを構築する方法を見つける最初の手のように見えます.

実装


ImageView、AppBarとdrawerLayoutsのようなウィジェットをアニメーション化するのは簡単です.MotionLayoutLayoutLayout 2.0のリリースで安定していますが、リスト項目はどうですか?ウィジェットをアニメーションについてのブログの束が非常に少数のアニメーションリストの項目についてです.私のサンプルプロジェクトのために、私は我々のFilmdomアプリを選んで、その着陸画面を実行しようとしました.以下に、あなたはどのように行って、何がこのようなレイアウトを構築するの長所と短所を見つけるでしょう.
レイアウトの最終版は、下記のビデオで示されます.

注意: MotionLayoutとViewPager 2の基本を知っていると考えられます.これらのコンポーネントがどのように動作するかについては詳細は説明しません.
このブログの目的はリストアニメーションですので、簡潔さのために他のすべてを除外します.

あなたのためのトップピック


ショーケース映画を紹介するには、ViewPageer 2を使用して、RegulerViewを拡張します.言及する価値があるXMLレイアウトからの唯一の特質は、クリップトップとClipChildrenです.属性ClipChildrenは、各子ビューが親の範囲内で独自の境界線の外側に描画されるかどうかを判断し、ClipTopAdd属性によって、子プロセスが親自身の外側で描画できるかどうかを判断します.これらの属性がfalseに設定されていない場合、私たちのViewPagerは2つの今後のページをクリップします、そして、我々は我々が望むものでない現在選択されたページだけを見ます.この場合、属性paddingendは私たちの選択されたページを狭くします.
<androidx.viewpager2.widget.ViewPager2
    android:layout_width="match_parent"
    android:layout_height=”wrap_content"
    android:clipChildren="false"
    android:clipToPadding="false"
    android:paddingEnd="120dp"  />
マージンはViewPagerによってサポートされません.LayoutParamsので、私は私のマージンのレイアウトを適用するために私のフラグメントのレイアウトをラップしなければならなかった.私は私のレイアウトにパディングを使用することができませんでした、それはバックグラウンドを持っています、そして、私が直接ViewPagerウィジェットにマージンを適用したならば、我々はスクリーンを残している選択されたページを見ません.ViewPagerウィジェットにパディングを適用することも、オプションではありません.なぜなら、私たちはクリップtopaddとclipchildren属性を設定しており、結果は前のページが見えることです.
デザインのような選択されていないカードにフェードされた効果を得るために、内側のフレームの背景を黒に設定しなければならなかった.バックグラウンドは積載された画像の形に従っているので、余分な黒い領域を得ない.私がこれを達成するためにいくつかの異なるアプローチを使用しなかった理由は、私がフェードイン/フェードアウト効果を作成する必要があったということです.次のセクションでこれについてさらに詳しく説明します.スクリーンの3ページを同時に取得するために別のことを行うには、SetoffScreenPageLimitメソッドを使用して、制限値を「3」に設定しました.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:background="@drawable/bg_rounded_circle_drawable">

        <ImageView
               
         />

    </FrameLayout>
</FrameLayout>
スケーリングとフェードイン/フェードアウトアニメーションの目的のために、私はPageTransformerをViewPagerに実装しました.オーバーライドされたTransformPageメソッドでは、まずその位置に応じて各カードに対して標高を設定します.行viewcompat.SetAscenation(ページ、- ABS(位置))の3つのスタック内の最初のカード上で最も高い標高を設定し、各連続カード上で比例的に小さな標高.これは私たちに向かってカードの目的の視覚効果を与える.我々がしなければならない次のことは我々のスケールファクターを計算するので、我々は我々の映画カードに正しい値を適用することができます.
私たちの関数は、画面上に表示されている場合、私たちのカードをスケーリングされていない場合、それ以外の場合はデフォルト値があります.X軸上の遷移はスケールファクタに比例するので、滑らかなアニメーションを持つことができます.その後の条件は、カード内のムービーポスターの可視性を決定します.中央の位置の選択されたカードは、それが視認者の中心から離れて動くように、100 %で可視性を持ちます、そして、その可視性は低下します.我々は、SetallPharメソッドを介してカードの映画ポスターの可視性を変更することができます.このPageTransformerセットアップは、所望の結果を達成するために試行錯誤を少し必要とします.
class SliderTransformer(private val offscreenPageLimit: Int) : ViewPager2.PageTransformer {

    companion object {
        private const val DEFAULT_TRANSLATION_X = .0f
        private const val DEFAULT_TRANSLATION_FACTOR = 1.46f
        private const val SCALE_FACTOR = .14f
        private const val DEFAULT_SCALE = 1f
    }

    override fun transformPage(page: View, position: Float) {

        page.apply {
            ViewCompat.setElevation(page, -abs(position))
            val scaleFactor = -SCALE_FACTOR * position + DEFAULT_SCALE
            when (position)  {
                    in 0f..offscreenPageLimit - 1f -> {
                         scaleX = scaleFactor
                         scaleY = scaleFactor
                         translationX = -(width / DEFAULT_TRANSLATION_FACTOR) * position
                    }
                    else -> {
                         translationX = DEFAULT_TRANSLATION_X
                         scaleX = DEFAULT_SCALE
                         scaleY = DEFAULT_SCALE
                   }
            }

            val recommendedMovieIV: ImageView = findViewById(R.id.recommendedMovieIV)
            if (position <= -1.0f || position >= 1.0f) {
                recommendedMovieIV.alpha = 0.5f
            } else if (position == 0.5f) {
                recommendedMovieIV.alpha = 1.0f
            } else if (position < 0.5f) {
                recommendedMovieIV.alpha = 1.0f - abs(position)
            }
        }
    }
}
MotionLayoutは1つのViewPager 2ページで動作するように設計されていないと結論づけられます.ページをアニメーション化するためには、PageTransformerを使用する必要があります.また、1つのページに実際にMotionLayoutを使用したい場合は、その実装ロジックをTransformPageメソッド内に置くことによって行うことができます.このアプローチの問題は、位置引数の値からMotionLayoutの進捗状況を追跡する方法を見つけなければならないことです.ViewPageer 2/RefererViewのすべてのアイテムをかなり簡単にアニメーション化する可能性がありますが、それは我々がここを探しているものではありません.

カミングスーン


今度の映画リストのために、私もviewpager 2を使いました.私たちのページのレイアウトは、我々は強打のジェスチャーで傾斜アニメーションを達成するためにここでそれを使用してMotionLayout内にラップされています.各スワイプは、全体のviewpagerとそのページのすべてをアニメーション化します.
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:minHeight="240dp"
    app:layoutDescription="@xml/tilt_scene">

    <LinearLayout
            
     />

</androidx.constraintlayout.motion.widget.MotionLayout>
以下のMotionSceneは自己説明的ですので、詳細については知りません.ページは、右または左にジェスチャーの方向に応じて傾斜している.
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        android:id="@+id/rightToLeft"
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@id/start"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragLeft"
            app:touchAnchorId="@id/motionContainer" />

        <KeyFrameSet>
            <KeyAttribute
                android:rotationY="0"
                app:framePosition="0"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="-15"
                app:framePosition="25"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="-30"
                app:framePosition="50"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="-15"
                app:framePosition="75"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="0"
                app:framePosition="100"
                app:motionTarget="@id/motionContainer" />
        </KeyFrameSet>
    </Transition>

    <Transition
        android:id="@+id/leftToRight"
        app:constraintSetEnd="@id/end"
        app:constraintSetStart="@id/start"
        app:duration="1000">

        <OnSwipe
            app:dragDirection="dragRight"
            app:touchAnchorId="@+id/motionContainer" />

        <KeyFrameSet>
            <KeyAttribute
                android:rotationY="0"
                app:framePosition="0"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="15"
                app:framePosition="25"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="30"
                app:framePosition="50"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="15"
                app:framePosition="75"
                app:motionTarget="@id/motionContainer" />
            <KeyAttribute
                android:rotationY="0"
                app:framePosition="100"
                app:motionTarget="@id/motionContainer" />
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start" />

    <ConstraintSet android:id="@+id/end" />

</MotionScene>
同時に5つのページを画面に表示するには、「3」に制限を設定するには、SetHoffScreenPageLimitメソッドをViewPagerに使用しなければなりませんでした.私はカスタムonPageChangeCallbackをViewPagerに適用しました.このコールバックは、現在のオフセットを前のものと比較することによって、ジェスチャの方向を決定します.この情報により、RealCurrentPosition、NextPosition、RealOffsetを計算できます.
RealCurrentPositionとNextPositionプロパティを使用して、ViewPagerから最初の2つの可視ページを取得します.スワイプ左に、我々は現在選択したページを縮小し、次のページをスケーリングしている.右への強打のために、それは逆です.このコールバックもジェスチャーの強さを決定するのに責任があるので、我々はそれに応じてページを傾けることができます.
class UpcomingMovieChangedCallback(private val binding: ActivityMainBinding, private val upcomingMoviesAdapter: GenericMoviesAdapter) : ViewPager2.OnPageChangeCallback() {

    var goingLeft: Boolean by Delegates.notNull()
    private var lastOffset = 0f
    var progress: Float by Delegates.notNull()

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
        val realCurrentPosition: Int
        val nextPosition: Int
        val realOffset: Float
        goingLeft = lastOffset > positionOffset
        if (goingLeft) {
            realCurrentPosition = position + 1
            nextPosition = position
            realOffset = 1 - positionOffset
        } else {
            nextPosition = position + 1
            realCurrentPosition = position
            realOffset = positionOffset
        }

        val currentCard = (binding.upcomingMoviesVP[0] as RecyclerView).layoutManager?.findViewByPosition(realCurrentPosition)
        currentCard?.scaleX = (1 + 0.4 * (1 - realOffset)).toFloat()
        currentCard?.scaleY = (1 + 0.4 * (1 - realOffset)).toFloat()
        currentCard?.pivotY = 0f

        val nextCard = (binding.upcomingMoviesVP[0] as RecyclerView).layoutManager?.findViewByPosition(nextPosition)
        nextCard?.scaleX = (1 + 0.4 * realOffset).toFloat()
        nextCard?.scaleY = (1 + 0.4 * realOffset).toFloat()
        nextCard?.pivotY = 0f

        lastOffset = positionOffset
        progress = when (position) {
            position -> positionOffset
            position + 1 -> 1 - positionOffset
            position - 1 -> 1 - positionOffset
            else -> 0f
        }
    }
}
次の映画のためのPageTransformerは、リストのページ位置と加算されたページ余白の負の値に等しいX軸に翻訳を適用しています.我々のカスタムonPageChangeCallbackで計算された値を使用して、正しい移行アニメーションと我々のViewPagerの移行の進行を決定する.また、ここに記載されていないページの装飾の束がありますが、下部にリンクされているリポジトリでそれらをチェックアウトすることができます.
val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)  //50dp
val currentItemHorizontalMarginPx = resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin_right)  //230dp
val pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPx
val pageTransformer = PageTransformer { page: View, position: Float ->
    page.translationX = -pageTranslationX * position

    if (upcomingMovieChangedCallback.goingLeft) {
        ((page as ViewGroup).getChildAt(0) as MotionLayout).setTransition(R.id.leftToRight)
    } else {
        ((page as ViewGroup).getChildAt(0) as MotionLayout).setTransition(R.id.rightToLeft)
    }
    (page.getChildAt(0) as MotionLayout).progress = upcomingMovieChangedCallback.progress
}
binding.upcomingMoviesVP.setPageTransformer(pageTransformer)

結論


アニメーションを含む複雑なレイアウトを構築することは、まだMotionLayoutとPageTransformerクラスでさえする簡単な仕事でありません.数年前から人気のアプリの束が画面上の複数のページを1つだけのページを強調表示されているという事実にもかかわらず、まだこの問題に対処する任意の標準化された方法はありません.これは、画面上に3ページがある場合は、簡単な仕事ですが、画面上に5つ以上のページが表示されている場合は、あなたが望むように動作するように物事を取得する前に大規模な頭痛を取得します(アイテムのリストを表示するために使用するウィジェットは関係ありません).
MotionLayoutとViewPager 2は両方とも素晴らしいツールですが、上記の問題を解決するためのものではありません.私は、リストのプレゼンテーションのこの種の専用ライブラリを持っている必要があります-複雑な計算の束を書くだけで一般的に求められた行動を得るために意味がないです.

詳細をお探しですか?


あなたはGitLabでこのプロジェクト全体を見つけることができるか、フルFilmdom case studyをチェックアウトする当社のプロジェクトページにホップすることができます.
あなたがこの問題を解決する簡単な方法を知っているか、またはあなたが人生に来て見たいと思う心のいくつかの他の複雑なレイアウトを持っている場合は、コメントを残して自由に感じる.