Please indicate the source of reprint:Juejin. Im/post / 684490…

Since the release of wechat, the animation of the bottom navigation bar has been making developers happy, and with the version update, the animation of the bottom navigation bar has been improving. Recently, IN my spare time, I watched the bottom navigation bar animation of wechat, and then I thought about the principle of this animation, which was very interesting, so I wrote this article.

The picture below is the effect I achieved, you can compare the effect of wechat, almost can pass for the real.

The animation process

About the process of animation, I just started looking at it for a long time, because if we do not understand the process of animation is also impossible to achieve, so the animation process is very important, this animation actually has two processes

  1. The first step is to change the outline of the default image.
  2. When the contours change color to a certain extent, the whole image has a green fill effect, which means the whole image starts to turn green until the whole image is completely green. In fact, this is the opacity of the two images to achieve the effect.

Animation implementation principle

In the process of sliding, we can get a sliding proportion value by listening to the sliding event of the ViewPager.

The four tabs of the navigation bar at the bottom can be implemented with a custom View, which I call a TabView. So, during the slide, the TabView on the current page performs a fade animation, and the next page performs a color change animation. How far the animation goes is definitely determined by the ViewPager’s sliding scale value. Therefore, the TabView needs a method that receives the animation progress scale value to control the degree of animation.

Code implementation

As the saying goes, Talk is cheap, show me the code! . Let’s do it in code. It must be a very exciting journey!

I’m leaving out some of the ViewPager boilerplate code because I don’t want to overwrite it, because it’s basic. If you don’t know how to use ViewPager, you can find a lot of articles on the Internet. This article is about how to customize the TabView.

There are many ways to customize a View, and I’m sure many people know better than I do. Instead, I chose to combine system controls to implement the custom View. So some people might ask me, can I completely customize a View for better drawing performance? This is certainly possible, and by the end of this article you will be able to do this awesome operation. However, this improvement in rendering performance is actually negligible on today’s high-spec phones. In order to develop efficiency, combining system controls should be the first choice.

Realize the layout of composite controls

The layout of the combined controls required by the TabView is as follows

// tab_layout.xml

<?xml version="1.0" encoding="utf-8"? >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:gravity="center_horizontal"
    android:orientation="vertical">

    <FrameLayout
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        android:layout_weight="1">

        <ImageView
            android:id="@+id/tab_image"
            android:layout_width="wrap_content"
            android:layout_height="match_parent" />

        <ImageView
            android:id="@+id/tab_image_top"
            android:layout_width="wrap_content"
            android:layout_height="match_parent" />
    </FrameLayout>

    <TextView
        android:id="@+id/tab_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="12sp" />
</LinearLayout>
Copy the code

The TextView of the layout must be used to display the title, but there are two ImageViews. Why? This has to do with the implementation of our animation.

The @+id/tab_image ImageView is at the bottom. It is used to display a default image, which I call a profile. For example, the profile of the TabView on the first page is shown below

We need to change the color of the contour, and if you look at the animation process, the first process is obviously the color change of the contour.

The ImageView of @+id/tab_image_top is on the top, which is used to display the selected image of a page. It is also used to display the final image of the animation. For example, the selected image of the TabView of the first page is shown below

Now let’s show you how to animate this layout.

  1. First of allTabViewShow the contours, and hide the selected ones. How to hide it, I chose to hide the selected image with transparency, because the entire animation has a transformation of transparency.
  2. When the slidingViewPagerThe time,TabViewTo get the progress value of the slide, we let the contour of the contour graph begin to change color. So how do you change the color? There’s a handy way to do that, rightDrawable.setTint()Methods. And the way this works isPorterDuff.Mode.DST_INMixed mode. If you’re interested, you can go and see how it works.
  3. whenViewPagerWhen you slide to a certain distance, if you release your finger, the page will automatically slide to the next page. What is the ratio value? I haven’t looked at it yet, but I’ll assume 0.5. When the slide comparison is more than 0.5, let the opacity of the contour gradually change to 0, which means it is gradually invisible. At the same time, the opacity of the selected image gradually changes to 255, which means it is gradually clear. This will give the overall color fill effect of the contour.

Okay, so if the implementation idea is interesting, let’s implement this custom ViewTabView based on that idea.

Realize the TabView

Loading layout

Now that you have the layout, first load it in the constructor of the TabView

public TabView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // Load the layout
        inflate(context, R.layout.tab_layout, this);
}
Copy the code

Custom properties and parsing

To better use TabView in XML layout, I extracted custom attributes for TabView

// res/values/tabview_attrs.xml

<?xml version="1.0" encoding="utf-8"? >
<resources>
    <declare-styleable name="TabView">
        <attr name="tabColor" format="color|integer" />
        <attr name="tabImage" format="reference" />
        <attr name="tabSelectedImage" format="reference" />
        <attr name="tabTitle" format="string|reference" />
    </declare-styleable>
</resources>
Copy the code

TabColor represents the final color of the color change, which can be obtained from the selected graph using the color picker.

TabImage represents the outline that is displayed by default.

TabSelectedImage represents the selected figure.

TabTitle represents the title to be displayed.

With these custom properties, you must parse them in the TabView

public TabView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // Load the layout
        inflate(context, R.layout.tab_layout, this);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabView);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            switch (attr) {

                case R.styleable.TabView_tabColor:
                    // Get the final coloring of the title and outline
                    mTargetColor = a.getColor(attr, DEFAULT_TAB_TARGET_COLOR);
                    break;

                case R.styleable.TabView_tabImage:
                    // Get the contours
                    mNormalDrawable = a.getDrawable(attr);
                    break;

                case R.styleable.TabView_tabSelectedImage:
                    // Get the selected graph
                    mSelectedDrawable = a.getDrawable(attr);
                    break;

                case R.styleable.TabView_tabTitle:
                    // Get the title
                    mTitle = a.getString(attr);
                    break;
            }

        }
        a.recycle();
    }
Copy the code

After the custom properties are resolved, you need to initialize the control with these property values. The onFinishInflate() method of the View means that the layout is loaded, so you get the control here and initialize it.

    @Override
    protected void onFinishInflate(a) {
        super.onFinishInflate();

        // 1. Set the title, which is colored black by default
        mTitleView = findViewById(R.id.tab_title);
        mTitleView.setTextColor(DEFAULT_TAB_COLOR);
        mTitleView.setText(mTitle);

        // 2. Set the contour image to be opaque and the default color is black
        mNormalImageView = findViewById(R.id.tab_image);
        mNormalDrawable.setTint(DEFAULT_TAB_COLOR);
        mNormalDrawable.setAlpha(255);
        mNormalImageView.setImageDrawable(mNormalDrawable);

        // 3. Set the selected image to transparent, and the default color is black
        mSelectedImageView = findViewById(R.id.tab_selected_image);
        mSelectedDrawable.setAlpha(0);
        mSelectedImageView.setImageDrawable(mSelectedDrawable);
    }
Copy the code

The title sets a default color DEFAULT_TAB_COLOR, which is black. Also, set the contour to black. The opacity of the contour is initially 255, which means it is fully visible, and the opacity of the selected graph is set to 0, which means it is completely invisible. All of this is the initial state of the animation.

Control animation progress

One of the things I mentioned earlier is that the TabView needs to use the ViewPager to slide the progress value to control the progress of the animation. Therefore, we need to define a method for the TabView to receive the progress value.

    /** * Color and transparency processing according to the progress value. * *@paramPercentage Indicates the progress value. The value is 0 or 1. * /
    public void setXPercentage(float percentage) {
        if (percentage < 0 || percentage > 1) {
            return;
        }

        // 1. Color change
        int finalColor = evaluate(percentage, DEFAULT_TAB_COLOR, mTargetColor);
        mTitleView.setTextColor(finalColor);
        mNormalDrawable.setTint(finalColor);

        // 2
        if (percentage >= 0.5 && percentage <= 1) {
            // The principle is as follows
            // The progress value is 0.5 ~ 1
            // Transparency: 0 ~ 1
            Percentage - 1 = (alpha - 1) * 0.5
            int alpha = (int) Math.ceil(255 * ((percentage - 1) * 2 + 1));
            mNormalDrawable.setAlpha(255 - alpha);
            mSelectedDrawable.setAlpha(alpha);
        } else {
            mNormalDrawable.setAlpha(255);
            mSelectedDrawable.setAlpha(0);
        }

        // 3. Update UI
        invalidateUI();
    }
Copy the code

In this open interface, we first calculate the color to use for the contour based on the progress value. We start with a black color, we end with a green color, and then we have a progress value, so how do we compute the color value for a progress? In fact, there is a class in the property animation, ArgbEvaluator, which provides the method for calculating the color. The code is as follows

    public Object evaluate(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        float startA = ((startInt >> 24) & 0xff) / 255.0 f;
        float startR = ((startInt >> 16) & 0xff) / 255.0 f;
        float startG = ((startInt >>  8) & 0xff) / 255.0 f;
        float startB = ( startInt        & 0xff) / 255.0 f;

        int endInt = (Integer) endValue;
        float endA = ((endInt >> 24) & 0xff) / 255.0 f;
        float endR = ((endInt >> 16) & 0xff) / 255.0 f;
        float endG = ((endInt >>  8) & 0xff) / 255.0 f;
        float endB = ( endInt        & 0xff) / 255.0 f;

        // convert from sRGB to linear
        startR = (float) Math.pow(startR, 2.2);
        startG = (float) Math.pow(startG, 2.2);
        startB = (float) Math.pow(startB, 2.2);

        endR = (float) Math.pow(endR, 2.2);
        endG = (float) Math.pow(endG, 2.2);
        endB = (float) Math.pow(endB, 2.2);

        // compute the interpolated color in linear space
        float a = startA + fraction * (endA - startA);
        float r = startR + fraction * (endR - startR);
        float g = startG + fraction * (endG - startG);
        float b = startB + fraction * (endB - startB);

        // convert back to sRGB in the [0..255] range
        a = a * 255.0 f;
        r = (float) Math.pow(r, 1.0 / 2.2) * 255.0 f;
        g = (float) Math.pow(g, 1.0 / 2.2) * 255.0 f;
        b = (float) Math.pow(b, 1.0 / 2.2) * 255.0 f;

        return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
    }
Copy the code

If you’re familiar with property animation, you’ll know that the float Fraction is in the range of 0.f to 1.f, so you can copy this method over and use it.

After calculating the color values, you can color the headings and Outlines.

The second step is to transform the opacity of the contours and the selected images when the slide progress reaches 0.5, according to the animation principle mentioned above.

So first we have to figure out what the transparency is for a given progress. Obviously, this is a math problem, and the progress varies from 0.5 to 1.0, and the transparency changes from 0 to 1.0(then multiply by 255 to get the actual transparency). If the ratio value of transparency to progress is 2, then the formula alpha – 1 = (percentage – 1.0) * 2 can be obtained. With this formula, you can calculate the transparency for any progress value.

With that in place, we went for the big kill, updating the UI and letting the system redraw.

With the ViewPager linkage

Now that the most important custom View is ready, it’s time to test the results. So we need to know how to get the ViewPager slide progress value. Can we set the slide listener for the ViewPager

        mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}});Copy the code

The float positionOffset parameter is a progress value, but it’s a little tricky to use this progress value, as explained in the source code

        /**
         * This method will be invoked when the current page is scrolled, either as part
         * of a programmatically initiated smooth scroll or a user initiated touch scroll.
         *
         * @paramposition Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset  is nonzero. *@param positionOffset Value from [0, 1) indicating the offset from the page at position.
         * @param positionOffsetPixels Value in pixels indicating the offset from position.
         */
        void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
Copy the code

The onPageScrolled method is called when you slide, and the position parameter represents the page that is currently displayed. This can be very easy to interpret. Whether you slide from left to right or right to left, position always represents the left page. So position + 1 always represents the page on the right.

The positionOffset parameter represents the progress of the slide, and one of the important things that most people ignore is that if the positionOffset parameter is non-zero, it means that the page on the right is visible. This will show up in the code.

Now that you know the parameters, let’s look at the implementation

            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                // Animate the left View
                mTabViews.get(position).setXPercentage(1 - positionOffset);
                // If the positionOffset is not 0, then the View on the right is visible, which means that the View on the right needs to be animated
                if (positionOffset > 0) {
                    mTabViews.get(position + 1).setXPercentage(positionOffset); }}Copy the code

MTabViews is an ArrayList that holds all of our TabViews. We have four TabViews on our page. Mtabviews.get (posistion) obtains the page on the left, mTabviews.get (position + 1) obtains the page on the right.

When swiping from left to right, the positionOffset on the left page is 0 to 1, and we need the TabView on the left page to fade. However, in the TabView we designed, when the progress value reaches 1, the animation will change color, not fade, so change the progress value of the TabView on the left page by taking 1 – positionOffset. So the progress on the right side of the page is going to be positionOffset.

The principle of sliding from right to left is exactly the same as the principle of sliding from left to right, and you can see that from the Log.

However, when animating the TabView on the left, we must make sure that the page on the right exists. We said earlier that if the positionOffset is 0, the page on the right is not visible, so we have to do something to exclude it, which is reflected in the code.

Code optimization

  1. ViewPagerWhat is the critical value of progress to automatically slide to the next page?TabViewThis critical point is needed to control the transformation of transparency.
  2. TabViewOnly through the XML attributes to control the image display, control the final color color and so on, in fact, these can be dynamically controlled through the code, we can achieve an external interface.

If you are a person of excellence, you can study and implement these two points.

The end of the

This article explains the principles of animation, and how to use code to achieve these principles, these are the key parts. I don’t show you the rest of the code. I uploaded the code to Github for the convenience of those who want to see the demo. The so-called gift rose, hand leaves fragrance, if you think the code is ok, guest officer to a star or fork ~