前言,這個是用到了自定義控件的源碼分析,感覺裏面有很多基礎的點,有時間覺得這些內容很簡單,但是讓自己寫卻是非常困難的,於是對源碼進行了分析,這裏只是僅僅分析,沒有自己去實現,希望看到這篇文章的同學,如果感興趣,最好自己去實現一遍。
簡單了畫一個類圖,作爲開篇,希望對後面的閱讀有所幫助。
公共類
傳輸類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,這三個方法實現了對 文本和標籤的數據抽象,
5.3 在設置adapter時,是在CommonNavigator的setAdapter方法中實現的,可以在這裏更新控件,並且DataObserver是唯一的,每次都是取消註冊上一個,換成新的adapter。
到這裏就結束了,以後有新的發現,再修改。