preface

Recently, I am making a requirement about the bottom navigation bar. The result is as follows:

The icon is implemented using Lottie animation, which can be backtraced, that is, when the fragment is switched, the animation can be loaded halfway and returned, which is easy to implement using Lottie animation.

Then there is the text color, which is also gradient based on sliding and can be backtracked. I wanted to optimize this effect and make it into a component, but I found that there are still some details to consider. In order to reduce the number of pits, I am going to study a well-known indicator framework I used before: MagicIndicator.

The open source code library is: github.com/hackware199…

The renderings are as follows

So these are some of the MagicIndicator effects that are very powerful, so let’s see how it works.

The body of the

Start simple and analyze what needs to be done because there is so much source code that you have to start with a problem. Let’s look at the following:

From here we can briefly list a few issues that need to be addressed:

With the problem, let’s look at the source code again, so that there will be ideas.

MagicIndicator

Look at the XML layout:

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/navigator_margin_top"
    android:background="#455a64"
    android:orientation="vertical">

    <net.lucode.hackware.magicindicator.MagicIndicator
        android:id="@+id/magic_indicator1"
        android:layout_width="wrap_content"
        android:layout_height="@dimen/common_navigator_height"
        android:layout_gravity="center_horizontal" />

</LinearLayout>
Copy the code

The three tabs in the above illustration are a custom View. The advantage of this design is that you can place an infinite number of tabs. In this case, you must have an adapter to provide TAB text, number, and indicator appearance.

MagicIndicator magicIndicator = (MagicIndicator) findViewById(R.id.magic_indicator1); CommonNavigator = new CommonNavigator(this); / / navigation instance adapter commonNavigator setAdapter (new CommonNavigatorAdapter () {/ / need to know how many TAB items @ Override public int getCount () { return mDataList == null ? 0 : mDataList.size(); } @override public IPagerTitleView getTitleView(Context Context, final int index) {public IPagerTitleView getTitleView(Context Context, final int index) { Omit the return simplePagerTitleView; } @override public IPagerIndicator getIndicator(Context Context) {LinePagerIndicator = new LinePagerIndicator(context); indicator.setColors(Color.parseColor("#40c4ff")); return indicator; }}); . / / the navigator set up the adapter magicIndicator setNavigator (commonNavigator);Copy the code

The logic for this is the same as for the normal setup adapter. The next step is to combine the MagicIndicator with the ViewPager:

ViewPagerHelper.bind(magicIndicator, mViewPager);
Copy the code

This is just one line of code, using the ViewPagerHelper class.

We can’t help but see that a lot of the logic is in the CommonNavigator. Let’s look at the MagicIndicator code:

Public class MagicIndicator extends FrameLayout {private IPagerNavigator mNavigator; public MagicIndicator(Context context) { super(context); } public MagicIndicator(Context context, AttributeSet attrs) { super(context, attrs); } //ViewPager public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (mNavigator ! = null) { mNavigator.onPageScrolled(position, positionOffset, positionOffsetPixels); }} //ViewPager select callback, public void onPageSelected(int position) {if (mNavigator! = null) { mNavigator.onPageSelected(position); }} public void onPageScrollStateChanged(int state) {if (mNavigator! = null) { mNavigator.onPageScrollStateChanged(state); } } public IPagerNavigator getNavigator() { return mNavigator; Public void setNavigator(IPagerNavigator) {if (mNavigator == navigator) {return; } if (mNavigator ! = null) { mNavigator.onDetachFromMagicIndicator(); } mNavigator = navigator; removeAllViews(); // The navigator is actually a View, Add View to FrameLayout if (mNavigator instanceof View) {LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); addView((View) mNavigator, lp); mNavigator.onAttachToMagicIndicator(); }}}Copy the code

The MagicIndicator is just a bridge. The specific View is implemented in the IPagerNavigator, and the MagicIndicator passes the sliding state of the ViewPager to the IPagerNavigator.

So it’s worth taking a look at the ViewPagerHelper class:

Public class ViewPagerHelper {public static void bind(final MagicIndicator) {public static void bind(final MagicIndicator magicIndicator, ViewPager viewPager) { viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { magicIndicator.onPageScrolled(position, positionOffset, positionOffsetPixels); } @Override public void onPageSelected(int position) { magicIndicator.onPageSelected(position); } @Override public void onPageScrollStateChanged(int state) { magicIndicator.onPageScrollStateChanged(state); }}); }}Copy the code

So when you see the code, it looks exactly as we expected. General relation of class:

I have a question here, which is that when you swipe the ViewPager state is passed to the MagicIndicator, and the tabView can do that, but when you click on the tabView, how does the ViewPager switch, where does it do that, it’s actually done in getting the title View, Take a look at the getTitleView method in the IPagerNavigator adapter above:

@Override public IPagerTitleView getTitleView(Context context, final int index) { SimplePagerTitleView simplePagerTitleView = new ColorTransitionPagerTitleView(context); simplePagerTitleView.setText(mDataList.get(index)); simplePagerTitleView.setNormalColor(Color.parseColor("#88ffffff")); simplePagerTitleView.setSelectedColor(Color.WHITE); / / here directly set the click event to switch ViewPager simplePagerTitleView. SetOnClickListener (new View. An OnClickListener () {@ Override public void onClick(View v) { mViewPager.setCurrentItem(index); }}); return simplePagerTitleView; }Copy the code

Ok, the first problem has been solved, which is that switching viewPager when clicking TAB is handled manually in getTitleView.

Now that you know the general architecture, you can mainly take a look at the IPagerNavigator implementation class, the following is the CommonNavigator, is also the source code used the most simple one IPagerNavigator.

CommonNavigator

This is the Navigator in the above illustration, normally a MagicIndicator corresponds to a Navigator, because it is very simple, the Navigator is also a View, so the main logic here is focused on how to add title View and indicator View, It’s all implemented here.

public class CommonNavigator extends FrameLayout implements IPagerNavigator
        , NavigatorHelper.OnNavigatorScrollListener 
Copy the code

There are two interfaces implemented here, so let’s look at them.

IPagerNavigator

So this is the main navigator interface,

Public interface IPagerNavigator {// ViewPager void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); void onPageSelected(int position); void onPageScrollStateChanged(int state); // ** * when IPagerNavigator is added to MagicIndicator */ void onAttachToMagicIndicator(); / * * * removed from MagicIndicator when IPagerNavigator calls when * / void onDetachFromMagicIndicator (); /** * This method needs to be called first when the ViewPager content changes. Custom IPagerNavigator should obey this convention */ void notifyDataSetChanged(); }Copy the code

The main ones are MagicIndicator to pass ViewPager status to the navigator, so 3 callbacks from the ViewPager are required, as well as add, delete, and update the navigator callbacks.

NavigatorHelper.OnNavigatorScrollListener

What is this? Look at the code:

public interface OnNavigatorScrollListener {
    void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight);

    void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight);

    void onSelected(int index, int totalCount);

    void onDeselected(int index, int totalCount);
}
Copy the code

It’s a little ridiculous. This is a change from 3 callbacks in ViewPager to 4 callbacks, and the reason for this is very simple. The previous callbacks in ViewPager didn’t work very well, so we changed them to 4 callbacks.

Add the view

So far we have split the two ways, first looking at how to add a View to the Navigator, and then looking at how to associate with the ViewPager.

Take a look at the init code in CommonNavigator:

Private void init() {// Remove all view removeAllViews(); View root; Adjustmode (mAdjustMode) {root = adjustMode LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout_no_scroll, this); } else { root = LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout, this); } // This is the title container mTitleContainer = (LinearLayout) root.findViewById(R.id.testle_container); mTitleContainer.setPadding(mLeftPadding, 0, mRightPadding, 0); // mIndicatorContainer = (LinearLayout) root.findViewById(R.iddicator_container); // mIndicatorContainer = (LinearLayout) root.findViewById(R.iddicator_container); if (mIndicatorOnTop) { mIndicatorContainer.getParent().bringChildToFront(mIndicatorContainer); } // initialize initTitlesAndIndicator(); }Copy the code

Take a look at what the rootView looks like here:

<? The XML version = "1.0" encoding = "utf-8"? > <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/indicator_container" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" /> <LinearLayout android:id="@+id/title_container" android:layout_width="match_parent" android:layout_height="match_parent"  android:orientation="horizontal" /> </FrameLayout>Copy the code

Surprisingly, there are only two views, one for the title and one for the indicator,

The second problem is that the layout is made up of two layout containers, separated by headings and indicators.

Therefore, it is crucial to make the two containers as linked as a View. The main code is how to add views to the two containers:

Private void initTitlesAndIndicator() {// NavigatorHelper is a helper class. Save some of the information for (int I = 0, j = mNavigatorHelper getTotalCount (); i < j; i++) { IPagerTitleView v = mAdapter.getTitleView(getContext(), i); If (v instanceof View) {View View = (View) v; if (v instanceof View) {View View = (View) v; LinearLayout.LayoutParams lp; if (mAdjustMode) { lp = new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT); lp.weight = mAdapter.getTitleWeight(getContext(), i); } else { lp = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); } mTitleContainer.addView(view, lp); } } if (mAdapter ! MIndicator = madapter.getIndicator (getContext()); mIndicator = madapter.getIndicator (getContext()); if (mIndicator instanceof View) { LayoutParams lp = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); mIndicatorContainer.addView((View) mIndicator, lp); }}}Copy the code

Since adding a View is so simple, the complex logic must be encapsulated in a specific TitleView. Let’s analyze a wave of TitleViews.

IPagerTitleView

This is the interface that all TitleViews inherit from. Here’s the key. Take a look at the interface:

Public interface IPagerTitleView {/** * selected */ void onSelected(int index, int totalCount); /** * unselected */ void onDeselected(int index, int totalCount); /** * @param leftToRight from leftToRight */ void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight); /** * enter ** @param enterPercent, 0.0f - 1.0f * @param leftToRight from leftToRight */ void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight); }Copy the code

It is very easy to understand the selected and unselected, there is a leave is what it is, and there is a percentage and direction, here is to do the special effects of the text, used to do the animation progress, this is very key, here is an example:

It will be found that when the ViewPager slides left and right, the text in the first row will become larger and smaller, and there will be color gradient at the same time. Instead of discussing how to know the sliding progress, the animation can be realized only by knowing that I know the sliding progress, i.e., Percent and which is entering and leaving. The direction is to design the effect realization of the second row.

The TextView in the middle of the second row, when both of them are in the leave state, is different from the TextView in the right state, and the color of the text changes from one to the left and one to the right, so we also need to know the direction.

Here we have a general idea of why text headers are implemented, and then we have a look at the most common text implementation. The first is text color change, which I have already mentioned many times in previous articles:

public class ColorTransitionPagerTitleView extends SimplePagerTitleView {

    public ColorTransitionPagerTitleView(Context context) {
        super(context);
    }

    @Override
    public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) {
        int color = ArgbEvaluatorHolder.eval(leavePercent, mSelectedColor, mNormalColor);
        setTextColor(color);
    }

    @Override
    public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) {
        int color = ArgbEvaluatorHolder.eval(enterPercent, mNormalColor, mSelectedColor);
        setTextColor(color);
    }

    @Override
    public void onSelected(int index, int totalCount) {
    }

    @Override
    public void onDeselected(int index, int totalCount) {
    }
}
Copy the code

Calculate the difference of 2 colors according to and progress, and then change the size:

Public class ScaleTransitionPagerTitleView extends ColorTransitionPagerTitleView {private float mMinScale = 0.75 f; public ScaleTransitionPagerTitleView(Context context) { super(context); } @Override public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) { super.onEnter(index, totalCount, enterPercent, leftToRight); SetScaleX (mMinScale + (1.0f-mminscale) * enterPercent); SetScaleY (mMinScale + (1.0f-mminscale) * enterPercent); } @Override public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) { super.onLeave(index, totalCount, leavePercent, leftToRight); SetScaleX (1.0f + (mminScale-1.0f) * leavePercent); SetScaleY (1.0F + (mminscale-1.0F) * leavePercent); } public float getMinScale() { return mMinScale; } public void setMinScale(float minScale) { mMinScale = minScale; }}Copy the code

These two kinds of the simplest animation will not do too much description, know the principle of which can be, about the complex point of the text color change left and right, later to say separately.

Now that we know how the titles are arranged and how they change depending on the viewPager switch, let’s look at the indicators.

IPagerIndicator

It’s a little bit more complicated for indicators, but the reason is simple, the title is multiple views added one by one to the container, but the indicator is just a view, and it has to be able to slide along with the ViewPager, and slide and animate, so there’s a little bit more to think about, and we’re not going to talk about the ViewPager slide callback here, but we’ll talk about it later, Take a look at the interface to the indicator:

public interface IPagerIndicator {
    void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);

    void onPageSelected(int position);

    void onPageScrollStateChanged(int state);

    void onPositionDataProvide(List<PositionData> dataList);
}
Copy the code

The first three methods are easy to understand, which are the callbacks of the ViewPager switch. The fourth method records the position of the title so that the indicator can move around. Take a look at PositionData:

Public class PositionData {//TextView's top, bottom, left, right coordinates public int mLeft; public int mTop; public int mRight; public int mBottom; // The coordinate of the content of the TextView, which is to remove the padding public int mContentLeft; public int mContentTop; public int mContentRight; public int mContentBottom; Public int width() {return mright-mleft; } public int height() { return mBottom - mTop; } public int contentWidth() {return mContentRight - mContentLeft; } public int contentHeight() { return mContentBottom - mContentTop; } public int horizontalCenter() {return mLeft + width() / 2; } public int verticalCenter() { return mTop + height() / 2; }}Copy the code

Now, the reason why I want to make this distinction here is very simple, because the width of the indicator is definable, like the width is the same as the content of the TextView,

The width is the width of the TextView,

The width is custom small,

So with the PositionData above, the data width problem is solved. Now let’s look at how to move the indicators.

LinePagerIndicator

A line indicator is a line, and you can probably guess how it works by controlling the size and position of the View based on how the ViewPager slides.

The code is as follows:

@override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (mPositionDataList == null || mPositionDataList.isEmpty()) { return; }... Slightly / / calculate the anchor position PositionData current = FragmentContainerHelper. GetImitativePositionData (mPositionDataList, position); PositionData next = FragmentContainerHelper.getImitativePositionData(mPositionDataList, position + 1); Float leftX; float leftX; float leftX; float nextLeftX; float rightX; float nextRightX; if (mMode == MODE_MATCH_EDGE) { leftX = current.mLeft + mXOffset; nextLeftX = next.mLeft + mXOffset; rightX = current.mRight - mXOffset; nextRightX = next.mRight - mXOffset; } else if (mMode == MODE_WRAP_CONTENT) { leftX = current.mContentLeft + mXOffset; nextLeftX = next.mContentLeft + mXOffset; rightX = current.mContentRight - mXOffset; nextRightX = next.mContentRight - mXOffset; } else { // MODE_EXACTLY leftX = current.mLeft + (current.width() - mLineWidth) / 2; nextLeftX = next.mLeft + (next.width() - mLineWidth) / 2; rightX = current.mLeft + (current.width() + mLineWidth) / 2; nextRightX = next.mLeft + (next.width() + mLineWidth) / 2; } // 4 vertex attributes of the line indicator // plus animation, Can produce better result mLineRect. Left = leftX + (nextLeftX - leftX) * mStartInterpolator getInterpolation (positionOffset); mLineRect.right = rightX + (nextRightX - rightX) * mEndInterpolator.getInterpolation(positionOffset); mLineRect.top = getHeight() - mLineHeight - mYOffset; mLineRect.bottom = getHeight() - mYOffset; invalidate(); }Copy the code

We can then redraw the rect in onDraw(), but without going into that, we can say a little bit about animation, which we’ve covered in detail in previous articles, especially in Bezier curves, using nonlinear interpolators to get a better look,

This animation is not the default linear animation, it’s non-linear,

public IPagerIndicator getIndicator(Context context) { LinePagerIndicator indicator = new LinePagerIndicator(context); / / to accelerate animation indicator. SetStartInterpolator (new AccelerateInterpolator ()); / / slow animation indicator. SetEndInterpolator (new DecelerateInterpolator (1.6 f)); indicator.setYOffset(UIUtil.dip2px(context, 39)); indicator.setLineHeight(UIUtil.dip2px(context, 1)); indicator.setColors(Color.parseColor("#f57c00")); return indicator; }Copy the code

Here deceleration animation has a value of 1.6, in the previous Bezier curve QQ little red dot said, there is a website can debug animation is:

Inloop. Making. IO/interpolato…

In this website, you can find the right factor to draw a more beautiful animation.

NavigatorHelper

In NavigatorHelper, we have three callbacks in ViewPager. We have three callbacks in ViewPager. We have three callbacks in ViewPager.

viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { Log.i("zyh", "onPageScrolled: position = " + position); Log.i("zyh", "onPageScrolled: positionOffset = " + positionOffset); Log.i("zyh", "onPageScrolled: positionOffsetPixels = " + positionOffsetPixels); magicIndicator.onPageScrolled(position, positionOffset, positionOffsetPixels); } @Override public void onPageSelected(int position) { magicIndicator.onPageSelected(position); Log.i("zyh", "onPageSelected: position = " + position); } @Override public void onPageScrollStateChanged(int state) { magicIndicator.onPageScrollStateChanged(state); Log.i("zyh", "onPageScrollStateChanged: state = " + state); }});Copy the code

For example, here I slide from position 0 to 1, where the value change in the onPageScrolled callback is:

position: 0 -> 0 -> 0 …. – > 1

positionOffset: 0 -> 1

But IF I slide 0 from position 1, the change in value here is 0

position: 1 -> 0 -> 0 …. – > 0

positionOffset: 1 -> 0

So it’s important to remember that this position is always the same position as the TAB on the left.

Then it is a matter of logic to convert the above three callbacks into the following four callbacks:

public interface OnNavigatorScrollListener {
    void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight);

    void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight);

    void onSelected(int index, int totalCount);

    void onDeselected(int index, int totalCount);
}
Copy the code

Specific code is not detailed, you can go to the source code to see.

At the end

This article only introduces the basic principle, many slightly more complicated animation effects will be useful later, in fact, after understanding the principle, other animation implementation will not be troublesome.

Summarize the following points in our daily use of common places:

  • The slide callback method of ViewPager, in which position is always left of the page.

  • The ViewPager’s three callback methods need to be converted in use to a more user-friendly four callbacks, while recording the direction.

  • The title and indicator are separated and put into two containers respectively, and linked through callback progress.

  • Animation should be proficient, which is also the basic quality of being an Android UI Boy.