preface

Recently, I encountered an interface with round – cast while writing a business

While the interaction was being finalized, my colleagues suggested that Carousel in MotionLayout be considered as a Carousel component, which is designed for Carousel.

After learning MotionLayout really useful, in the words of colleagues, is to make the world without difficult animation.

Interpret A MotionLayout defines the first frame of an animation and the second frame of an animation. It automatically handles the animation after the event is triggered.

The point of this article is not to learn MotionLayout, but to teach you how to use Carousel.

FIG. 1 Figure 2 FIG. 3

Plus, the official effect is pretty cool:

Also, the sliding animation is very silky, so it is recommended to write your own experience.

Demo address: github.com/mCyp/Motion…

A list,

Before you learned Carousel, I assumed you had the basics of MotionLayout.

If you don’t know what a MotionLayout is, take a look at the official tutorial:

MotionLayout- Code Lab

Ok, back to the point, the Chinese meaning of Carousel – round cast, it is a round cast solution, can help us deal with more complex round cast animation.

1. Core concepts

We start with the official example to build a round broadcast as shown in the figure:

In MotionLayout, these views can be written:

In general, a rotation, will support sliding forward and sliding back these two actions. If we want to fully display a Carousel animation, we need at least three states:

  • previous
  • start
  • next

Start is the initial state of the component, start-previous represents the process of sliding forward, and start-next represents the process of sliding backward. The corresponding state is shown in the figure below:

As you can see from above, the screen can only display three views in the middle. Start is the starting state. At this time, there are five views A, B, C, D and E. The screen displays B, C and D. When A backward slide occurs, B, C, D, and E move to A, B, C, and D (the A component can do nothing about it) and see C, D, and E on the screen. Same with sliding forward.

Is three states really enough? This is the question of many students.

Carousel has an unexpected ability that resets the interface to start after each animation, but the data mapped to the control changes:

As this image shows, the interface resets to the start state after a start-next animation. Notice that although the state is reset, the relationship between the data and the control has changed!

Components from the original screen:

  • Widget BThe correspondingItem2
  • Widget CThe correspondingItem3
  • Widget DThe correspondingItem4

After resetting:

  • Widget BThe correspondingItem3
  • Widget CThe correspondingItem4
  • Widget DThe correspondingItem5

It’s the same as the data displayed on the screen in the Next state, so these three states are really good enough.

Code 2.

Explain the pseudocode for the procedure above

Step one

In the layout file MotionLayout, we write out all the elements in the interface:

<androidx.constraintlayout.motion.widget.MotionLayout . >

        <ImageView  android:id="@+id/imageView0" . />
        <ImageView  android:id="@+id/imageView1" . />
        <ImageView  android:id="@+id/imageView2" . />
        <ImageView  android:id="@+id/imageView3" . />
        <ImageView  android:id="@+id/imageView4" . />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

Layout hierarchy is important!

Layout hierarchy is important!

Layout hierarchy is important!

Important things need to be said three times. Make sure controls are placed in the right order. The system determines the order of views according to the hierarchy of views.

Otherwise, after the code is written, you will ask: why does the code have no problem, but the data arrangement after the animation is executed does not match?

Step 2

Constraintlayout describes the positions of components in the start, Previous, and Next states in the MotionScene file, which is equivalent to constraintLayout:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <Transition
        android:id="@+id/forward"
        motion:constraintSetEnd="@+id/next"
        motion:constraintSetStart="@id/start"
        motion:duration="1000">
        <OnSwipe
            motion:dragDirection="dragUp"
            motion:touchAnchorSide="left" />

    </Transition>

    <Transition
        android:id="@+id/backward"
        motion:constraintSetEnd="@+id/previous"
        motion:constraintSetStart="@+id/start">
        <OnSwipe
            motion:dragDirection="dragDown"
            motion:touchAnchorSide="right" />
    </Transition>

    <! -- Component location corresponding to previous state -->
    <ConstraintSet android:id="@+id/previous">
        <Constraint
            android:id="@+id/iv1"
            . />

        <Constraint
            android:id="@+id/iv2"
            . />

        <Constraint
            android:id="@+id/iv3"
            . />

        <Constraint
            android:id="@+id/iv4"
            . />
    </ConstraintSet>


    <! -- Component position corresponding to the start state -->
    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/iv1"
            . />

        <Constraint
            android:id="@+id/iv2"
            . />

        <Constraint
            android:id="@+id/iv3"
            . />

        <Constraint
            android:id="@+id/iv4"
            . />

        <Constraint
            android:id="@+id/iv5"
            . />
    </ConstraintSet>

    <! -- Next state corresponding component position -->
    <ConstraintSet android:id="@+id/next">
        <Constraint
            android:id="@+id/iv2"
            . />

        <Constraint
            android:id="@+id/iv3"
            . />

        <Constraint
            android:id="@+id/iv4"
            . />

        <Constraint
            android:id="@+id/iv5"
            . />
    </ConstraintSet>

</MotionScene>
Copy the code

ConstraintSet represents a state in which a constraint layout tells the system where the control with each ID should appear.

Then use the Transition corresponding to forward to represent the over-animation of start-previous, and use the Transition corresponding to backward to represent the over-animation of start-next, This is to tell the system what action will trigger what animation.

We did very little, but MotionLayout did a lot for us!

Step 3 Add Carousel

Introducing Carousel into the layout:

    <androidx.constraintlayout.motion.widget.MotionLayout . >

        <ImageView  android:id="@+id/iv1" . />
        <ImageView  android:id="@+id/iv2" . />
        <ImageView  android:id="@+id/iv3" . />
        <ImageView  android:id="@+id/iv4" . />
        <ImageView  android:id="@+id/iv5" . />

        <androidx.constraintlayout.helper.widget.Carousel
            android:id="@+id/carousel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:carousel_forwardTransition="@+id/forward"
            app:carousel_backwardTransition="@+id/backward"
            app:carousel_previousState="@+id/previous"
            app:carousel_nextState="@+id/next"
            app:carousel_infinite="true"
            app:carousel_firstView="@+id/iv2"
            app:constraint_referenced_ids="iv0,iv1,iv2,iv3,iv4" />

    </androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

Explain a few properties:

  • app:carousel_forwardTransition: forward jump animation, referenceforwardThe correspondingTransition
  • app:carousel_backwardTransitionBackward jump animation, referencebackwardThe correspondingTransition
  • app:carousel_previousState: Indicates the state after the forward jump animation is complete, referencepreviousThe state of theConstraintSet
  • app:carousel_nextState: Indicates the state after the backward jump animation, referencenextThe state of theConstraintSet
  • app:carousel_infinite: Starts an infinite loop
  • app:carousel_firstViewWhich control displays the first data,iv2Represents the control in the middleiv2The first data will be shown

Step 4 Declare it in the Activity

The adapter needs to be set up:

carousel.setAdapter(object : Carousel.Adapter {
            override fun count(a): Int {
              // need to return the number of items we have in the carousel
            }

            override fun populate(view: View, index: Int) {
                // need to implement this to populate the view at the given index
            }

            override fun onNewItem(index: Int) {
                // called when an item is set}})Copy the code

In this adapter, we need to tell the adapter how many pieces of data to display, how the control should update the display when a new page arrives, and so on.

Second, the actual combat

Here’s an example:

Confident analysis of this animation, also need to deal with two points:

  1. The picture changed from black and white to color
  2. Image to flip

The flip animation is relatively simple and can be handled with rotationY, but there is a pit, which I’ll look at later.

What about black and white pictures changing to color? The answer is ImageFilterView, he can set black and white to color effect through app: Saturation one key.

1. Describe the interface

We need to write all the controls in the XML file. There are four ImageViews in this interface, which may not be obvious in the screenshot, so I took another 3D view:

GuideLine 0.2, 0.4, 0.6 and 0.8 are used for the left and right orientation, as shown in the figure below:

Simplified layout file code:

<androidx.constraintlayout.motion.widget.MotionLayout .>

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/iv1" ./>

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/iv2" ./>

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/iv4" ./>

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/iv3" ./>

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glLeft"
        .
        app:layout_constraintGuide_percent="0.2" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glm1"
        .
        app:layout_constraintGuide_percent="0.4" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glm2"
        .
        app:layout_constraintGuide_percent="0.6" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/glRight"
        .
        app:layout_constraintGuide_percent="0.8" />


    <androidx.constraintlayout.helper.widget.Carousel
        android:id="@+id/carousel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:carousel_backwardTransition="@+id/backward"
        app:carousel_firstView="@+id/iv1"
        app:carousel_forwardTransition="@+id/forward"
        app:carousel_infinite="true"
        app:carousel_nextState="@+id/next"
        app:carousel_previousState="@+id/previous"
        app:constraint_referenced_ids="iv1,iv2,iv4,iv3" />

</androidx.constraintlayout.motion.widget.MotionLayout>
Copy the code

2. Draw animation

In the previous pseudocode, we learned that to define previou, Start, and Next states, let’s look at the basic start state. We can just express the constraints in the interface, just like we used in the constraint layout.

Take out the MotionScene file and put only the start state for now:

MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/iv1"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:translationZ="0dp"
            motion:layout_constraintLeft_toLeftOf="@+id/glLeft"
            motion:layout_constraintRight_toRightOf="@id/glRight"
            motion:layout_constraintTop_toTopOf="parent"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintDimensionRatio="2:1">
            <CustomAttribute
                motion:attributeName="Saturation"
                motion:customFloatValue="0.0"
                />
        </Constraint>

        <Constraint
            android:id="@+id/iv2"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:rotationY="30"
            android:scaleX="0.8"
            android:scaleY="0.8"
            android:translationZ="4dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintDimensionRatio="2:1"
            motion:layout_constraintLeft_toLeftOf="parent"
            motion:layout_constraintRight_toRightOf="@id/glm2"
            motion:layout_constraintTop_toTopOf="parent">
            <CustomAttribute
                motion:attributeName="Saturation"
                motion:customFloatValue="0.0"
                />
        </Constraint>

        <Constraint
            android:id="@+id/iv3"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:rotationY="To 30"
            android:scaleX="0.8"
            android:scaleY="0.8"
            android:translationZ="6dp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintDimensionRatio="2:1"
            motion:layout_constraintLeft_toLeftOf="@id/glm1"
            motion:layout_constraintRight_toRightOf="parent"
            motion:layout_constraintTop_toTopOf="parent">
            <CustomAttribute
                motion:attributeName="Saturation"
                motion:customFloatValue="0.0"
                />
        </Constraint>

        <Constraint
            android:id="@+id/iv4"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:scaleX="1.2"
            android:scaleY="1.2"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintDimensionRatio="2:1"
            android:translationZ="10dp"
            motion:layout_constraintLeft_toLeftOf="@+id/glLeft"
            motion:layout_constraintRight_toRightOf="@id/glRight"
            motion:layout_constraintTop_toTopOf="parent"
            >
            <CustomAttribute
                motion:attributeName="Saturation"
                motion:customFloatValue="1.0"
                />
        </Constraint>
    </ConstraintSet>
</MotionScene>
Copy the code

A brief description:

  • iv1On the inside, the position is centered above and below, the left side is at 0.2, the right side is at 0.8,SaturationIs 0, that is, the picture is black and white,translationZ0 dp.
  • iv2On the far left, the position is centered above and below, the left side is on the left side of the parent layout, and the right side is on the 0.4 line,SaturationIs zero,translationZAs the 4 dp,rotationYOffset by 30 degrees.
  • iv3On the far right, the position is centered above and below, the left is on the 0.6 line, the right is aligned to the right of the parent layout,SaturationIs zero,translationZFor the 6 dp,rotationYOffset -30 degrees.
  • iv3At the top, the position is centered above and below, the left side is at 0.2, the right side is at 0.8, and the length and width are enlarged 1.2 times each.SaturationIs 1, that is, color picture,translationZFor 10 dp.

If you’re careful, you’ll notice that I’m using too much translationZ.

This property allows us to set the shadow if there is a background. In addition, it also helps us to set the hierarchy. The default hierarchy in the layout file is that the View declared first has a lower hierarchy and the View declared later has a higher hierarchy, which means that the View written later will hide the View written earlier.

If you animate at this level, the pit will appear:

The animation would be messy without changing the hierarchy, so we need to adjust the hierarchy dynamically as the animation progresses.

Next state is very simple, the corresponding position can be changed, IV3 to IV1, IV1 to IV2, IV2 to IV4, IV4 to IV3, simple position swap, the same state, for the sake of space, the code will not be put.

In addition to the three states, you need to declare the animation in the MotionScene:

<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">
    <Transition
        motion:constraintSetStart="@id/start"
        motion:constraintSetEnd="@+id/next"
        motion:duration="1000"
        android:id="@+id/forward">
        <OnSwipe
            motion:dragDirection="dragLeft"
            motion:touchAnchorSide="left" />

    </Transition>

    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/previous"
        android:id="@+id/backward">
        <OnSwipe
            motion:dragDirection="dragRight"
            motion:touchAnchorSide="right" />

    </Transition>

    <ConstraintSet android:id="@+id/previous">.</ConstraintSet>

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


    <ConstraintSet android:id="@+id/next">.</ConstraintSet>
</MotionScene>
Copy the code

It tells the system to swipe left to animate start to next state, swipe right to animate start to previous state, and then reference Carousel in the layout file and the system knows what to do.

3. Set the number of rounds

Finally, you need to set the number of rounds in the call, and how to notify the View of changes after sliding index:

class CarMotionActivity : AppCompatActivity() {
    var images = intArrayOf(
        R.drawable.car1,
        R.drawable.car3,
        R.drawable.car4,
        R.drawable.car2
    )

    override fun onCreate(savedInstanceState: Bundle?). {
       / /...

        val carsoul = findViewById<Carousel>(R.id.carousel)
        carsoul.setAdapter(object : Carousel.Adapter {
            override fun count(a): Int {
                return 4
            }

            override fun populate(view: View? , index:Int) {
                if(view is ImageView){
                    view.setImageResource(images[index])
                }
            }

            override fun onNewItem(index: Int) {
                // called when an item is set}}}})Copy the code

Ok, the beginner version of Carousel has been successfully mastered.

Put it in the last sentence

Carousel is simple and based on ConstraintLayout, with low threshold of use.

Carousel is a great choice when you need a rotation with a few page elements and a little complex animation.

On the other hand, this may not be true when the page has a lot of changing elements and the animation is simple. Because you need to write three interfaces, the idea is simple, but it is not worth the amount of code!

After a wave of learning, I found that the design guru’s final dynamic effect is image overlay, one image needs two ImageViews to achieve!

Finished, learned again in vain!!

However, I took this opportunity to master the MotionLayout, later, when writing animation more technical reserve.