MagicIndicator原理浅析

MagicIndicator库相信大家都用过,尤其是做移动端应用,各种酷炫的导航条大都使用这个库来开发。该库从2016年发布到现在一晃10年过去了,依稀记得当初用这个库的时候就想着有一天好好分析下源码,奈何因为自己当时年少无知以及各种拖延,导致有始无终。这两天下载了源码看了下,准备写一篇文章记录下,也算是了却10年前的心愿。

作者还是很贴心的,不仅有各种demo的详细用法,还有专门写博客介绍原理。当时我也看过这篇文章,但是就是看不太明白,没法从宏观上建立认知。我这篇博客写的没有作者详细,但是会从整体把握项目的架构。

以下是项目的基础用法:

一共4步:

1.xml中添加MagicIndicator

2.初始化CommonNavigator对象

3.设置CommonNavigator适配器

4.将CommonNavigator注入MagicIndicator

可以使用ViewPagerHelper.bind(magicIndicator, viewPager)来设置和viewPager之间的绑定关系。

各类的继承关系如下:

1.布局原理

MagicIndicator是整个库的入口

public class MagicIndicator extends FrameLayout {
    private IPagerNavigator mNavigator;

    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mNavigator != null) {
            mNavigator.onPageScrolled(position, positionOffset, positionOffsetPixels);
        }
    }

    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 navigator) {
        addView((View) mNavigator, lp);
    }
}

CommonNavigation就是导航条的容器,一共有两种类型。

不可滚动的:

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

可滚动的:

<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/scroll_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fadingEdge="none"
    android:scrollbars="none">

    <FrameLayout
        android:layout_width="wrap_content"
        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="wrap_content"
            android:layout_height="match_parent"
            android:orientation="horizontal" />

    </FrameLayout>

</HorizontalScrollView>

indicator_container是导航条指示器的容器,title_container是导航条标题的容器。

回头再看上面的基础用法,接下来就是将CommonNavigatorAdaptergetTitleView()返回的IPageTitleView加入到title_container中,将getIndicator()返回的IPagerIndicator加入到indicator_container中。

private void init() {
    removeAllViews();

    View root;
    if (mAdjustMode) {
        root = LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout_no_scroll, this);
    } else {
        root = LayoutInflater.from(getContext()).inflate(R.layout.pager_navigator_layout, this);
    }

    mScrollView = (HorizontalScrollView) root.findViewById(R.id.scroll_view);   // mAdjustMode为true时,mScrollView为null

    mTitleContainer = (LinearLayout) root.findViewById(R.id.title_container);
    mTitleContainer.setPadding(mLeftPadding, 0, mRightPadding, 0);

    mIndicatorContainer = (LinearLayout) root.findViewById(R.id.indicator_container);
    if (mIndicatorOnTop) {
        mIndicatorContainer.getParent().bringChildToFront(mIndicatorContainer);
    }

    initTitlesAndIndicator();
}

/**
    * 初始化title和indicator
    */
private void initTitlesAndIndicator() {
    for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
        IPagerTitleView v = mAdapter.getTitleView(getContext(), i);
        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 != null) {
        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);
        }
    }
}

以上就是布局的大概情况,详细的就不展开了,本文主要目的是理清整体框架原理。

2.滚动原理

接下来重点分析ViewPager滚动的时候如何做到联动导航条做出各种酷炫的效果。

入口就是这段代码ViewPagerHelper.bind(magicIndicator, mViewPager)

public class ViewPagerHelper {
    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);
            }
        });
    }
}

ViewPager滚动的时候会触发上面3个方法的调用,进而触发Navigator对应的3个方法的调用:

class CommonNavigator {
    ......
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        if (mAdapter != null) {

            mNavigatorHelper.onPageScrolled(position, positionOffset, positionOffsetPixels);
            if (mIndicator != null) {
                mIndicator.onPageScrolled(position, positionOffset, positionOffsetPixels);
            }
            ......
            mScrollView.scrollTo((int) (scrollTo + (nextScrollTo - scrollTo) * positionOffset), 0);
            ......
        }
    }

    @Override
    public void onPageSelected(int position) {
        if (mAdapter != null) {
            mNavigatorHelper.onPageSelected(position);
            if (mIndicator != null) {
                mIndicator.onPageSelected(position);
            }
        }
    }

    @Override
    public void onPageScrollStateChanged(int state) {
        if (mAdapter != null) {
            mNavigatorHelper.onPageScrollStateChanged(state);
            if (mIndicator != null) {
                mIndicator.onPageScrollStateChanged(state);
            }
        }
    }
}

上面一看就明白,CommonNavigator因为是导航条标题和导航条指示器的容器,所以控制也直接控制着它们的滚动。

复杂点的就是使用NavigatorHelper配合滚动,这个类主要作用是将onPageScrolled()方法细化成onEnter(进入)、onLeave(离开)、onSelected(选中)、onDesselected(未选中),这样在做一些定制的滑动效果时就简单很多,只要关注这些状态就行。

class CommonNavigator {
    ......
    @Override
    public void onEnter(int index, int totalCount, float enterPercent, boolean leftToRight) {
        if (mTitleContainer == null) {
            return;
        }
        View v = mTitleContainer.getChildAt(index);
        if (v instanceof IPagerTitleView) {
            ((IPagerTitleView) v).onEnter(index, totalCount, enterPercent, leftToRight);
        }
    }

    @Override
    public void onLeave(int index, int totalCount, float leavePercent, boolean leftToRight) {
        if (mTitleContainer == null) {
            return;
        }
        View v = mTitleContainer.getChildAt(index);
        if (v instanceof IPagerTitleView) {
            ((IPagerTitleView) v).onLeave(index, totalCount, leavePercent, leftToRight);
        }
    }
}

以上就是整个项目的原理介绍,项目整体思路还是非常清晰的,细节部分做了封装处理,然后暴露出来一些方法做定制化开发。这种思想在我们日常工作中也经常有遇到,将类似的业务功能点尽量都抽象成一样的架构,然后具体的业务实现在架构的基础上做定制化的开发。