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
是导航条标题的容器。
回头再看上面的基础用法,接下来就是将CommonNavigatorAdapter
中getTitleView()
返回的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); } } }
以上就是整个项目的原理介绍,项目整体思路还是非常清晰的,细节部分做了封装处理,然后暴露出来一些方法做定制化开发。这种思想在我们日常工作中也经常有遇到,将类似的业务功能点尽量都抽象成一样的架构,然后具体的业务实现在架构的基础上做定制化的开发。