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