前言,这个是用到了自定义控件的源码分析,感觉里面有很多基础的点,有时间觉得这些内容很简单,但是让自己写却是非常困难的,于是对源码进行了分析,这里只是仅仅分析,没有自己去实现,希望看到这篇文章的同学,如果感兴趣,最好自己去实现一遍。
简单了画一个类图,作为开篇,希望对后面的阅读有所帮助。
公共类
传输类PositionData,这个类是用来记录TextView中屏幕中的位置的,可以实现线的偏移效果,具体代码如下
/**
* 控件的左位置
*/
public int mLeft;
public int mTop;
public int mRight;
public int mBottom;
/**
* 文字的左位置
*/
public int mContentLeft;
public int mContentTop;
public int mContentRight;
public int mContentBottom;
一、首先我们来看SimplePagerTitleView,这个就是下划线上方的文字效果的TextView,
TextView 获取左侧位置,getLeft() + getWidth() / 2 - contentWidth / 2,原理是先获取TextView左侧的位置,然后再加上文字相对TextView左侧的位置,来计算出文字位置屏蔽左侧的位置
@Override
public int getContentLeft() {
Rect bound = new Rect();
String longestString = "";
//如果包括了换行,则取换行后最长的长度
if (getText().toString().contains("\n")) {
String[] brokenStrings = getText().toString().split("\\n");
for (String each : brokenStrings) {
if (each.length() > longestString.length()) longestString = each;
}
} else {
longestString = getText().toString();
}
getPaint().getTextBounds(longestString, 0, longestString.length(), bound);
//计算出文字的宽度
int contentWidth = bound.width();
//计算出文字对于的左侧位置
return getLeft() + getWidth() / 2 - contentWidth / 2;
}
获取顶部位置,
@Override
public int getContentTop() {
Paint.FontMetrics metrics = getPaint().getFontMetrics();
float contentHeight = metrics.bottom - metrics.top;
return (int) (getHeight() / 2 - contentHeight / 2);
}
二、我们这里使用的是ColorTransitionPagerTitleView,这个是可以定义TextView的渐变颜色,on主要是有两个方法构成,一个是onEntry,一个是onLeave,在这两个方法执行时,会调用ArgbEvaluatorHolder,方法,我们来看一下这个方法的代码,通过代码可以看出,通过偏移量来不停的计算出新的颜色,实现颜色的渐变
//分解开始颜色的色值
int startA = (startValue >> 24) & 0xff;//获取透明度
int startR = (startValue >> 16) & 0xff;//获取红色
int startG = (startValue >> 8) & 0xff;//获取绿色
int startB = startValue & 0xff; //获取蓝色
//分解终止颜色的值,原理同上
int endA = (endValue >> 24) & 0xff;
int endR = (endValue >> 16) & 0xff;
int endG = (endValue >> 8) & 0xff;
int endB = endValue & 0xff;
//fraction为偏移量
int currentA = (startA + (int) (fraction * (endA - startA))) << 24;
int currentR = (startR + (int) (fraction * (endR - startR))) << 16;
int currentG = (startG + (int) (fraction * (endG - startG))) << 8;
int currentB = startB + (int) (fraction * (endB - startB));
//通过【或】运算符拼接出新的颜色
return currentA | currentR | currentG | currentB;
三、LinePagerIndicator,这个是就是文字下面的线,这个线的规则有三种模式,分别是
MODE_MATCH_EDGE、MODE_WRAP_CONTENT、MODE_EXACTLY三种模式,在onPageScrolled方法中实现线的滚动,和滚动效果
onPageScrolled解析,如果对线条移动感兴趣的小伙伴可以查看我的另一篇文章自定义控件基础之绘制可以滑动的线和可以滚动的textView,在这里详情的分析了如何自己绘制一个可以滑动的线条,以及基础点和难点讲解。
3.1 回弹效果的实现,它是通过FragmentContainerHelper的getImitativePositionData方法来实现计算是不是超出范围
// 计算锚点位置
PositionData current = FragmentContainerHelper.getImitativePositionData(mPositionDataList, position);
PositionData next = FragmentContainerHelper.getImitativePositionData(mPositionDataList, position + 1);
3.2 实现滚动效果 ,是通过修改left和right的值来实现,在这里left和rightx,来控制的,如果position选择是在当前的左边,则leftx就会变小,这时就会向左移动,如果viewpager选择向右滑,则leftx则会变大,向右移动,positionOffset这是一个向右移动的偏移量,则增加移动的速度控件,来实现平滑的移动。在这里需要注意的是,向左移动时,假设A和B,当向A移动时,这时position已经是A的位置了,所以leftX会是A的left,这时线就会向左移动,并不是去减少leftx来实现的。
mLineRect.left = leftX + (nextLeftX - leftX) * mStartInterpolator.getInterpolation(positionOffset);
mLineRect.right = rightX + (nextRightX - rightX) * mEndInterpolator.getInterpolation(positionOffset);
四、指示器、CommonNavigator,这是一个把文字、下划线、还有滚动结合起来的一个中继器,用来组合各个独立的控件,主要是由二部分构成,一个是CommonNavigator,还有一个就是
CommonNavigatorAdapter。
这个通过初始化方法init()来实现控件的数据的初始刷新,在init方法中可以实现布局的初始化,以及title和indicator的数据填充
4.1 CommonNavigator 中有一个
mPositionDataList,这个属性 是用来记录第一个title的位置的,这个方法是在
CommonNavigator的onLayout中执行的,
4.2 在Common在CommonNavigator有一个
setAdapter方法,代码如下,在这里,进行了init方法,这个方法用来初始化mTitleContainer方法的,添加了许多的title,在init中通过bringChildToFront,可以调整下滑的位置,可以放到上面,然后是调用
initTitlesAndIndicator方法,来初始化title和indicator方法,来实现子控件的值的初始化。
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();
}
4.2.2initTitlesAndIndicator的代码,这里实现了所有title类和线性布局的初始化
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);
}
}
}
4.3 onPageScrolled方法,这个方法是整个magicindicator的核心方法,是否实现移动的方法,这里有个小技巧,当我们如果判断一个数超过某个范围时,如果一直取这个值,则使用 Math.min(mPositionDataList.size() - 1, position),即可,这样子也可以实现。mIndicator.onPageScrolled,这个方法就不再介绍了,在第二步有详细说明,在这里需要看一下手指跟随滚动,他是利用scrollview来实现位置不停的实时移动的,这个技巧其实我们在工作中会经常用到的,建议熟练这个用法。
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 实现待选中项完全显示出来
}
}
}
}
4.4 在onSelected方法中,主要是二个工作,一是设置当前选择文字的颜色,二是实现文字如果遮挡则滚动到不遮挡的位置
View v = mTitleContainer.getChildAt(index);
if (v instanceof IPagerTitleView) {
((IPagerTitleView) v).onSelected(index, totalCount);
}
if (!mAdjustMode && !mFollowTouch && mScrollView != null && mPositionDataList.size() > 0) {
int currentIndex = Math.min(mPositionDataList.size() - 1, index);
PositionData current = mPositionDataList.get(currentIndex);
if (mEnablePivotScroll) {
float scrollTo = current.horizontalCenter() - mScrollView.getWidth() * mScrollPivotX;
if (mSmoothScroll) {
mScrollView.smoothScrollTo((int) (scrollTo), 0);
} else {
mScrollView.scrollTo((int) (scrollTo), 0);
}
} else {
// 如果当前项被部分遮挡,则滚动显示完全
if (mScrollView.getScrollX() > current.mLeft) {
if (mSmoothScroll) {
mScrollView.smoothScrollTo(current.mLeft, 0);
} else {
mScrollView.scrollTo(current.mLeft, 0);
}
} else if (mScrollView.getScrollX() + getWidth() < current.mRight) {
if (mSmoothScroll) {
mScrollView.smoothScrollTo(current.mRight - getWidth(), 0);
} else {
mScrollView.scrollTo(current.mRight - getWidth(), 0);
}
}
}
}
4.5 这里有一个NavigatorHelper类,这是一个CommonNavigator的补充类,主要是用来实现当整体滚动时title的字体颜色的变化,这里有一个点,就是如何实现控件是左移动还是右移动,它是通过位置和偏移量来计算的,比如当从0移动到1时,会执行三次,如果是0,这时会移动到1,执行前二次positioin是0,第三次会变成1,这时判断发生了变化,则记录是移动了。
dispatchOnLeave和dispatchOnEnter分别是移动的离开和进入的方法,在这两个方法中去实现title的变化操作。
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
//得到当前位置和偏移量的和
float currentPositionOffsetSum = position + positionOffset;
boolean leftToRight = false;
//判断是左滑还是右滑
if (mLastPositionOffsetSum <= currentPositionOffsetSum) {
leftToRight = true;
}
//判断状态是不是滚动的状态,这个状态是viewpager的状态传值进来的
if (mScrollState != ScrollState.SCROLL_STATE_IDLE) {
//如果位置没有发生变化,则返回
if (currentPositionOffsetSum == mLastPositionOffsetSum) {
return;
}
int nextPosition = position + 1;
boolean normalDispatch = true;
if (positionOffset == 0.0f) {
if (leftToRight) {
nextPosition = position - 1;
normalDispatch = false;
}
}
for (int i = 0; i < mTotalCount; i++) {
if (i == position || i == nextPosition) {
continue;
}
Float leavedPercent = mLeavedPercents.get(i, 0.0f);
if (leavedPercent != 1.0f) {
dispatchOnLeave(i, 1.0f, leftToRight, true);
}
}
//调用离开和进入的方法,来修改字体的颜色
if (normalDispatch) {
if (leftToRight) {
dispatchOnLeave(position, positionOffset, true, false);
dispatchOnEnter(nextPosition, positionOffset, true, false);
} else {
dispatchOnLeave(nextPosition, 1.0f - positionOffset, false, false);
dispatchOnEnter(position, 1.0f - positionOffset, false, false);
}
} else {
dispatchOnLeave(nextPosition, 1.0f - positionOffset, true, false);
dispatchOnEnter(position, 1.0f - positionOffset, true, false);
}
} else {
for (int i = 0; i < mTotalCount; i++) {
if (i == mCurrentIndex) {
continue;
}
boolean deselected = mDeselectedItems.get(i);
if (!deselected) {
dispatchOnDeselected(i);
}
Float leavedPercent = mLeavedPercents.get(i, 0.0f);
if (leavedPercent != 1.0f) {
dispatchOnLeave(i, 1.0f, false, true);
}
}
dispatchOnEnter(mCurrentIndex, 1.0f, false, true);
dispatchOnSelected(mCurrentIndex);
}
//完成后重新赋值
mLastPositionOffsetSum = currentPositionOffsetSum;
}
五、CommonNavigatorAdapter的实现,这个是获取控件(非绑定数据)的适配器,具体介绍如下
5.1 适配器的必备定义了一个DataSetObservable,实现了观察者模式,用来实现被观察的数据
5.2 获取实现数据的布局,和数量,在这里使用了getCount、getTitleView、getIndicator,这三个方法实现了对 文本和标签的数据抽象,
public abstract class CommonNavigatorAdapter {
private final DataSetObservable mDataSetObservable = new DataSetObservable();
public abstract int getCount();
public abstract IPagerTitleView getTitleView(Context context, int index);
public abstract IPagerIndicator getIndicator(Context context);
public float getTitleWeight(Context context, int index) {
return 1;
}
public final void registerDataSetObserver(DataSetObserver observer) {
mDataSetObservable.registerObserver(observer);
}
public final void unregisterDataSetObserver(DataSetObserver observer) {
mDataSetObservable.unregisterObserver(observer);
}
public final void notifyDataSetChanged() {
mDataSetObservable.notifyChanged();
}
public final void notifyDataSetInvalidated() {
mDataSetObservable.notifyInvalidated();
}
}
5.3 定义了一个DataSetObserver变量,这是一个观察者,当DataSetObservable执行notifyChanged()方法时,会通过DataObserver发生了变化。这时会执行DataSetObserver的onChanged()方法,这时再进行,在这里可以清空布局的控件,对获取的布局重新更新
private DataSetObserver mObserver = new DataSetObserver() {
@Override
public void onChanged() {
mNavigatorHelper.setTotalCount(mAdapter.getCount()); // 如果使用helper,应始终保证helper中的totalCount为最新
init();
}
@Override
public void onInvalidated() {
// 没什么用,暂不做处理
}
};
5.4 通过init方法引入布局,这里使用的填充布局LayoutInflater,定义一个FrameLayout,直接填充布局,完成布局的实现,
private void init(){
View root;
root= LayoutInflater.from(getContext()).inflate(R.layout.activity_imagesize,this);
}
5.5在 onLayout中计算各个控件的坐标
private void preparePositionData() {
mPositionDataList.clear();
for (int i = 0, j = mNavigatorHelper.getTotalCount(); i < j; i++) {
PositionData data = new PositionData();
View v = mTitleContainer.getChildAt(i);
if (v != null) {
data.mLeft = v.getLeft();
data.mTop = v.getTop();
data.mRight = v.getRight();
data.mBottom = v.getBottom();
if (v instanceof IMeasurablePagerTitleView) {
IMeasurablePagerTitleView view = (IMeasurablePagerTitleView) v;
data.mContentLeft = view.getContentLeft();
data.mContentTop = view.getContentTop();
data.mContentRight = view.getContentRight();
data.mContentBottom = view.getContentBottom();
} else {
data.mContentLeft = data.mLeft;
data.mContentTop = data.mTop;
data.mContentRight = data.mRight;
data.mContentBottom = data.mBottom;
}
}
mPositionDataList.add(data);
}
}
5.6 在设置adapter时,是在CommonNavigator的setAdapter方法中实现的,可以在这里更新控件,并且DataObserver是唯一的,每次都是取消注册上一个,换成新的adapter。