| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> Android ViewPager -> 正文阅读 |
|
[移动开发]Android ViewPager |
1.ViewPager ViewPager是一个允许用户左右翻转数据页的布局管理器。 ViewPager继承自ViewGroup: public class ViewPager extends ViewGroup {} ? 2.ItemInfo 先看ViewPager的一个内部类ItemInfo,它包含了一个页面的基本信息,在调用Adapter的instantiateItem方法时,在ViewPager内部就会创建这个类的对象,但它不包含view,结构如下: static class ItemInfo { ? ? Object object; //instantiateItem方法返回的对象 ? ? int position; // 页面position ? ? boolean scrolling; // 是否正在滑动 ? ? float widthFactor; // 当前页面宽度和ViewPager宽度的比例(默认是1,跟ViewPager宽度一致,可以通过重写adapter的 getPageWidth(int position) 方法自定义内容宽度) ? ? float offset; // 当前页面在所有已加载的页面中的索引(用于页面布局) } ? 3.ViewPager初始化 通常我们会在布局文件中使用ViewPager,当Activity的onCreate的setContentView方法将Xml布局设置给视图的时候,此时,ViewPager会被初始化,会按照如下方法进行初始化: ViewPager构造方法 -> initViewPager -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> draw -> onDraw 注意:以上方法有的不止调用一次,调用整体顺序按照上述步骤。 ①ViewPager构造方法: public ViewPager(Context context) { ? ? super(context); ? ? initViewPager(); } public ViewPager(Context context, AttributeSet attrs) { ? ? super(context, attrs); ? ? initViewPager(); } ViewPager在构造方法里调用了initViewPager这个初始化方法。 ②initViewPager void initViewPager() { ? ? setWillNotDraw(false);//重写onDraw需要调用,ViewGroup默认true? ? ? setDescendantFocusability( FOCUS_AFTER_DESCENDANTS);//获取焦点,孩子没有的时候才有 ? ??setFocusable(true); ? ??final Context context = getContext(); ?? ?mScroller = new Scroller(context, sInterpolator); ? ? final ViewConfiguration configuration = ViewConfiguration.get(context); ? ? final float density = context.getResources( ).getDisplayMetrics().density; ? ??mTouchSlop = configuration. getScaledPagingTouchSlop(); //滑动阈值 ? ? mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); ? ??mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); ? ? //左右边界效果 ? ? mLeftEdge = new EdgeEffect(context); ? ? mRightEdge = new EdgeEffect(context);? ? ?? ? ?//… } 在初始化方法中,首先调用了setWillNotDraw()方法,并将其设置false(ViewGroup中这个默认是true),意味着ViewPager会执行draw()的绘制方法。然后调用setDescendantFocusability( FOCUS_AFTER_DESCENDANTS),这个方法对获取焦点规则作了明确,就是在孩子都没有获取焦点的时候才获取(after descendants)。还配置了一些初始化的变量,涉及滑动的阈值、速度的范围、边界效果(EdgeEffect),以及边界的处理(ViewCompat.setOnApplyWindowInsetsListener),以及辅助功能分发配置(ViewCompat.setAccessibilityDelegate)。 ③onAttachedToWindow 初始化完成后还没有粘贴到真正的窗口Window上,所以之后会调用回调onAttachedToWindow方法。这个方法很只是重置变量mFirstLayout。 protected void onAttachedToWindow() { ? ??super.onAttachedToWindow(); ? ??mFirstLayout = true; } mFirstLayout变量是一个标记位,对是否第一次执行布局操作layout进行标记,这个时候没执行,所以重置为true。这个变量在布局方法、滚动判断和设置适配器后会用到。 然后就是View绘制三部曲onMesure、onLayout、onDraw,当然由于上边打开了允许draw调用,所以在onDraw前还会调用draw方法。 ④onMeasure 我们依次来看(记住,这个时候,我们还没有给ViewPager添加子View,所以只是在测量绘制自身),首先是onMeasure()方法: protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ? ? //测量ViewPager自身大小,根据布局文件设置尺寸信息,默认大小为0(与普通自定义ViewGroup不同,普通的会先去测量子view) ? ? setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),?getDefaultSize(0, heightMeasureSpec)); ? ??final int measuredWidth=getMeasuredWidth(); ? ? final int maxGutterSize = measuredWidth / 10; ? ? mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); ? ? //ViewPager的显示区域只能显示一个view,childWidthSize和childHeightSize为一个view的宽高大小,即去除了ViewPager的内边距后的宽高 ? ? int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); ? ? int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); ?? ?int size = getChildCount(); ?? ?for (int i = 0; i < size; ++i) { ? ? ? //由于此时没有子View,所以这个对子View的测量放在下面分析,这里省略 ? ? } ? ?//组装子View的宽高 ? ? mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); ? ? mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); ? ? //mInLayout标记是为了在计算排列子View的时候避免与添加和删除子View产生冲突 ? ? mInLayout = true; ? ??populate(); //ViewPager原理的核心关键方法 ? ??mInLayout = false; ? ? size = getChildCount(); ? ??//遍历所有不是decorView的子View去测量自己 ? ? for (int i = 0; i < size; ++i) {? ? ?? ? ?final View child = getChildAt(i); ? ? ?? ?if (child.getVisibility() != GONE) { ? ? ? ? ? ? final LayoutParams lp = (LayoutParams) child.getLayoutParams(); ? ? ? ? ? ? if (lp == null || !lp.isDecor) { ? ? ? ? ? ? ? ? final int widthSpec = MeasureSpec.makeMeasureSpec((int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); ? ? ? ? ? ? ? ? child.measure(widthSpec, mChildHeightMeasureSpec); ? ? ? ? ? ? } ? ? ? ? } ? ??} }? 这样看来,在一开始的onMeasure方法里并没有做什么,测量了下自身,并且还计算了子View的位置。由于开始的size是0,0,这个时候有了新的值,所以之后执行onSizeChanged()方法 ⑤onSizeChanged protected void onSizeChanged(int w, int h, int oldw, int oldh) { ? ? super.onSizeChanged(w, h, oldw, oldh); ? ? if (w != oldw) { ? ?? //如果两值不同,就重新计算滚动位置,这里刚初始化时就是原始位置 ? ? ?? ?recomputeScrollPosition(w, oldw, mPageMargin, mPageMargin); ? ? } } ⑥onLayout 在完成size改变的方法之后,会触发onLayout执行操作,这是三部曲里边的布局方法,在这个时候没有子View的话,就是在为自身做布局,在之后提供了子View之后,才会引发复杂的布局过程,这个时候不需要,这里先省略: 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(); ? ? int decorCount = 0; ? ? for (int i = 0; i < count; i++) { ? ?? ? //这个时候没有子View,内部布局暂时省略 ? ? } ? ??final int childWidth = width - paddingLeft - paddingRight; ? ? for (int i = 0; i < count; i++) { ? ? ? ? final View child = getChildAt(i); ? ? ? ? ? if (child.getVisibility() != GONE) { ? ? ? ? ? ? ? final LayoutParams lp = (LayoutParams) child.getLayoutParams(); ? ? ? ? ? ? ? ItemInfo ii; ? ? ? ? ? ? ? if (!lp.isDecor && (ii = infoForChild(child)) != null) { ? ? ? ? ? ? ? ? ? int loff = (int) (childWidth * ii.offset); ? ? ? ? ? ? ? ? ? int childLeft = paddingLeft + loff; ? ? ? ? ? ? ? ? ? int childTop = paddingTop; ? ? ? ? ? ? ? ? ? if (lp.needsMeasure) { ? ? ? ? ? ? ? ? ? ? ? lp.needsMeasure = false; ? ? ? ? ? ? ? ? ? ? ? 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); ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(),?childTop + child.getMeasuredHeight()); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? } ? ? mTopPageBounds = paddingTop; ? ? mBottomPageBounds = height - paddingBottom; ? ? mDecorChildCount = decorCount; ? ??if (mFirstLayout) { ? ? ? ? //在第一次布局的时候将位置滚动到当前全局变量的位置 ? ? ?? ?scrollToItem(mCurItem, false, 0, false); ? ? } ? ? ? ? //这个时候,将第一次布局设置为false,每次都重置,实际上没有必要,不过不耗时 ? ??mFirstLayout = false; } onLayout执行完成之后,就完成了布局,测量布局都完成之后,就可以绘制了。 ⑦onDraw protected void onDraw(Canvas canvas) { ?? ?super.onDraw(canvas); ?? ?// Draw the margin drawable between pages if needed. ? ? if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { ? ? ?//这个时候mItems没有添加任何item进去,所以size一定为0(从上边初始化的过程并没有对mItems进行设置,添加,所以为0) ? ??} } 初始化的全过程就分析完成了。在这个部分因为没有设置子View,所以省略了全部关于子View的部分,但是唯一一点要注意的是mFirstLayout这个时候的boolean值为false,已经完成了第一次布局。 ? 4.ViewPager显示item ViewPager初始化的过程因为没有子View,所以整个绘制过程并没有做出实质性的绘制。把容器给配置好了,现在需要添加子view了。这是最重要的一部分内容,涉及ViewPager的缓存机制,预加载机制,以及加载与销毁的时机。 在代码中如果没有调用ViewPager的setAdapter()方法,ViewPager并不会显示出子View,即使已经创建了PagerAdapter适配器。所以我们断言:ViewPager生效,触发子View加载与绘制。 下面来看源码: ①set Adapter 销毁旧的Adapter数据,用新的Adaper更新UI public void setAdapter(PagerAdapter adapter){ ? ? if (mAdapter != null) { //清除旧的Adapter,对已加载的item调用destroyItem? ? ?? ? ? ? ? mAdapter.setViewPagerObserver(null);//清除数据监听器 ? ? ? ? //?开始更新标记。如果在自定义Adapter里覆写这个方法,就可以在ViewPager正式显示子View之前干点什么事,比如在第一次加载特别耗时的情况下,显示进度条 ? ? ? ??mAdapter.startUpdate(this); ? ? ? ? //遍历销毁视图 ? ? ? ? for (int i = 0; i < mItems.size(); i++) { ? ? ? ? ? ? final ItemInfo ii = mItems.get(i); ? ? ? ? ?? ?mAdapter.destroyItem(this, ii.position, ii.object); ? ? ?? ?} ? ? ? ? mAdapter.finishUpdate(this);//结束更新 ? ? ? ??mItems.clear();//清空子View容器 ? ? ? ??//移除所有子View,除DecorView外。这个DecorView是装饰ViewPager的,默认是一旦设置了就会一直存在 ? ? ? ??removeNonDecorViews(); ? ? ? ? mCurItem = 0;//重置当前位置为0 ? ? ? ? scrollTo(0, 0);//将自身滚动到初始位置 ? ??} ? //将全局变量mAdapter保存到局部变量 ? ??final PagerAdapter oldAdapter = mAdapter; ?? ?//把新设置的adapter赋值给全局变量 ? ??mAdapter = adapter; ? ? mExpectedAdapterCount = 0; ? ?//如果设置的adapter为空,就忽视,并不报错 ? ? if (mAdapter != null) {? ? ? ? ? ? ? if (mObserver == null) { //初始化观察者 ? ? ? ?? ? ?mObserver = new PagerObserver(); ? ? ? ? }? ? ? ? ? ? ? ? ? //设置适配器数据监听,用于更新视图,外部类只能通过pagerAdapter.notifyDataSetChanged方法通知ViewPager更新视图? ? ? ? ? ? ? ? ? ? mAdapter.setViewPagerObserver( mObserver);? ? ? ? ? //mPopulatePending标志是为了避免下面populate这个方法状态冲突 ? ? ? ??mPopulatePending = false; ? ? ? ? //全局变量mFirstLayout设为局部变量 ? ?? ? ?final boolean wasFirstLayout = mFirstLayout; ? ? ? ? //此时将首次布局标志置为true。在onLayout的时候设为了false,这个时候又重置为true了 ? ? ? ? mFirstLayout = true;? ? ? ? ??? mExpectedAdapterCount = mAdapter.getCount(); //期望显示的子View的数量 ? ? ? ? if (mRestoredCurItem >= 0) { //重新保存状态,这个是关于状态值得保存,用于意外恢复数据 ? ? ? ? ?? ?mAdapter.restoreState( mRestoredAdapterState, mRestoredClassLoader); ? ? ? ? ? ? //设置当前的位置为保存的位置 ? ? ? ? ? ? setCurrentItemInternal( mRestoredCurItem, false, true); ? ? ? ? ?? ?mRestoredCurItem = -1; ? ? ? ? ?? ?mRestoredAdapterState = null; ? ? ? ? ? ? mRestoredClassLoader = null; ? ? ? ? } else if (!wasFirstLayout) { ? ? ? ? ? ? populate();//计算并初始化View ? ? ?? ?} else {?? ? ? ? ? ? ? requestLayout();? //重新测量重新布局 ? ?? ? ?} ? ??} ? ? //通知适配器观察者,ViewPager的适配器更改了 ?? ?if (mAdapterChangeListeners != null && !mAdapterChangeListeners.isEmpty()) { ? ? ? ??for (int i = 0, count = mAdapterChangeListeners.size(); i < count; i++) { ? ? ? ? ? ? mAdapterChangeListeners.get(i ).onAdapterChanged(this, oldAdapter, adapter); ? ? ? ??} ? ??} } 这个方法的主要作用就是: 1)清除旧的Adapter,对已加载的item调用destroyItem 2)将自身滚动到初始位置this.scrollTo(0, 0) 3)设置PagerObserver: mAdapter.setViewPagerObserver(mObserver); 4)调用populate()方法计算并初始化View 5)如果设置了OnAdapterChangeListener,进行回调 在adapter不为空的情况下进行处理(如果为空就忽视): 1)实现双向绑定:初始化PagerObserver,并且将其设置给适配器,用于客户端在数据改变的情况下,通过调用notifyDataSetChanaged方法进行数据变更。这个方法就是调用初始化的PagerObserver的onChanged方法进行,onChanged方法内部则是调用ViewPager的方法dataSetChanged,从而实现ViewPager数据变更。ViewPager里边设置Adapter,Adapter里设置了Observer,从而实现双向绑定。 2)重置变量:重置mPopulatePending变量为false,这个变量是用于在执行核心方法populate()的时候避免与滑动事件产生冲突。重置mFirstLayout为true,这个是在执行到后面条件判断的第一个条件时用到的,而当前的重置前的mFirstLayout方法会保存到局部变量wasFirstLayout里,条件判断的第二个条件正是和这个变量相关。还有重置mExpectedAdapterCount变量为实现的Adapter里getCount()方法返回的值。 3)条件判断,分情况执行:这里分了三种情况,第一种是处理意外,保存了上次的状态,这个是在ViewPager中覆写View的onSaveInstanceState和onRestoreInstanceState方法会受到影响,在save方法中保存意外关闭的重要状态值,而在重新恢复的restore方法中会将mRestoredCurItem设置为上次保存的位置,所以一旦有上次保存,mRestoredCurItem就一定不小于0,这个时候就会进入恢复流程,进入第一个条件语句,里边主要是跳转到上次保存的item位置;第二种是wasFirstLayout为false的情况,也就是在onLayout方法执行过了的时候(因为在onLayout方法中会将赋值给wasFirstLayout的全局变量mFirstLayout设置为false,标志着第一次layout的结束),这个时候会调用到核心方法populate()进行缓存计算;最后一种情况就是当setAdapter方法在onLayout方法中变量置为false之前执行了,这个时候就会requestLayout引起重新的测量和布局,所以会执行onMeasure(),onLayout()方法。而在onMeasure()方法中会调用populate()方法。 4)Adapter改变监听分发:最后一部分就是在对Adapter改变的监听,这里需要注意一点,当Adapter是第一次设置,这个时候oldAdapter为null,在监听回调那里我们可以通过null值来判断是否是第一次。 ②populate() 该方法是ViewPager非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁的页面,然后通过调用Adapter的instantiateItem和destroyItem两个方法初始化新页面和销毁不需要的页面! void populate() { ? ? ?populate(mCurItem); } 调用带参数的方法,传入的是全局变量mCurItem,第一次设置Adapter的时候mCurItem为0; void populate(int newCurrentItem) { ? ? ItemInfo oldCurInfo = null; ? ??//如果新选中的位置不是当前位置 ? ??if (mCurItem != newCurrentItem) { ? ? ? ? //获取旧元素信息 ? ?? ? ?oldCurInfo = infoForPosition(mCurItem); ? ? ? ? //更新当前视图index ? ? ? ? mCurItem = newCurrentItem; ? ? } ? ? if (mAdapter == null) { //设置的是空适配器 ? ? ?? ?sortChildDrawingOrder(); //视图位置重排 ? ? ? ? return; ? ? } ? ? //若滑动未停止,则暂停populate操作防止出现问题 ? ??if (mPopulatePending) { ? ? ? ? if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); ? ? ? ? sortChildDrawingOrder(); ? ? ? ???return; ? ? } ?? ?//若视图未依附于窗口则暂停populate操作 ? ? if (getWindowToken() == null) { ? ? ? ? return; ? ? } ? ? mAdapter.startUpdate(this); //开始更新 ? ? final int pageLimit = mOffscreenPageLimit;//获取预加载数量 ? ? final int startPos = Math.max(0, mCurItem - pageLimit); //确定绘制子View的起始位置 ? ??final int N = mAdapter.getCount();//需要显示的子View的数量 ? ??final int endPos = Math.min(N - 1, mCurItem + pageLimit);//确定绘制子View的结束位置 ? ??if (N != mExpectedAdapterCount) {//数量不一致,说明数据发生变化,但是没有提示ViewPager更新,抛出异常 ? ? ? ??throw new IllegalStateException("The application's PagerAdapter changed the adapter's?contents without calling PagerAdapter#notifyDataSetChanged!" ; ? ? } ? ? // 在内存中定位所需视图元素,若不存在则重新添加。 ? ?? int curIndex = -1; ?? ?ItemInfo curItem = null; ? ??for (curIndex = 0; curIndex < mItems.size(); curIndex++) { //在mItems中查找是否已添加该位置的视图元素 ? ? ?? ?final ItemInfo ii = mItems.get(curIndex); ? ?? ? ?if (ii.position >= mCurItem) { ? ? ? ? ? ? if (ii.position == mCurItem) curItem = ii; ? ? ? ? ?? ?break; ? ? ?? ?} ? ??}? ? ? // 若当前显示页面为空(第一次进来)则通过addNewItem调用instantiateItem加载 ? ? if (curItem == null && N > 0) { ? ? ? ? curItem = addNewItem(mCurItem, curIndex); ? ? } ? ? if (curItem != null) { ? ? ? ? float extraWidthLeft = 0.f; ? ? ? ? // 当前视图左边的元素位置 ? ? ? ? int itemIndex = curIndex - 1; ? ? ?? ?ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; ? ? ? ? //获取子View的可用宽大小,即viewPager测量宽度-内边距 ? ? ? ? final int clientWidth = getClientWidth(); ? ?? ? ?// 计算左侧预加载视图宽度 ? ?? ? ?final float leftWidthNeeded = clientWidth <= 0 ? 0 :?2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; ? ? ? ? // 遍历当前视图左边的所有元素 ? ? ? ? for (int pos = mCurItem - 1; pos >= 0; pos--) { ? ? ? ? ? ? // 若该元素在预加载范围外 ? ? ? ? ?? ?if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { ? ? ? ? ? ? ? ? if (ii == null) { ? ? ? ? ? ? ?? ? ? ?break; ? ? ? ? ? ? ?? ?} ? ? ? ? ? ? ?? ?//移除该页面元素,销毁视图 ? ? ? ? ? ?? ? ?if (pos == ii.position && !ii.scrolling) { ? ? ? ? ? ? ? ?? ? ?mItems.remove(itemIndex); ? ? ? ? ? ? ? ?? ? ?mAdapter.destroyItem(this, pos, ii.object); ? ? ? ? ? ? ? ? ?? ?itemIndex--; ? ? ? ? ? ? ? ? ?? ?curIndex--; ? ? ? ? ? ? ? ? ? ? ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; ? ? ? ? ? ? ?? ?} ? ? ? ? ?? ?} else if (ii != null && pos == ii.position) { ? ? ? ? ? ? ? ? // 若该左侧元素在内存中,则更新记录 ? ? ? ? ? ? ? ?extraWidthLeft += ii.widthFactor; ? ? ? ? ? ? ?? ?itemIndex--; ? ? ? ? ? ? ?? ?ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; ? ? ? ? ?? ?} else { ? ? ? ? ? ? ?? ?// 若该左侧元素不在内存中,则重新添加,再一次来到了addNewItem ? ? ? ? ? ?? ? ?ii = addNewItem(pos, itemIndex + 1); ? ? ? ? ? ?? ? ?extraWidthLeft += ii.widthFactor; ? ? ? ? ?? ? ? ?curIndex++; ? ? ? ? ? ? ? ? ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; ? ? ?? ? ? ?} ? ? ? ? } ? ??? ?// 来到当前视图右侧,思路大致和左侧相同 ? ? ? ? float extraWidthRight = curItem.widthFactor; ? ? ? ? itemIndex = curIndex + 1; ? ? ? ? if (extraWidthRight < 2.f) { ? ? ? ?? ? ?ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; ? ? ? ? ? ? // 计算右侧预加载视图宽度 ? ? ? ? ? ? final float rightWidthNeeded = clientWidth <= 0 ? 0 : (float) getPaddingRight() / (float) clientWidth + 2.f; ? ? ? ? ? ??// 遍历当前视图右边的所有元素 ? ? ? ? ? ? for (int pos = mCurItem + 1; pos < N; pos++) { ? ? ? ? ?? ? ? ?// 若该元素在预加载范围外 ? ? ? ? ? ? ? ? if (extraWidthRight >= rightWidthNeeded && pos > endPos) { ? ? ? ? ? ? ? ?? ? ?if (ii == null) { ? ? ? ? ? ? ? ? ? ?? ? ?break; ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? ? ? // 移除该页面元素 ? ? ? ? ? ? ? ? ?? ?if (pos == ii.position && !ii.scrolling) { ? ? ? ? ? ? ? ? ? ?? ? ?mItems.remove(itemIndex); ? ? ? ? ? ? ? ? ? ? ? ? mAdapter.destroyItem(this, pos, ii.object); ? ? ? ? ? ? ? ? ? ? ?? ?ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ??} else if (ii != null && pos == ii.position) { ? ? ? ? ? ? ? ? ?? // 若该右侧元素在内存中,则更新记录 ? ? ? ? ? ? ? ? ? ? extraWidthRight += ii.widthFactor; ? ? ? ? ? ? ? ? ? ? ? ? itemIndex++; ? ? ? ? ? ? ? ? ? ? ? ? ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; ? ? ? ? ? ? ? ??} else { ? ? ? ? ? ? ? ? ? ? // 若该右侧元素不在内存中,则重新添加,再一次来到了addNewItem ? ? ? ? ? ? ? ? ? ? ii = addNewItem(pos, itemIndex); ? ? ? ? ? ? ? ? ? ? itemIndex++; ? ? ? ? ? ? ? ? ? ? extraWidthRight += ii.widthFactor; ? ? ? ? ? ? ? ?? ? ?ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; ? ? ? ? ? ? ? ? } ? ? ? ?? ? ?} ? ? ?? ?} ? ? ?? ?// 计算页面偏移量 ? ? ?? ?calculatePageOffsets(curItem, curIndex, oldCurInfo); ? ?? ? ?//设置当前选中item ? ? ? ??mAdapter.setPrimaryItem(this, mCurItem, curItem.object); ?? ?} ? ? //结束更新,如果是PagerAdapter则为空实现;如果是FragmentStatePagerAdapter或者FragmentPagerAdapter,则会调用FragmentTransaction的commitNowAllowingStateLoss方法提交fragemnt,进行detach或者attach之类的操作 ?? ?mAdapter.finishUpdate(this); ? ??// 遍历子视图,若宽度不合法则重绘 ? ??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) { ? ? ? ? ? ? //没有有效的宽度,则获取内存中保存的信息给子视图的LayoutParams ? ? ? ? ? ??final ItemInfo ii = infoForChild(child); ? ? ? ? ? ? if (ii != null) { ? ? ? ? ? ? ? ? lp.widthFactor = ii.widthFactor; ? ? ? ? ? ? ? ? lp.position = ii.position; ? ? ? ? ?? ?} ? ? ? ? } ? ? } ?? ?//重新将子视图顺序排序 ? ? sortChildDrawingOrder(); ? ? if (hasFocus()) { //如果焦点在ViewPager上 ? ? ? ? //找到焦点View,如果存在则获取它的信息 ? ? ? ? View currentFocused = findFocus(); ? ? ? ? ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; ? ??//如果没找到,或者找到的焦点view不是当前位置,则遍历元素,如果找到对应元素则请求焦点 ? ? ? ? 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) { ? ? ? ? ? ? ? ? ? ??//找到view,请求焦点 ? ? ? ? ? ? ? ? ? ? if (child.requestFocus( View.FOCUS_FORWARD)){ ? ? ? ? ? ? ? ? ? ? ?? ?break; ? ? ? ? ? ? ? ? ?? ?} ? ? ? ? ? ? ? ??} ? ? ? ? ? ? } ? ? ?? ?} ? ? } } populate方法首先会根据newCurrentItem和mOffscreenPageLimit计算要加载的page页面,计算出startPos和endPos。然后根据startPos和endPos初始化页面ItemInfo,先从缓存里面获取,如果没有就调用addNewItem方法,同时将不需要的ItemInfo移除:mItems.remove(itemIndex),并调用mAdapter.destroyItem方法。然后设置LayoutParams参数(包括position和widthFactor),根据position排序待绘制的View列表。最后一步是获取当前显示View的焦点:child.requestFocus(View.FOCUS_FORWARD)。 populate方法很重要,所以我们总结一下,分为以下几个部分: A、特殊情况的拦截及处理 在整个populate中,计算缓存前,有四次对特殊情况的处理,有三次是在Adapter的startUpdate()标记方法之前,一次在其之后。这四种情况分别是: 1)mAdapter == null,用户还未设置适配器,这个时候停止处理并返回,这种情况是在ViewPager初始化的时候,在调用onMeasure()方法中,会调用populate()方法,而此时populate()方法的执行没有意义,避免浪费性能及其他错误,直接返回。 2)mPopulatePending == true 的情况,这个值默认是false,而且在setAdapter中也会重置为false,只有在以下两种情况会为true:一种是在onTouchEvent当手指离开屏幕的时候,另外一种是结束虚拟滑动endFakeDrag的时候,看这个两个方法,可知都是和滑动相关的。而且,从调用位置可以发现,正好是手指离开屏幕,或者模拟手指离开屏幕,而产生的fling到新的位置,这个时候避免出错,延迟绘制。 3)getWindowToken() == null,这个方法获得的token正是当前View粘贴到(attach)到的window的token,如果为空,则说明此时View树还未与window产生联系,主页面还未调用onResume方法。这个时候也不能计算缓存。 4)N != mExpectedAdapterCount ,上边刚刚在setAdapter中提到这个变量的赋值,是调用Adapter的getCount()方法,而N也是用相同的方法获得的,如果同一个方法获得的数据个数不同,那说明用户修改了数据源,而并未调用notifyDataSetChanged()方法来告知ViewPager同步,所以给开发者抛出了异常,从而让开发者意识到错误的原因并及时修改。 对特殊情况处理完就正式进入缓存计算了,缓存计算是在两个方法中进行的: mAdapter.startUpdate(this); mAdapter.finishUpdate(this); 这两个方法,是标记方法,我们可以在实现的Adapter中覆写这两个方法,来掌握时机,做一些事情… 缓存计算正式开始了———— 一个重要的缓存配置变量mOffscreenPageLimit,用来定制缓存数量。根据这个配置变量计算出startPos、endPos,也就是缓存的起始边界,边界有了就要加载Item了。 在找当前要显示的item信息的时候,首先是从容器里找。因为populate是多个地方调用,不一定是setAdapter才调用的,所以可能mItems这个变量开始有元素,那么就尝试去里边找: ?for (curIndex = 0; curIndex < mItems.size(); curIndex++) { ? ? final ItemInfo ii = mItems.get(curIndex); ?? ?if (ii.position >= mCurItem) { ? ? ? ? if (ii.position == mCurItem) curItem = ii; ? ? ? ? break; ? ? } } 如果找不到就去创建,即curItem为null: if (curItem == null && N > 0) { ? ? //关键代码,增加子View,只添加当前View ? ? curItem = addNewItem(mCurItem, curIndex); ?} 现在一个关键方法出来了,就是addNewItem(),来看源码: ItemInfo addNewItem(int position, int index) { ? ?ItemInfo ii = new ItemInfo(); ? ?ii.position = position;//赋值位置信息 ?? ii.object = mAdapter.instantiateItem(this, position);//在adapter中重写,新创建的对象 ? ?ii.widthFactor = mAdapter.getPageWidth( position);//赋值子View的宽度因子,方法可覆写的,默认是1.0f ?? if (index < 0 || index >= mItems.size()) { ? ? ? ?mItems.add(ii); //在mItems末尾依次添加 ? ?} else { ? ? ? ?mItems.add(index, ii);//添加到指定位置 ? ?} ? ?return ii; } 里边有我们熟悉的方法instantiateItem,从而证明我们创建的子View确实就是从这个方法获得的,所以,我们在实现这个方法的时候,返回什么,他这就缓存什么。 另外还调用了getPageWidth,这个方法是实现ViewPager高级功能时实现的,就是设置当前item的显示宽度,是个比例值。(这个宽度因子,不能设置2或者更大,否则就会显示异常,而如果设置太小的话,所有item能在一屏内显示的时候,就会发生鬼畜的现象) 这个时候如果我们正确设置实现了instantiateItem这个方法并返回,那么此时的curItem将不再为空,要显示的ItemInfo实现结束了,就在这个判空里边实现它的左右缓存,由于左和右实现原理相同,只是方向相反,这里只分析左实现。 for循环是实现的关键,但是在这之前有一些准备工作: 储存一个循环处理变量,用于和leftWidthNeeded比较: float extraWidthLeft = 0f; 获得当前位置的前一个位置: int itemIndex = curIndex - 1; 在mItems缓存列表中找一找,有没有符合条件的,没有返回null :? ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; 计算一下这个item的左边需要缓存多少空间(leftWidthNeeded ),是个比例关系。我们按照默认来看(即curItem.widthFactor == 1.0f),leftWidthNeeded就是1+左padding占的宽度比例值,如果我们假设padding也没有,那么就是1了。 final int clientWidth = getClientWidth(); final float leftWidthNeeded = clientWidth <= 0 ? 0 :2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; 准备工作就绪,就进入循环,循环是从当前位置减1开始的,依次递减,到0结束,也就是当前显示页的左侧,循环分三种情况(三个条件对应) 1)extraWidthLeft >= leftWidthNeeded && pos < startPos:左侧已有的extraWidthLeft大于左侧实际需要的leftWidthNeeded,并且循环到的pos比缓存左边界位置startPos要小,所以需要销毁掉pos位置的元素,同时itemIndex减1,ii变为此时itemIndex对应的元素,也就是又向左推进了一个继续循环更左侧的元素。 2)ii != null && pos == ii.position :&&符号左边是每次循环获得的ItemInfo不为空,右边是循环到的这个元素的位置和当前的循环位置相等,也就是说正在循环的这个元素ii与正在显示的元素的距离的在[0,limit]范围内,即现有的缓存mItems中已经有需要缓存的这个元素了,那就把extraWidthLeft加大,然后继续循环更左侧的。 3)第三个条件是else,也就是需要缓存的这个元素还不存在,则需要addNewItem。 刚开始,这部分可能不好理解,那我们来动态分析一下(以下示意图展示的都是mItems变化) ?第一种分析:假如现在是第一次设置Adapter,这个时候mItem除了有当前位置一个元素外,没有其他。并且,循环一次都不会执行。 按照缓存策略,默认缓存为1,右边虽然我们不分析,但是可以知道这次populate之后会缓存第二个Item。 第二种分析:滑到下一页,这时候就到了第二个页面,当前的position为1,也就是第二个位置,这个时候左侧有一个item,可以进入循环,我们发现符合第二个条件,因为ii已经缓存到Items里边了,所以不为null,并且position和pos均为0,这时我们看下符合条件我们做了什么: else if (ii != null && pos == ii.position) { ? ? ?extraWidthLeft += ii.widthFactor; ? ? ?itemIndex--; ? ? ?ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; }? 将widthFactor,给extraWidthLeft变量加上去,此时itemIndex为0,再自减1的话,就是-1,所以下边的ii为null。然后进入下次循环,发现循环变量为-1,不符合循环条件,退出循环。 第三种分析:然后来到第三页,这个时候的curIndex = 2,并且itemIndex = 1,循环可以执行,第一次循环pos=1,这个时候命中第二个条件,把宽度因子叠加到extraWidthLeft 上,将itemIndex自减,这个时候itemIndex为0了,并且也是可以从mItems里边获得到的;然后下一次循环pos=0,这个时候命中了第一个条件(extraWidthLeft >= leftWidthNeeded满足,并且pos < startPos也满足,startPos此时为1),所以看第一个条件里边做了些什么: if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { ? ? if (ii == null) { ? ?? ? ?break; ? ? } ? ? if (pos == ii.position && !ii.scrolling) { ? ? ? ? mItems.remove(itemIndex); ? ?? ? ?mAdapter.destroyItem(this, pos, ii.object); ? ? ? ? itemIndex--; ? ? ? ? curIndex--; ? ? ?? ?ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; ? ??} ?}? 首先判断ii是否空,为空就退出当前循环,不为空继续向下。又一个判断,&&左边的是相等的,假设这个时候已经结束滑动了,所以&&右边的也符合,执行判断内部逻辑,首先移除了mItems中itemIndex的位置的缓存,这个时候ItemIndex为0。然后看到了另一个熟悉的方法destroyItem(),这个方法也很重要,正是我们实现适配器的重要方法,可以在元素销毁的时候干点什么,定义销毁策略。然后是两个变量的自减,所以这个时候curIndex为1(因为mItems进行了remove操作,导致index和position错位,为了下边右缓存计算不出现问题,需要矫正这个变化,差距是1,所以这里curIndex进行了自减),itemIndex为-1。获取到的ii为null。然后循环不符合条件了,所以循环结束。 ?第四种分析:那什么时候执行第三个条件呢?这个时候在第三种分析的基础上,我们换方向,这个时候我们假设从第三页回到了第二页,这个时候由于我们之前删除了一个元素,所以我们回到之前确定curIndex的代码的地方,由于这段代码很容易让人不理解,所以这里我们不厌其烦的再copy一下: int curIndex = -1; ItemInfo curItem = null; for (curIndex = 0; curIndex < mItems.size(); curIndex++) { ? ? final ItemInfo ii = mItems.get(curIndex); ? ? if (ii.position >= mCurItem) { ? ? ? ? if (ii.position == mCurItem) curItem = ii; ? ? ? ? break; ? ? } } 就是这个for循环,因为我们在第三种分析的时候将本来mItems的第0个元素remove掉了,导致原来实际位置为1的元素,在mItems集合中,放到了第0个位置,所以我们此时要显示实际位置为1的item,循环的第一个curIndex为0的情况获取到的ii就会命中(他的位置position恰好和mCurItem一致,这个时候mCurItem为1)。所以局部变量在进入下边的左右缓存计算处理时的curIndex =0; 接着进入左右缓存,此时curIndex = 0,itemIndex = -1,所以准备就绪后的ii,为null,所以左边缓存的条件判断,前两个条件都将不符合,那么就会命中第三个条件,下面我们来看,最后一个条件的源码: else { ? ?ii = addNewItem(pos, itemIndex + 1); ? ?extraWidthLeft += ii.widthFactor; ? ?curIndex++; ? ?ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } 由于此时itemIndex为-1,而实际上应该0,所以为了纠正,这里加了个1,刚好重新获得了在第三种分析中被销毁的实际位置是0的Item。另外还需要注意一点是,由于此时curIndex是0,而实际上应该是1,所以这里又做了一次自增调整,而下边的ii是null,此时循环变量pos是0,下次循环是-1,不满足循环条件,退出循环。这里需要提醒的是,从右往左返回时,由于mItems数组会删除前边的元素,导致错位,所以,这里在第三个条件里边做了矫正。也正因为上边的矫正,才会使得下边的右边缓存的计算才不会出现错误。 第五种分析:其实第五种分析是针对开发者在代码中调用setOffscreenPageLimit,改变缓存配置变量mOffscreenPageLimit,这个值默认是1,正如我们前面四种分析的采用的缓存策略一样,但是我们想象这样一种情况,如果我们当前显示的是第四页,也就是mCurIndex = 3,这个时候我们设置了缓存配置变量为2,这个时候mItems中应该保存有序号为1,2,3,4,5,而由于默认的策略是1,导致mItems中保存的仅是序号为2,3,4三个Item元素,这时会先触发第二个条件,从缓存中获取,然后将itemIndex自减,针对左边接下来就会触发第三个条件,导致创建元素并插入到正确的位置。而如果之后又重新设置了缓存配置变量为1,这个时候针对左边缓存又需要销毁一个元素,还是会先进入第二个条件,然后进入第一个条件,进行remove。这样我们就知道了这个setOffscreenPageLimit是如何影响到ViewPager的缓存策略的。 对于缓存的分析是很重要的一块,也是一个理解的难点。接下来就是计算偏移值,调用calculatePageOffsets这个方法。该方法主要分两大块,oldCurInfo != null和oldCurInfo == null。就是初始化和滑动的两个过程,实际的逻辑取决于mCurItem这个属性变量与newCurrentItem这个参数是否相等。 先看源码: private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { ? ? final int N = mAdapter.getCount(); ?? ?final int width = getClientWidth(); ?? ?final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; ? ? --------第一部分-------- ? ??//为之后的布局修复调整偏移 ?? ?if (oldCurInfo != null) { ? ?? ? ?final int oldCurPosition = oldCurInfo.position; ?? ? ? ?// Base offsets off of oldCurInfo. ? ? ? ? if (oldCurPosition < curItem.position) { ? ? ? ? ? ? int itemIndex = 0; ? ? ? ? ? ? ItemInfo ii = null; ? ? ? ? ? ? float Offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; ? ? ? ? ? ? for(int pos = oldCurPosition + 1; pos <= curItem.position && itemIndex < mItems.size;pos++) { ? ? ? ? ? ? ? ? ii = mItems.get(itemIndex); ? ? ? ? ? ? ? ? while(pos > ii.position && itemIndex < mItems.size() - 1) { ? ? ? ? ? ? ? ? ? ? itemIndex++; ? ? ? ? ? ? ? ? ? ? ii = mItems.get(itemIndex); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ??while(pos <?ii.position) { ? ? ? ? ? ? ? ? ? ? offset += mAdapter.getPageWidth( pos) + marginOffset; ? ? ? ? ? ? ? ? ? ? pos++; ? ? ? ? ? ? ? ? } ? ? ? ? ? ?? ? ?ii.offset = offset; ? ? ? ? ? ??? ? offset += ii.widthFactor + marginOffset; ? ?? ? ?? ? } ? ? ?? ?} else if (oldCurPosition > curItem.position) { ? ? ? ? ? ?int itemIndex = mItems.size() - 1; ? ? ? ?? ? ItemInfo ii = null; ? ? ? ? ? ?float Offset = oldCurInfo.offset; ? ? ? ? ? ?for(int pos = oldCurPosition -?1; pos >= curItem.position && itemIndex >=0;pos--) { ? ? ? ? ? ? ? ? ii = mItems.get(itemIndex); ? ? ? ? ? ? ? ? while(pos <?ii.position && itemIndex>0) { ? ? ? ? ? ? ? ? ? ? itemIndex--; ? ? ? ? ? ? ? ? ? ? ii = mItems.get(itemIndex); ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ??while(pos >?ii.position) { ? ? ? ? ? ? ? ? ? ? offset -= mAdapter.getPageWidth( pos) + marginOffset; ? ? ? ? ? ? ? ? ? ? pos--; ? ? ? ? ? ? ? ? } ? ? ? ? ?? ?offset -= ii.widthFactor + marginOffset; ? ? ?? ? ? ?ii.offset = offset; ? ? ? ? } ? ??} } ? ? //初始化操作或者刷新当前页从这里开始? ? ? ? // Base all offsets off of curItem ?? ?final int itemCount = mItems.size(); ? ? float offset = curItem.offset; ?? ?int pos = curItem.position - 1; ? ? mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; ? ? mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; ? ? --------第二部分--------? ? ? ? // Previous pages前边的页,计算偏移 ? ? for (int i = curIndex - 1; i >= 0; i--, pos--) { ? ? ? ? final ItemInfo ii = mItems.get(i); ? ? ? ? while(pos > ii.position) { ? ? ? ? ? ? offset -= mAdapter.getPageWidth(pos--) + marginOffset; ? ? ? ? } ? ? ? ? offset -= ii.widthFactoor + marginOffset; ? ? ?? ?ii.offset = offset; ? ? ? ??if (ii.position == 0) mFirstOffset = offset; ?? ?} ? ? offset = curItem.offset + curItem.widthFactor + marginOffset; ? ??pos = curItem.position + 1; ? ? --------第三部分-------- ? ??// Next pages后边的页计算偏移 ? ? for (int i = curIndex + 1; i < itemCount; i++, pos++) { ? ? ? ??final ItemInfo ii = mItems.get(i); ? ? ? ? while(pos <?ii.position) { ? ? ? ? ? ? offset += mAdapter.getPageWidth(pos++) + marginOffset; ? ? ? ? } ? ? ?? ?if (ii.position == N - 1) { ? ? ? ? ? ? mLastOffset = offset + ii.widthFactor - 1; ? ? ? ? } ? ? ? ? ii.offset = offset; ? ? ? ? offset += ii.widthFactor + marginOffset; ? ??} ?? ?mNeedCalculatePageOffsets = false; } 以上代码分了三部分,第一部分是对当前item的offset偏移量进行计算,第二部分是对当前位置的左边所有item的offset偏移量依次进行计算赋值,第三部分是对当前位置的右边所有item的offset偏移量依次进行计算赋值。 有个ViewPager属性关注下:mPageMargin 这个变量是通过setPageMargin方法设置的,默认0; 如果第一次设置或者刷新当前页,即oldCurInfo == null。这个时候当前位置不变,这个当前位置不用改变,但是由于有一种情况是针对缓存策略的改变,所以即使当前位置不变,但是左右两边的偏移会有变化,所以需要重新计算。 这里我们需要注意的是,代码中出现的curIndex和pos两个变量,这里解释下: curIndex:是针对mItems的序号,是当前要显示的item在List里对应的位置。 pos:是针对ViewPager的所有Item,是真实的位置。 只有我们清楚的了解以上两点,才不会被不断的while循环搞混,而while循环只是在不断矫正list的curIndex和pos的匹配。他两虽然没有关系,但是通过mItems集合获取curIndex位置的ItemInfo,这个对象的position属性记录的正是真实位置。所以这样就建立起了联系,所以curIndex位置获取到的ItemInfo的postion属性比pos变量小,那就增大curIndex或者减小pos;反之,curIndex位置获取到的ItemInfo的postion属性比pos变量大,那就减小curIndex或者增大pos,之所以可以连续处理,是因为mItems存的item虽然对不上号,但是他们是连续的。 有了以上这个算法的基础,我们再来看calculatePageOffsets的源码,计算当前位置的偏移offset的时候先判断了下是向左滑,还是向右滑,这个是通过对比oldCurInfo的position和curItem的position的大小,分成了两种情况,里边都是根据上边的算法矫正之后,给当前的item赋上正确的offset的值。 然后第二部分和第三部分是计算左边右边的偏移量。其实都是直接算mItems里保存的item的偏移量,但是都是相对真实的第0个item开始计算的,虽然有可能缓存策略会把前边的回收,但相对位置还是按照有他计算的。举个栗子:如果缓存是1的话,当前位置是3,那么mItems里边,这时会只有实际位置2,3,4。这个时候0,1会回收,但是计算的实际偏移值offset,这三个分别是:2,3,4,而不是0,1,2。(这个offset是宽度倍数关系) 随后在calculatePageOffsets还需要关注两个属性变量: private float mFirstOffset = -Float.MAX_VALUE;//用来标记是否到达了最左边 private float mLastOffset = Float.MAX_VALUE;//用来标记是否到达了最右边 这两个变量会在这个时候做矫正,而在滑动的时候做判断 mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; mLastOffset = curItem.position == N - 1? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; 只要当前显示的不是第一个或者最后一个,他们的值会和curItem的offset设置了相应的计算关系。这个时候在滑动的时候就能很快判断是否到达了边缘,只要这两个变量不是默认值,就没有到边! 分析完calculatePageOffsets,我们返回调用他的地方(populate方法),接着向下: mAdapter.setPrimaryItem(this, mCurItem, curItem.object); 调用mAdapter.setPrimaryItem() 方法用来设置ViewPager要显示的Item信息。 在适配器创建对象的方法是instantiateItem,但是由于ViewPager的缓存策略导致创建的并不一定是要显示的,那么适配器为了解决用户回调当前显示的信息,而且是最及时的获取,就有了setPrimaryItem这个回调方法。 接着调用mAdapter.finishUpdate(this); 结束更新缓存,这是标记方法,与startUpdate成对出现。 然后就是通过一个循环,将所有子View中的ItemInfo对象的widthFactor和position属性赋值给LayoutParams。如下: lp.widthFactor = ii.widthFactor; lp.position = ii.position; 然后是一个关于转场或者叫翻页动画的方法sortChildDrawingOrder(); 代码不长: private void sortChildDrawingOrder() { ? ? //只有在设置transform的时候才会调用 ?? if (mDrawingOrder != DRAW_ORDER_DEFAULT){ ? ? ?? ?if (mDrawingOrderedChildren == null) { ? ? ? ? ? ? mDrawingOrderedChildren = new ArrayList<View>(); ? ? ? ? } else { ? ? ? ? ?? ?mDrawingOrderedChildren.clear(); ? ? ? ? } ? ? ? ? final int childCount = getChildCount(); ? ? ?? ?for (int i = 0; i < childCount; i++) { ? ? ? ?? ? ?final View child = getChildAt(i); ? ? ? ? ? ? mDrawingOrderedChildren.add(child); ? ? ?? ?} ? ? ? ? //排序,位置关系及是否是decor ?? ? ?Collections.sort(mDrawingOrderedChildren, sPositionComparator); ?? ?} } 整个方法执行的先决条件是mDrawingOrder != DRAW_ORDER_DEFAULT,只有满足这个条件才执行方法体的逻辑,mDrawingOrder默认就是DRAW_ORDER_DEFAULT,而只有当调用了setPageTransform函数设置transform的时候,才会被赋值成DRAW_ORDER_REVERSE或者DRAW_ORDER_FORWARD,不管是哪个,都会导致条件满足。 代码很简单,依次添加子View到mDrawingOrderedChildren集合中,然后排序,所以我们只要知道他排序的规则是什么就行,看sPositionComparator(LayoutParams的内部类ViewPositionComparator): static class ViewPositionComparator implements Comparator<View> { ? ??@Override ? ? public int compare(View lhs, View rhs) { ? ? ? ??final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); ? ? ? ??final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); ? ? ? ??if (llp.isDecor != rlp.isDecor) { ? ? ? ? ? ? return llp.isDecor ? 1 : -1; ? ? ?? ?} ? ? ? ? return llp.position - rlp.position; ? ??} } 简单说下:如果比较的两个View一个是DecorView,一个不是,就按照参数前边的View的isDecor布尔值来排序。其他直接按照参数前边的位置减后边的位置,来决定排序。按照位置升序排序。 populate最后一步,遍历子View让其获得焦点,设置获取焦点的规则为FOCUS_FORWARD: 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; ? ? ? ? ? ? ? ?} ? ? ? ? ? ?} ? ? ? ?} ? ?} } populate方法就分析完了。 但是,注意addNewItem只是将View加到了List集合里,而ViewGroup并没有收到他的子View,所以此时依然不会显示item。这个时候你一定想到了ViewGroup添加子View的方法:addView。那这个方法什么时候调用的呢? 答案:在实现Adapter的时候,或者FragmentAdapter默认的instantiateItem方法中,往往是创建了View并返回,在这之中还有一句很关键的代码就是container.addView(),而container就是ViewGroup,他将自身传递过来,就是让开发者往里边添加子View的。而如果你一不小心漏了这句话,你会发现ViewPager什么都没有显示出来! addView是ViewGroup的方法,ViewPager重载了三个参数的addView方法。下面来看: public void addView(View child, int index, ViewGroup.LayoutParams params) { ? ? if (!checkLayoutParams(params)) { ? ?? ? ?params = generateLayoutParams(params); ? ? } ? ? ?final LayoutParams lp = (LayoutParams) params; ? ? // Any views added via inflation should be classed as part of the decor ?? ?lp.isDecor |= isDecorView(child); ? ? if (mInLayout) { ? ? ?? ?if (lp != null && lp.isDecor) { ? ? ? ? ? ? throw new IllegalStateException("Cannot add pager decor view during layout"); ? ? ? ? } ? ? ? ? lp.needsMeasure = true; ? ?? ? ?addViewInLayout(child, index, params); ?? ?} else { ? ? ?? ?super.addView(child, index, params); ? ? } ?? ?if (USE_CACHE) { ? ? ?? ?if (child.getVisibility() != GONE) { ? ? ? ? ? ? child.setDrawingCacheEnabled( mScrollingCacheEnabled); ? ?? ? ?} else { ? ? ? ?? ? ?child.setDrawingCacheEnabled(false); ? ?? ? ?} ? ? } } 其实这个实现是为了处理ViewPager搞出来的这个DecorView的,然后会根据是否在执行从Measure中调用的populate的方法(mInLayout在onMeasure方法中做了赋值操作),分成了两个分支addViewInLayout和super.addView。 addViewInLayout只是将View加到ViewGroup的View[]数组中而不会调用requestLayout引起重新测量布局,但是这部分也不能不管,刚刚通过这种方法加进去的View还没有测量,所以没办法layout,这个时候在onLayout里边会有相应的处理。 看ViewGroup的addView方法: public void addView(View child, int index, LayoutParams params) { ? ??//省略部分源码 ? ? requestLayout(); ?? ?invalidate(true); ? ??addViewInner(child, index, params, false); } 这是最关键的几行代码,requestLayout、invalidate,这个时候就会引起重新测量,布局,绘制。 invalidate方法会让ViewGroup向下引起子View的绘制,位置大小有改变的才绘制。最终才能见到ViewPager显示的子View。 ? ? |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 20:36:46- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |