Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
August 23, 2021 11:36 am GMT

How to build animated listswith MotionLayout andViewPager2

Hello! This is my first post on this platform, I hope youll like it. Pleas leave a comment or react to the post to show me support. P.S. Many more posts are on their way!

We are all seeing beautiful design concepts made by creative designers every day on various websites and blogs, but most of us never had a chance to try to build them. Have you ever wondered what its like to build such layouts in practice? Is it super easy with modern frameworks or is it too much hassle? Well the purpose of this blog is to solve that mystery and find out how building one of those complex designs looks like first hand.

Animating widgets like ImageViews, AppBars and DrawerLayouts is easy thanks to MotionLayout (which became stable with the release of ConstrainLayout 2.0), but what about list items? There are a bunch of blogs about animating widgets, but very few are about animation list items. For my sample project, I chose our Filmdom app and tried to implement its landing screen. Below, you will find out how it went and what are the pros and cons of building such a layout.

Final version of the layout is shown in the video below.

Alt Text

Note: It is considered that you know the basics of MotionLayout and ViewPager2, as we will not go into details of how each of these components work.

The purpose of this blog is list animation, so we will exclude everything else for the point of brevity.

Top picks for you

To showcase recommended movies, I used ViewPager2 which extends RecyclerView. The only attributes from the XML layout worth mentioning are clipToPadding and clipChildren, we need to set both of those attributes to false. Attribute clipChildren determines whether each child view is allowed to draw outside its own bounds within the parent, and clipToPadding attribute determines whether child view is allowed to draw outside of the parent itself. Without these attributes set to false, our ViewPager would clip two upcoming pages and we would only see the currently selected page which is not what we want. In this case, attribute paddingEnd narrows our selected page, so we have space to show two more pages on the screen.

<androidx.viewpager2.widget.ViewPager2    android:layout_width="match_parent"    android:layout_height=wrap_content"    android:clipChildren="false"    android:clipToPadding="false"    android:paddingEnd="120dp"  />

Margins are not supported by ViewPager.LayoutParams, so I had to wrap my fragments layout into an additional FrameLayout for our margin to be applied. I couldnt use padding on my layout because it has a background and if I had applied margin on the ViewPager widget directly, then we wouldnt see the selected page leaving the screen. Applying padding on the ViewPager widget is also not an option because we have set our clipToPadding and clipChildren attributes, and the result would be that the previous page is visible which is not what we want.

To achieve a faded effect on the unselected cards like in the design, I had to set the background of the inner FrameLayout to black. It is important that the background follows the shape of the loaded image so we dont get excess black area. The reason I didnt use some different approach to achieve this is that I needed to create a fade-in/fade-out effect, and the only way I found to do that is by changing the opacity of the view. We will go in further detail about this in the next section. Another thing I had to do to get three pages on the screen at the same time was to use the setOffscreenPageLimit method and set the limit value to 3 on our ViewPager.

<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>

For the purpose of scaling and fade-in/fade-out animations, I implemented PageTransformer on our ViewPager. In the overridden transformPage method, we first have to set the elevation on each card depending on its position. Line ViewCompat.setElevation(page, -abs(position)) sets the highest elevation on the first card in our stack of three and a proportionally smaller elevation on each consecutive card. This gives us the desired visual effect of cards coming towards us. Next thing we have to do is calculate our scale factor, so we can apply the correct values on our movie cards.

Our when function is scaling our cards if they are visible on the screen, otherwise they have default values. The transition on the X axis is proportional to the scale factor, so we can have smooth animations. The conditions afterwards determine the visibility of movie posters inside our card. Selected card in the center position has visibility at 100% and its visibility drops as it moves away from the center of the ViewPager. We can change the cards movie poster visibility through setAlpha method. This PageTransformer setup requires a little bit of trial and error to achieve the desired result.

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)            }        }    }}

We can conclude that MotionLayout is not designed to work with a single ViewPager2 page. For animating pages, we have to use PageTransformer and if we really want to use MotionLayout on a single page we can do so by putting its implementation logic inside the transformPage method. The issue with that approach is that we then have to find a way to track MotionLayouts progress from the value of the position argument and thats not an easy job to do. There is a possibility to animate all items in the ViewPager2/RecyclerView pretty easily, but that is not what we are looking for here.

Coming soon

For the upcoming movie list, I also used ViewPager2. Our pages layout is wrapped inside MotionLayout as we use it here to achieve the tilting animation on swipe gestures. Each swipe animates the whole ViewPager and all of its pages.

<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 below is self-explanatory, so I wont be getting into details about it. Pages are tilted to the right or to the left depending on the gesture direction.

<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>

To show 5 pages on the screen at the same time, I again had to use the setOffscreenPageLimit method on our ViewPager to set the limit to 3. I applied a custom OnPageChangeCallback on our ViewPager. This callback is determining the direction of the gesture by comparing the current offset to the previous one. With this information, we can calculate the realCurrentPosition, nextPosition and realOffset.

With the realCurrentPosition and nextPosition properties, we are fetching the first two visible pages from ViewPager. On swipe left, we are scaling down the currently selected page and scaling up the following page. For the swipe to the right, its vice-versa. This callback is also responsible for determining the strength of the gesture, so that we can tilt the pages accordingly.

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 for upcoming movies is applying translation on the X axis that is equivalent to the page position in the list and negative value of summed page margins. We are using values calculated in our custom OnPageChangeCallback to determine correct transition animation and progress of the transition on our ViewPager. There are also a bunch of page decorations that are not mentioned here, but you can check them out in a repository that is linked at the bottom.

val nextItemVisiblePx = resources.getDimension(R.dimen.viewpager_next_item_visible)  //50dpval currentItemHorizontalMarginPx = resources.getDimension(R.dimen.viewpager_current_item_horizontal_margin_right)  //230dpval pageTranslationX = nextItemVisiblePx + currentItemHorizontalMarginPxval 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)

Conclusion

Building complex layouts that include animations is still not an easy job to do even with MotionLayout and PageTransformer classes. Despite the fact that a bunch of popular apps from years ago have designs with multiple pages on the screen with only one page highlighted, there still isnt any standardised way of dealing with this issue. This is an easy job to do if we have three pages on the screen, but if we have five or more pages visible on the screen, you will get a massive headache before getting things to behave as you desire (no matter what widget you use for displaying a list of items).

MotionLayout and ViewPager2 are both great tools, but they are not meant to resolve the above mentioned issue. I think we should have a library dedicated to this kind of list presentation there is no sense in writing a bunch of complex calculations just to get a commonly wanted behaviour.

Looking to learn more?

You can find this entire project on GitLab, or you can hop over to our projects page to check out the full Filmdom case study.

If you know an easier way of solving this issue, or have some other complex layouts in mind that you would like to see come to life, feel free to leave a comment.


Original Link: https://dev.to/bornfightcompany/how-to-build-animated-lists-with-motionlayout-and-viewpager2-5di

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To