解析ViewPager(二)——ViewPager源码解析
生活随笔
收集整理的這篇文章主要介紹了
解析ViewPager(二)——ViewPager源码解析
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
前言
前一篇博客介紹了ViewPager的簡單使用,這篇博客主要從源碼的角度來解析ViewPager。
ViewPager的一些變量
? ? ? ViewPager是一組視圖,那么它的父類必然是ViewGroup,也就是說ViewPager繼承了ViewGroup的所有屬性。我們先看一下部分源碼:
public class ViewPager extends ViewGroup {private static final String TAG = "ViewPager";private static final boolean DEBUG = false;private static final boolean USE_CACHE = false;private static final int DEFAULT_OFFSCREEN_PAGES = 1;private static final int MAX_SETTLE_DURATION = 600; // msprivate static final int MIN_DISTANCE_FOR_FLING = 25; // dipsprivate static final int DEFAULT_GUTTER_SIZE = 16; // dipsprivate static final int MIN_FLING_VELOCITY = 400; // dipsstatic final int[] LAYOUT_ATTRS = new int[] {android.R.attr.layout_gravity};/*** Used to track what the expected number of items in the adapter should be.* If the app changes this when we don't expect it, we'll throw a big obnoxious exception.*用于監測項目中我們需要適配器的期望的頁卡數*/private int mExpectedAdapterCount;/*** 該類用于保存頁面信息*/static class ItemInfo {Object object;//頁面展示的頁卡對象int position;//頁卡下標(頁碼)boolean scrolling;//是否滾動float widthFactor;//表示加載的頁面占ViewPager所占的比例[0~1](默認返回1) ,這個值可以設置一個屏幕顯示多少個頁面float offset;//頁卡偏移量}//頁卡排序private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){@Overridepublic int compare(ItemInfo lhs, ItemInfo rhs) {return lhs.position - rhs.position; } }; //插值器:他的作用就是根據不同的時間控制滑動的速度。 private static final Interpolator sInterpolator = new Interpolator() { @Override public float getInterpolation(float t) { t -= 1.0f; return t * t * t * t * t + 1.0f; } }; //表示已經緩存的頁面信息 private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>(); private final ItemInfo mTempItem = new ItemInfo(); PagerAdapter mAdapter;//頁卡適配器 int mCurItem; // Index of currently displayed page.當前頁面的下標// Offsets of the first and last items, if known. // Set during population, used to determine if we are at the beginning // or end of the pager data set during touch scrolling. private float mFirstOffset = -Float.MAX_VALUE;//第一個頁卡的滑動偏移量 private float mLastOffset = Float.MAX_VALUE;//最后一個頁卡的滑動偏移量 。。。。。。 省略部分代碼 。。。。。。 /*** Position of the last motion event.最后頁卡滑動事件的位置*/ private float mLastMotionX; private float mLastMotionY; private float mInitialMotionX; private float mInitialMotionY; /*** ID of the active pointer. This is used to retain consistency during* drags/flings if multiple pointers are used.*/ private int mActivePointerId = INVALID_POINTER;//活動指針標示 如果使用多個指針,這用于保持拖動/ flings期間的一致性。 /*** Sentinel value for no current active pointer.* Used by {@link #mActivePointerId}.*/ private static final int INVALID_POINTER = -1;//沒有活動的當前指針的哨兵值/*** Determines speed during touch scrolling*這個速度追蹤器用于觸摸滑動時追蹤滑動速度*/ private VelocityTracker mVelocityTracker; private int mMinimumVelocity; private int mMaximumVelocity; private int mFlingDistance; private int mCloseEnough;// If the pager is at least this close to its final position, complete the scroll // on touch down and let the user interact with the content inside instead of // "catching" the flinging pager. //如果頁面至少接近它的最終位置,完成向下滾動,讓用戶與內容中的內容進行交互,而不是“捕獲”flinging頁面。 private static final int CLOSE_ENOUGH = 2; // dpprivate boolean mFakeDragging; private long mFakeDragBeginTime;private EdgeEffectCompat mLeftEdge; private EdgeEffectCompat mRightEdge;private boolean mFirstLayout = true; private boolean mNeedCalculatePageOffsets = false; private boolean mCalledSuper; private int mDecorChildCount;private List<OnPageChangeListener> mOnPageChangeListeners; private OnPageChangeListener mOnPageChangeListener; private OnPageChangeListener mInternalPageChangeListener; private List<OnAdapterChangeListener> mAdapterChangeListeners; private PageTransformer mPageTransformer; private int mPageTransformerLayerType; private Method mSetChildrenDrawingOrderEnabled;private static final int DRAW_ORDER_DEFAULT = 0; private static final int DRAW_ORDER_FORWARD = 1; private static final int DRAW_ORDER_REVERSE = 2; private int mDrawingOrder; private ArrayList<View> mDrawingOrderedChildren; private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();/*** Indicates that the pager is in an idle, settled state. The current page* is fully in view and no animation is in progress.*/ public static final int SCROLL_STATE_IDLE = 0;//空閑/*** Indicates that the pager is currently being dragged by the user.*/ public static final int SCROLL_STATE_DRAGGING = 1;//滑動/*** Indicates that the pager is in the process of settling to a final position.*/ public static final int SCROLL_STATE_SETTLING = 2;//滑動結束這段代碼中我們我們看到ViewPager繼承自ViewGroup,主要我們看上面注釋的幾個變量:
- ?mExpectedAdapterCount:這個變量用于監測項目中我們需要適配器的期望的頁卡數,如果APP改變了它,當我們不期望它的時候,會拋出一個異常!
- ItemInfo:這個內部類是用來保存頁卡信息的
- sInterpolator:插值器,它的主要作用是根據不同的時間來控制滑動速度。
- ArrayList<ItemInfo> mItems:表示已經緩存的頁面信息(通常會緩存當前顯示頁面以前當前頁面前后頁面,不過緩存頁面的數量由mOffscreenPageLimit決定)
- PagerAdapter mAdapter:頁卡適配器
- int mCurItem:當前頁面的下標
- mFirstOffset/mLastOffset ?第/最后一個頁卡的滑動偏移量
- mActivePointerId:活動指針標示如果使用多個指針,這用于保持拖動/ flings期間的一致性。
- mVelocityTracker:速度追蹤器用于觸摸滑動時追蹤滑動速度
- SCROLL_STATE_IDLE = 0:表示ViewPager處于空閑,建立狀態。 當前頁面完全在視圖中,并且沒有正在進行動畫。
- SCROLL_STATE_DRAGGING = 1:表示用戶當前正在拖動ViewPager。
- SCROLL_STATE_SETTLING = 2:表示ViewPager正在設置到最終位置。
ViewPager的幾個重要方法
1、initViewPager()
? ?initViewPager 是初始化ViewPager,其實還是比較簡單的,不難理解,源碼如下: void initViewPager() {setWillNotDraw(false);setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);setFocusable(true);final Context context = getContext();mScroller = new Scroller(context, sInterpolator);//創建Scroller對象final ViewConfiguration configuration = ViewConfiguration.get(context);//一個標準常量final float density = context.getResources().getDisplayMetrics().density;//獲取屏幕密度mTouchSlop = configuration.getScaledPagingTouchSlop();//獲取TouchSlop:系統所能識別的被認為是滑動的最小距離mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);//最小速度mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();//獲取允許執行一個fling手勢的最大速度值mLeftEdge = new EdgeEffectCompat(context);mRightEdge = new EdgeEffectCompat(context);mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density);mCloseEnough = (int) (CLOSE_ENOUGH * density);mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density);ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate());if (ViewCompat.getImportantForAccessibility(this)== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {ViewCompat.setImportantForAccessibility(this,ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);}//ViewCompat一個安卓官方實現兼容的幫助類ViewCompat.setOnApplyWindowInsetsListener(this,new android.support.v4.view.OnApplyWindowInsetsListener() {private final Rect mTempRect = new Rect();@Overridepublic WindowInsetsCompat onApplyWindowInsets(final View v,final WindowInsetsCompat originalInsets) {// First let the ViewPager itself try and consume them...final WindowInsetsCompat applied =ViewCompat.onApplyWindowInsets(v, originalInsets);if (applied.isConsumed()) {// If the ViewPager consumed all insets, return nowreturn applied;}// Now we'll manually dispatch the insets to our children. Since ViewPager// children are always full-height, we do not want to use the standard// ViewGroup dispatchApplyWindowInsets since if child 0 consumes them,// the rest of the children will not receive any insets. To workaround this// we manually dispatch the applied insets, not allowing children to// consume them from each other. We do however keep track of any insets// which are consumed, returning the union of our children's consumptionfinal Rect res = mTempRect;res.left = applied.getSystemWindowInsetLeft();res.top = applied.getSystemWindowInsetTop();res.right = applied.getSystemWindowInsetRight();res.bottom = applied.getSystemWindowInsetBottom();for (int i = 0, count = getChildCount(); i < count; i++) {final WindowInsetsCompat childInsets = ViewCompat.dispatchApplyWindowInsets(getChildAt(i), applied);// Now keep track of any consumed by tracking each dimension's min// valueres.left = Math.min(childInsets.getSystemWindowInsetLeft(),res.left);res.top = Math.min(childInsets.getSystemWindowInsetTop(),res.top);res.right = Math.min(childInsets.getSystemWindowInsetRight(),res.right);res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(),res.bottom);}// Now return a new WindowInsets, using the consumed window insetsreturn applied.replaceSystemWindowInsets(res.left, res.top, res.right, res.bottom);}});}2、onLayout()
? ?ViewPager繼承了ViewGroup那么肯定就要重寫onLayout()方法,該方法的主要作用是布局,那么當然也復寫了onMeasure()方法測量。關于View的原理可以看看View的工作原理(三)--View的Layout和Draw過程。ViewPager的子View是水平擺放的,所以在onLayout中,大部分工作的就是計算childLeft,即子View的左邊位置,而頂部位置基本上是一樣的。Viewpager的onlayout其實就根據populate()方法中計算出的當前頁面的offset來繪制當前頁面,和其他頁面.下面我們仔細去研究內部滑動源碼或者setCurrentPage源碼都可以發現實際上是調用了populate()方法。當我們需要有View更新的時候比如addView()、removeView()都會進行requestLayout()重新布局、以及invalidate()重新繪制界面。 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) {final int count = getChildCount();int width = r - l;int height = b - t;int paddingLeft = getPaddingLeft();int paddingTop = getPaddingTop();int paddingRight = getPaddingRight();int paddingBottom = getPaddingBottom();final int scrollX = getScrollX();//DecorView 數量int decorCount = 0;//首先對DecorView進行layout,再對普通頁卡進行layout,之所以先對DecorView布局,是為了讓普通頁卡(頁卡)能有合適的偏移//下面循環主要是針對DecorViewfor (int i = 0; i < count; i++) {final View child = getChildAt(i);//visibility不為GONE才layoutif (child.getVisibility() != GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();//左邊和頂部的邊距初始化為0int childLeft = 0;int childTop = 0;if (lp.isDecor) {//只針對Decor View//獲取水平或垂直方向上的Gravityfinal int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;//根據水平方向上的Gravity,確定childLeft以及paddingRightswitch (hgrav) {default://沒有設置水平方向Gravity時(左中右),childLeft就取paddingLeftchildLeft = paddingLeft;break;case Gravity.LEFT://水平方向Gravity為left,DecorView往最左邊靠childLeft = paddingLeft;paddingLeft += child.getMeasuredWidth();break;case Gravity.CENTER_HORIZONTAL://將DecorView居中擺放childLeft = Math.max((width - child.getMeasuredWidth()) / 2,paddingLeft);break;case Gravity.RIGHT://將DecorView往最右邊靠childLeft = width - paddingRight - child.getMeasuredWidth();paddingRight += child.getMeasuredWidth();break;}//與上面水平方向的同理,據水平方向上的Gravity,確定childTop以及paddingTopswitch (vgrav) {default:childTop = paddingTop;break;case Gravity.TOP:childTop = paddingTop;paddingTop += child.getMeasuredHeight();break;case Gravity.CENTER_VERTICAL:childTop = Math.max((height - child.getMeasuredHeight()) / 2,paddingTop);break;case Gravity.BOTTOM:childTop = height - paddingBottom - child.getMeasuredHeight();paddingBottom += child.getMeasuredHeight();break;}//上面計算的childLeft是相對ViewPager的左邊計算的,//還需要加上x方向已經滑動的距離scrollXchildLeft += scrollX;//對DecorView布局child.layout(childLeft, childTop,childLeft + child.getMeasuredWidth(),childTop + child.getMeasuredHeight());//將DecorView數量+1decorCount++;}}}//普通頁卡的寬度final int childWidth = width - paddingLeft - paddingRight;// Page views. Do this once we have the right padding offsets from above.//下面針對普通頁卡布局,在此之前我們已經得到正確的偏移量了for (int i = 0; i < count; i++) {final View child = getChildAt(i);if (child.getVisibility() != GONE) {final LayoutParams lp = (LayoutParams) child.getLayoutParams();//ItemInfo 是ViewPager靜態內部類,前面介紹過它保存了普通頁卡(也就是頁卡)的position、offset等信息,是對普通頁卡的一個抽象描述ItemInfo ii;//infoForChild通過傳入View查詢對應的ItemInfo對象if (!lp.isDecor && (ii = infoForChild(child)) != null) {//計算當前頁卡的左邊偏移量int loff = (int) (childWidth * ii.offset);//將左邊距+左邊偏移量得到最終頁卡左邊位置int childLeft = paddingLeft + loff;int childTop = paddingTop;//如果當前頁卡需要進行測量(measure),當這個頁卡是在Layout期間新添加新的,// 那么這個頁卡需要進行測量,即needsMeasure為trueif (lp.needsMeasure) {//標記已經測量過了lp.needsMeasure = false;//下面過程跟onMeasure類似final int widthSpec = MeasureSpec.makeMeasureSpec((int) (childWidth * lp.widthFactor),MeasureSpec.EXACTLY);final int heightSpec = MeasureSpec.makeMeasureSpec((int) (height - paddingTop - paddingBottom),MeasureSpec.EXACTLY);child.measure(widthSpec, heightSpec);}if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object+ ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth()+ "x" + child.getMeasuredHeight());//對普通頁卡進行layoutchild.layout(childLeft, childTop,childLeft + child.getMeasuredWidth(),childTop + child.getMeasuredHeight());}}}//將部分局部變量保存到實例變量中mTopPageBounds = paddingTop;mBottomPageBounds = height - paddingBottom;mDecorChildCount = decorCount;//如果是第一次layout,則將ViewPager滑動到第一個頁卡的位置if (mFirstLayout) {scrollToItem(mCurItem, false, 0, false);}//標記已經布局過了,即不再是第一次布局了mFirstLayout = false; }3,onMeasure()
? 前面,onLayout()方法中布局,用到了measure測量的結果,下面我們就來看下ViewPager的onMeasure()。該方法主要做了四件事:4、populate()
? ? ? 前面多處用到了populate()方法,下面我們就來研究一下populate()方法,看這個方法我看得有點懵逼!!!主要是之前一直沒將重點放在PagerAdapter,與PagerAdapter聯系起來后發現原來還是比較容易的,populate()方法主要的作用是:根據制定的頁面緩存大小,做了頁面的銷毀和重建。 1.更新items,將items中的內容換成當前展示頁面以及預緩存頁面。我們從下面的源碼中可以看到,這里會調用PagerAdapter的startUpdate()、instantiateItem()、destroyItem()、setPrimaryItem()、finishUpdate()等方法,基本是把PagerAdapter的所有生命周期從頭走到尾。2.計算每個items的off(偏移量),這個就是布局時onLayout()方法中起作用的。 void populate(int newCurrentItem) {ItemInfo oldCurInfo = null;if (mCurItem != newCurrentItem) {oldCurInfo = infoForPosition(mCurItem);mCurItem = newCurrentItem;}if (mAdapter == null) {//對頁卡的繪制順序進行排序,優先繪制DecorView//再按照position從小到大排序sortChildDrawingOrder();return;}//如果我們正在等待populate,那么在用戶手指抬起切換到新的位置期間應該推遲創建頁卡,// 直到滾動到最終位置再去創建,以免在這個期間出現差錯if (mPopulatePending) {if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");//對頁卡的繪制順序進行排序,優先繪制Decor View//再按照position從小到大排序sortChildDrawingOrder();return;}//同樣,在ViewPager沒有attached到window之前,不要populate.// 這是因為如果我們在恢復View的層次結構之前進行populate,可能會與要恢復的內容有沖突if (getWindowToken() == null) {return;}//回調PagerAdapter的startUpdate函數,// 告訴PagerAdapter開始更新要顯示的頁面mAdapter.startUpdate(this);final int pageLimit = mOffscreenPageLimit;//確保起始位置大于等于0,如果用戶設置了緩存頁面數量,第一個頁面為當前頁面減去緩存頁面數量final int startPos = Math.max(0, mCurItem - pageLimit);//保存數據源中的數據個數final int N = mAdapter.getCount();//確保最后的位置小于等于數據源中數據個數-1,// 如果用戶設置了緩存頁面數量,第一個頁面為當前頁面加緩存頁面數量final int endPos = Math.min(N - 1, mCurItem + pageLimit);//判斷用戶是否增減了數據源的元素,如果增減了且沒有調用notifyDataSetChanged,則拋出異常if (N != mExpectedAdapterCount) {//resName用于拋異常顯示String resName;try {resName = getResources().getResourceName(getId());} catch (Resources.NotFoundException e) {resName = Integer.toHexString(getId());}throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +" contents without calling PagerAdapter#notifyDataSetChanged!" +" Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +" Pager id: " + resName +" Pager class: " + getClass() +" Problematic adapter: " + mAdapter.getClass());}//定位到當前獲焦的頁面,如果沒有的話,則添加一個int curIndex = -1;ItemInfo curItem = null;//遍歷每個頁面對應的ItemInfo,找出獲焦頁面for (curIndex = 0; curIndex < mItems.size(); curIndex++) {final ItemInfo ii = mItems.get(curIndex);//找到當前頁面對應的ItemInfo后,跳出循環if (ii.position >= mCurItem) {if (ii.position == mCurItem) curItem = ii;break;}}//如果沒有找到獲焦的頁面,說明mItems列表里面沒有保存獲焦頁面,// 需要將獲焦頁面加入到mItems里面if (curItem == null && N > 0) {curItem = addNewItem(mCurItem, curIndex);}//默認緩存當前頁面的左右兩邊的頁面,如果用戶設定了緩存頁面數量,// 則將當前頁面兩邊都緩存用戶指定的數量的頁面//如果當前沒有頁面,則我們啥也不需要做if (curItem != null) {float extraWidthLeft = 0.f;//左邊的頁面int itemIndex = curIndex - 1;//如果當前頁面左邊有頁面,則將左邊頁面對應的ItemInfo取出,否則左邊頁面的ItemInfo為nullItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;//保存顯示區域的寬度final int clientWidth = getClientWidth();//算出左邊頁面需要的寬度,注意,這里的寬度是指實際寬度與可視區域寬度比例,// 即實際寬度=leftWidthNeeded*clientWidthfinal float leftWidthNeeded = clientWidth <= 0 ? 0 :2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth;//從當前頁面左邊第一個頁面開始,左邊的頁面進行遍歷for (int pos = mCurItem - 1; pos >= 0; pos--) {//如果左邊的寬度超過了所需的寬度,并且當前當前頁面位置比第一個緩存頁面位置小//這說明這個頁面需要Destroy掉if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {//如果左邊已經沒有頁面了,跳出循環if (ii == null) {break;}//將當前頁面destroy掉if (pos == ii.position && !ii.scrolling) {mItems.remove(itemIndex);//回調PagerAdapter的destroyItemmAdapter.destroyItem(this, pos, ii.object);if (DEBUG) {Log.i(TAG, "populate() - destroyItem() with pos: " + pos +" view: " + ((View) ii.object));}//由于mItems刪除了一個元素//需要將索引減一itemIndex--;curIndex--;ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;}} else if (ii != null && pos == ii.position) {//如果當前位置是需要緩存的位置,并且這個位置上的頁面已經存在//則將左邊寬度加上當前位置的頁面extraWidthLeft += ii.widthFactor;//mItems往左遍歷itemIndex--;//ii設置為當前遍歷的頁面的左邊一個頁面ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;} else {//如果當前位置是需要緩存,并且這個位置沒有頁面//需要添加一個ItemInfo,而addNewItem是通過PagerAdapter的instantiateItem獲取對象ii = addNewItem(pos, itemIndex + 1);//將左邊寬度加上當前位置的頁面extraWidthLeft += ii.widthFactor;//由于新加了一個元素,當前的索引號需要加1curIndex++;//ii設置為當前遍歷的頁面的左邊一個頁面ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;}}//同理,右邊需要添加緩存的頁面/*........................** ** 省略右邊添加緩存頁面代碼 ** **........................*/calculatePageOffsets(curItem, curIndex, oldCurInfo);}if (DEBUG) {Log.i(TAG, "Current page list:");for (int i = 0; i < mItems.size(); i++) {Log.i(TAG, "#" + i + ": page " + mItems.get(i).position);}}//回調PagerAdapter的setPrimaryItem,告訴PagerAdapter當前顯示的頁面mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null);//回調PagerAdapter的finishUpdate,告訴PagerAdapter頁面更新結束mAdapter.finishUpdate(this);//檢查頁面的寬度是否測量,如果頁面的LayoutParams數據沒有設定,則去重新設定好final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();lp.childIndex = i;if (!lp.isDecor && lp.widthFactor == 0.f) {// 0 means requery the adapter for this, it doesn't have a valid width.final ItemInfo ii = infoForChild(child);if (ii != null) {lp.widthFactor = ii.widthFactor;lp.position = ii.position;}}}//重新對頁面排序sortChildDrawingOrder();//如果ViewPager被設定為可獲焦的,則將當前顯示的頁面設定為獲焦if (hasFocus()) {View currentFocused = findFocus();ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null;if (ii == null || ii.position != mCurItem) {for (int i = 0; i < getChildCount(); i++) {View child = getChildAt(i);ii = infoForChild(child);if (ii != null && ii.position == mCurItem) {if (child.requestFocus(View.FOCUS_FORWARD)) {break;}}}}} }
5、setAdapter()
? 這個方法很容易理解,就是設置ViewPager所需要的適配器。我們看下面的源碼: /*** Set a PagerAdapter that will supply views for this pager as needed.** @param adapter Adapter to use*/ public void setAdapter(PagerAdapter adapter) {//如果已經設置過PagerAdapter,即mAdapter != null,做一些清理工作if (mAdapter != null) {//清除觀察者mAdapter.setViewPagerObserver(null);//回調startUpdate函數,告訴PagerAdapter開始更新要顯示的頁面mAdapter.startUpdate(this);//4如果之前保存有頁面,則將之前所有的頁面destroy掉for (int i = 0; i < mItems.size(); i++) {final ItemInfo ii = mItems.get(i);mAdapter.destroyItem(this, ii.position, ii.object);}//回調finishUpdate,告訴PagerAdapter結束更新mAdapter.finishUpdate(this);//將所有的頁面清除mItems.clear();//將所有的非Decor View移除,即將頁面移除removeNonDecorViews();//當前的顯示頁面重置到第一個mCurItem = 0;//滑動重置到(0,0)位置scrollTo(0, 0);}//保存上一次的PagerAdapterfinal PagerAdapter oldAdapter = mAdapter;//設置mAdapter為新的PagerAdaptermAdapter = adapter;//設置期望的適配器中的頁面數量為0個mExpectedAdapterCount = 0;//如果設置的PagerAdapter不為nullif (mAdapter != null) {//確保觀察者不為null,觀察者主要是用于監視數據源的內容發生變化if (mObserver == null) {mObserver = new PagerObserver();}//將觀察者設置到PagerAdapter中mAdapter.setViewPagerObserver(mObserver);mPopulatePending = false;//保存上一次是否是第一次Layoutfinal boolean wasFirstLayout = mFirstLayout;//設定當前為第一次LayoutmFirstLayout = true;//更新期望的數據源中頁面個數mExpectedAdapterCount = mAdapter.getCount();//如果有數據需要恢復if (mRestoredCurItem >= 0) {//回調PagerAdapter的restoreState函數mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);setCurrentItemInternal(mRestoredCurItem, false, true);//標記無需再恢復mRestoredCurItem = -1;mRestoredAdapterState = null;mRestoredClassLoader = null;} else if (!wasFirstLayout) {//如果在此之前不是第一次Layout//由于ViewPager并不是將所有頁面作為頁卡,// 而是最多緩存用戶指定緩存個數*2(左右兩邊,可能左邊或右邊沒有那么多頁面)//因此需要創建和銷毀頁面,populate主要工作就是這些populate();} else {//重新布局(Layout)requestLayout();}}//如果PagerAdapter發生變化,并且設置了OnAdapterChangeListener監聽器//則回調OnAdapterChangeListener的onAdapterChanged函數if (mAdapterChangeListener != null && oldAdapter != adapter) {mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);} }6、onPageScrolled()
? ? 當滾動當前頁時,將調用此方法,作為程序啟動的平滑滾動或用戶啟動的觸摸滾動的一部分。如果你重寫這個方法,你必須調用到超類實現(例如,在onPageScrolled之前的super.onPageScrolled(position,offset,offsetPixels))。這段代碼也比較好理解,就是控制ViewPager的滾動,將我們需要的內容顯示在屏幕上,比如滑動到中間時,一半是position另一半是position+1.同時這個方法也是非常重要的,我們如若改造優化ViewPager,就需要重寫該方法。/*** This method will be invoked when the current page is scrolled, either as part* of a programmatically initiated smooth scroll or a user initiated touch scroll.* If you override this method you must call through to the superclass implementation* (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled* returns.** @param position 表示當前是第幾個頁面* * @param offset 表示當前頁面移動的距離,其實就是個相對實際寬度比例值,取值為[0,1)。0表示整個頁面在顯示區域,1表示整個頁面已經完全左移出顯示區域。* @param offsetPixels 表示當前頁面左移的像素個數。*/@CallSuper protected void onPageScrolled(int position, float offset, int offsetPixels) {// Offset any decor views if needed - keep them on-screen at all times.//如果有DecorView,則需要使得它們時刻顯示在屏幕中,不移出屏幕if (mDecorChildCount > 0) {//根據Gravity將DecorView擺放到指定位置。//這部分代碼與onMeasure()方法中的原理一樣,這里就不做解釋了final int scrollX = getScrollX();int paddingLeft = getPaddingLeft();int paddingRight = getPaddingRight();final int width = getWidth();final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();if (!lp.isDecor) continue;final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;int childLeft = 0;switch (hgrav) {default:childLeft = paddingLeft;break;case Gravity.LEFT:childLeft = paddingLeft;paddingLeft += child.getWidth();break;case Gravity.CENTER_HORIZONTAL:childLeft = Math.max((width - child.getMeasuredWidth()) / 2,paddingLeft);break;case Gravity.RIGHT:childLeft = width - paddingRight - child.getMeasuredWidth();paddingRight += child.getMeasuredWidth();break;}childLeft += scrollX;final int childOffset = childLeft - child.getLeft();if (childOffset != 0) {child.offsetLeftAndRight(childOffset);}}}//分發頁面滾動事件,類似于事件的分發dispatchOnPageScrolled(position, offset, offsetPixels);//如果mPageTransformer不為null,則不斷去調用mPageTransformer的transformPage函數if (mPageTransformer != null) {final int scrollX = getScrollX();final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View child = getChildAt(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();//只針對頁面進行處理if (lp.isDecor) continue;//計算child位置final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();//調用transformPagemPageTransformer.transformPage(child, transformPos);}}//標記ViewPager的onPageScrolled函數執行過mCalledSuper = true; }
? ViewPager的重點是滑動,那么我們來看一下ViewPager的觸摸事件,我們主要看事件的攔截onInterceptTouchEvent(),以及事件的消耗onTouchEvent()。我們這里就只看onInterceptTouchEvent(),明白了這段代碼,onTouchEvent()也就很容易理解了。
7、onInterceptTouchEvent()
?關于ViewPager對于事件的攔截,我們只有當拖動ViewPager時ViewPager才會變化,也就是只有當我們拖拽ViewPager時,才會攔截該觸摸事件。 @Override public boolean onInterceptTouchEvent(MotionEvent ev) {// 觸摸動作final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;// 時刻要注意觸摸是否已經結束if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {//Release the drag.if (DEBUG) Log.v(TAG, "Intercept done!");//重置一些跟判斷是否攔截觸摸相關變量resetTouch();//觸摸結束,無需攔截return false;}// 如果當前不是按下事件,我們就判斷一下,是否是在拖拽切換頁面if (action != MotionEvent.ACTION_DOWN) {//如果當前是正在拽切換頁面,直接攔截掉事件,后面無需再做攔截判斷if (mIsBeingDragged) {if (DEBUG) Log.v(TAG, "Intercept returning true!");return true;}//如果標記為不允許拖拽切換頁面,我們就不處理一切觸摸事件if (mIsUnableToDrag) {if (DEBUG) Log.v(TAG, "Intercept returning false!");return false;}}//根據不同的動作進行處理switch (action) {//如果是手指移動操作case MotionEvent.ACTION_MOVE: {//代碼能執行到這里,就說明mIsBeingDragged==false,否則的話,在第7個注釋處就已經執行結束了//使用觸摸點Id,主要是為了處理多點觸摸final int activePointerId = mActivePointerId;if (activePointerId == INVALID_POINTER) {//如果當前的觸摸點id不是一個有效的Id,無需再做處理break;}//根據觸摸點的id來區分不同的手指,我們只需關注一個手指就好final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);//根據這個手指的序號,來獲取這個手指對應的x坐標final float x = MotionEventCompat.getX(ev, pointerIndex);//在x軸方向上移動的距離final float dx = x - mLastMotionX;//x軸方向的移動距離絕對值final float xDiff = Math.abs(dx);//與x軸同理final float y = MotionEventCompat.getY(ev, pointerIndex);final float yDiff = Math.abs(y - mInitialMotionY);if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);//判斷當前顯示的頁面是否可以滑動,如果可以滑動,則將該事件丟給當前顯示的頁面處理//isGutterDrag是判斷是否在兩個頁面之間的縫隙內移動//canScroll是判斷頁面是否可以滑動if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&canScroll(this, false, (int) dx, (int) x, (int) y)) {mLastMotionX = x;mLastMotionY = y;//標記ViewPager不去攔截事件mIsUnableToDrag = true;return false;}//如果x移動距離大于最小距離,并且斜率小于0.5,表示在水平方向上的拖動if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {if (DEBUG) Log.v(TAG, "Starting drag!");//水平方向的移動,需要ViewPager去攔截mIsBeingDragged = true;//如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPagerrequestParentDisallowInterceptTouchEvent(true);//設置滾動狀態setScrollState(SCROLL_STATE_DRAGGING);//保存當前位置mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :mInitialMotionX - mTouchSlop;mLastMotionY = y;//啟用緩存setScrollingCacheEnabled(true);} else if (yDiff > mTouchSlop) {//27.否則的話,表示是豎直方向上的移動if (DEBUG) Log.v(TAG, "Starting unable to drag!");//豎直方向上的移動則不去攔截觸摸事件mIsUnableToDrag = true;}if (mIsBeingDragged) {//跟隨手指一起滑動if (performDrag(x)) {ViewCompat.postInvalidateOnAnimation(this);}}break;}//如果手指是按下操作case MotionEvent.ACTION_DOWN: {//記錄按下的點位置mLastMotionX = mInitialMotionX = ev.getX();mLastMotionY = mInitialMotionY = ev.getY();//第一個ACTION_DOWN事件對應的手指序號為0mActivePointerId = MotionEventCompat.getPointerId(ev, 0);//重置允許拖拽切換頁面mIsUnableToDrag = false;//標記開始滾動mIsScrollStarted = true;//手動調用計算滑動的偏移量mScroller.computeScrollOffset();//如果當前滾動狀態為正在將頁面放置到最終位置,//且當前位置距離最終位置足夠遠if (mScrollState == SCROLL_STATE_SETTLING &&Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {//如果此時用戶手指按下,則立馬暫停滑動mScroller.abortAnimation();mPopulatePending = false;populate();mIsBeingDragged = true;//如果ViewPager還有父View,則還要向父View申請將觸摸事件傳遞給ViewPagerrequestParentDisallowInterceptTouchEvent(true);//設置當前狀態為正在拖拽setScrollState(SCROLL_STATE_DRAGGING);} else {//結束滾動completeScroll(false);mIsBeingDragged = false;}if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY+ " mIsBeingDragged=" + mIsBeingDragged+ "mIsUnableToDrag=" + mIsUnableToDrag);break;}case MotionEventCompat.ACTION_POINTER_UP:onSecondaryPointerUp(ev);break;}//添加速度追蹤if (mVelocityTracker == null) {mVelocityTracker = VelocityTracker.obtain();}mVelocityTracker.addMovement(ev);//只有在當前是拖拽切換頁面時我們才會去攔截事件return mIsBeingDragged; }總結
? ?ViewPager的主要原理我的理解就是,保存緩存的數組mItems的大小永遠都在[0,mOffscreenPageLimit*2+1]范圍內,我們滑動下一頁卡時,它將前一頁卡移出數組,將下一頁卡加入緩存。本來打算一片文章寫完ViewPager的結果寫的時候發現,我對ViewPager的認識還是不足,ViewPager比我想象的要強大許多。以上有什么不準確的地方,希望大家多多指正。總結
以上是生活随笔為你收集整理的解析ViewPager(二)——ViewPager源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基于javaweb+mysql的健身房健
- 下一篇: 抓取网页工具querylist的使用简介