Android从零开搞系列:自定义View(5)基本的自定义ViewPager指示器+开源项目分析(下)

时间:2022-08-30 00:27:21

开源ViewPager指示器分析

转载请注意:http://blog.csdn.net/wjzj000/article/details/53664920
本菜开源的一个自己写的Demo,希望能给Androider们有所帮助,水平有限,见谅见谅…
https://github.com/zhiaixinyang/PersonalCollect (拆解GitHub上的优秀框架于一体,全部拆离不含任何额外的库导入)
https://github.com/zhiaixinyang/MyFirstApp(Retrofit+RxJava+MVP)


开始之前,先看一波效果

Android从零开搞系列:自定义View(5)基本的自定义ViewPager指示器+开源项目分析(下)

项目里包含了特别多的效果和细节之处。在这里就只分析上图中涵盖的效果…

本项目原GitHub地址:https://github.com/hackware1993/MagicIndicator

先看一看这个Activity中包含的内容

看之前,我们来拆分这个效果
ViewPager+多个指示器+自定义的ViewPager适配器

现在分析之前-各类之间的关系

  • MagicIndicator:此类用于绑定到ViewPager监听滑动事件,然后对事件转发至CommonNavigator类之上。并且绑定在CommonNavigator中,调用CommonNavigator中的初始化方法。
  • CommonNavigator:真正加载Tab标题以及View效果的类。此类与RecyclerView类似,一定要以RecyclerView的思想去想象!(此处涉及到观察者模式。有兴趣可以查看我的另一篇博客:

  • IPagerIndicator:接口类,由CommonNavigator实现。

  • IPagerTitleView:TabView的接口类,由具体的Tab继承并实现对应方法。
  • CommonNavigatorAdapter:抽象类,CommonNavigator的设配器。

指示器的相关代码

作者在这里为了方便的使用不同的Tab效果,并没有使用我们通常的直接自定义指示器类。
而是通过一个Adapter把其中的Item作为指示器的效果。就像我们使用RecyclerView的那样,每一种Item代表一种样式。而这里也是这样的思想。

//MagicIndicator:指示器类。下文有此类的详细分析
MagicIndicator magicIndicator = (MagicIndicator) findViewById(R.id.magic_indicator1);
magicIndicator.setBackgroundColor(Color.parseColor("#d43d3d"));
//CommonNavigator:(类比RecyclerView)此类用于装填Tab的标题和指示View。
CommonNavigator commonNavigator = new CommonNavigator(this);
commonNavigator.setSkimOver(true);
int padding = UIUtil.getScreenWidth(this) / 2;
commonNavigator.setRightPadding(padding);
commonNavigator.setLeftPadding(padding);
//此写法对应我们RecyclerView中Adapter的写法,思想是Adapter中的每一个Item就是对应一个指示器的效果。这样可以很方便的替换指示器。
//此适配器在下文被拿出,详细分析。
commonNavigator.setAdapter(new CommonNavigatorAdapter() {
@Override
public int getCount() {
return mDataList == null ? 0 : mDataList.size();
}

@Override
public IPagerTitleView getTitleView(Context context, final int index) {
//这里就是自定义的View,即指示器的真正效果
ClipPagerTitleView clipPagerTitleView = new ClipPagerTitleView(context);
clipPagerTitleView.setText(mDataList.get(index));
clipPagerTitleView.setTextColor(Color.parseColor("#f2c4c4"));
clipPagerTitleView.setClipColor(Color.WHITE);
clipPagerTitleView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mViewPager.setCurrentItem(index);
}
});
return clipPagerTitleView;
}

@Override
public IPagerIndicator getIndicator(Context context) {
return null;
}
});

magicIndicator.setNavigator(commonNavigator);
//一个ViewPager的辅助类,和第一部分setViewPager的效果是一样的。
ViewPagerHelper.bind(magicIndicator, mViewPager);

MagicIndicator类

此类的分析以写在注释之中。
此类仅仅用于分发ViewPager的回调事件和初始化CommonNavigator。
View的绘制在CommonNavigator类中。(一定要记住这个类和RecyclerView极为相似)

    public class MagicIndicator extends FrameLayout {
/**
* 梳理:
* 此类的作用,用于转发各种事件。
* 最终的调用在ViewPager的监听中,我们Tab随ViewPager的变化而变化。
* ViewPagerHelper会沟通ViewPager和MagicIndicator。
* 通过ViewPager的addOnPageChangeListener()监听在合适的位置调用本类方法。
* 然后本类进行事件转发,调用CommonNavigator中的特定方法。
*/

//此类的具体实现为CommonNavigator
private IPagerNavigator mNavigator;

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

public MagicIndicator(Context context, AttributeSet attrs) {
super(context, attrs);
}
//方法回调
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;
}
//此方法传入的值为:CommonNavigator(继承IPagerNavigator)
public void setNavigator(IPagerNavigator navigator) {
//判空
if (mNavigator == navigator) {
return;
}
//如果不为空先取消关联,并移除其中所有子View,重新关联与初始化。
if (mNavigator != null) {
mNavigator.onDetachFromMagicIndicator();
}
mNavigator = navigator;
removeAllViews();
//类型判断
if (mNavigator instanceof View) {
/**
* 将mNavigator加入到自己的内部。
* mNavigator是CommonNavigator,这个类是类似RecyclerView,真正的Tab显示
* 就在其中(注意想象RecyclerView与Adapter配合实现的效果)。
*/

LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
addView((View) mNavigator, lp);
/**
* 此方法最终会调用CommonNavigator的初始化方法,完成对CommonNavigator的布局加载。
* 此布局是一个垂直排放的俩个线性布局。也就是上边的显示文字效果,下边是导航指示。
*/

mNavigator.onAttachToMagicIndicator();
}
}
}

CommonNavigator类

此类的解析已经写在注释之中。
此类比较完善且复杂所以这里只摘取其中部分。

public void setAdapter(CommonNavigatorAdapter adapter) {
//判空以及取消注册并重新注册
if (mAdapter == adapter) {
return;
}
if (mAdapter != null) {
mAdapter.unregisterDataSetObserver(mObserver);
}
mAdapter = adapter;
if (mAdapter != null) {
mAdapter.registerDataSetObserver(mObserver);
mNavigatorHelper.setTotalCount(mAdapter.getCount());
if (mTitleContainer != null) {
// adapter改变时,应该重新init,但是第一次设置adapter不用,onAttachToMagicIndicator中有init
mAdapter.notifyDataSetChanged();
}
} else {
mNavigatorHelper.setTotalCount(0);
init();
}
}

初始化自身布局

/**
* 布局加载,初始化各个控件
* 布局效果:一个可以水平滑动的Layout然后垂直排放俩个线性布局。
*/

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);
}
// mAdjustMode为true时,mScrollView为null
mScrollView = (HorizontalScrollView) root.findViewById(R.id.scroll_view);

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);
}
//初始化title和indicator
initTitlesAndIndicator();
}

初始化标题和指示效果

private void initTitlesAndIndicator() {
for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
/**
* 最终我们会在Activity之中通过CommonNavigator.setAdapter来new一个CommonNavigatorAdapter
* 因此会实现对应的方法,而在对应的方法中我们会初始化继承IPagerTitleView的
* 自定义的View,也就是我们各式各样的适配器。
* 此处是标题View的添加,下面是指示条的添加。二者对应布局中的俩个线性布局
*/

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);
}
}
}

PagerView滚动后,于此对应的Tab滑动

    @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);
}

// 手指跟随滚动
if (mScrollView != null && mPositionDataList.size() > 0 && position >= 0 && position < mPositionDataList.size()) {
if (mFollowTouch) {
int currentPosition = Math.min(mPositionDataList.size() - 1, position);
int nextPosition = Math.min(mPositionDataList.size() - 1, position + 1);
PositionData current = mPositionDataList.get(currentPosition);
PositionData next = mPositionDataList.get(nextPosition);
float scrollTo = current.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
float nextScrollTo = next.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
mScrollView.scrollTo((int) (scrollTo + (nextScrollTo - scrollTo) * positionOffset), 0);
} else if (!mEnablePivotScroll) {
// TODO 实现待选中项完全显示出来
}
}
}
}

此类着实比较复杂,一俩句话真心理不全,因为作者考虑了拓展问题。因为引入了不少的类。功能使用方便很强势,但是着实不好理解。
但是总结一下,这类用于真正接受标题和指示器效果然后绘制到自身的布局之上。
并且回调相关方法,完成ViewPager的滚动效果。是自身的Tab也能够完成移动。


ClipPagerTitleView类

相关分析写在注释。代码比较长,摘取部分的。
自定义的View类,在这里我们只需要根据自己的需求,在对应通用接口中完成效果即可。

    /**
* 重写onMeasure(),使其可以支持wrap_content以及padding效果。
* 固定解决方案。
*/

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureTextBounds();
setMeasuredDimension(measureWidth(widthMeasureSpec), measureHeight(heightMeasureSpec));
}
//使其可以支持wrap_content以及padding效果。
private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int result = size;
switch (mode) {
case MeasureSpec.AT_MOST:
int width = mTextBounds.width() + getPaddingLeft() + getPaddingRight();
result = Math.min(width, size);
break;
case MeasureSpec.UNSPECIFIED:
result = mTextBounds.width() + getPaddingLeft() + getPaddingRight();
break;
default:
break;
}
return result;
}
    @Override
protected void onDraw(Canvas canvas) {
int x = (getWidth() - mTextBounds.width()) / 2;
int y = (getHeight() + mTextBounds.height()) / 2;

// 画底层
mPaint.setColor(mTextColor);
canvas.drawText(mText, x, y, mPaint);

// 画clip层
canvas.save(Canvas.CLIP_SAVE_FLAG);
if (mLeftToRight) {
canvas.clipRect(0, 0, getWidth() * mClipPercent, getHeight());
} else {
canvas.clipRect(getWidth() * (1 - mClipPercent), 0, getWidth(), getHeight());
}
mPaint.setColor(mClipColor);
canvas.drawText(mText, x, y, mPaint);
canvas.restore();
}

  • Ok,最后再梳理一下大概的流程。首先由一个设计上类似RecyclerView的CommonNavigator类,负责Tab的加载以及滑动Tab已达到和ViewPager同步的效果。
  • MagicIndicator类用于分发ViewPager的滚动事件到CommonNavigator上。
  • 当然为了更好的拓展性,作者抽象了很多接口,抽象类以及Helper类。这其中的乐趣需要我们自己去体会。

PS:相关源码基本都存放于我的这个开源项目之中:
https://github.com/zhiaixinyang/PersonalCollect


最后希望各位看官可以star我的GitHub,三叩九拜,满地打滚求star:
https://github.com/zhiaixinyang/PersonalCollect
https://github.com/zhiaixinyang/MyFirstApp