IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: 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、特殊情况的拦截及处理
B、计算缓存边界
C、获取需要显示的Item信息
D、分别循环计算显示Item左边和右边需要缓存的Item信息,创建没有的,销毁多余的
E、计算偏移值,为布局做准备
F、将保存在ItemInfo里的信息赋值给LayoutParams
G、为转场动画准备,遍历获取焦点

在整个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变化)

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_16,color_FFFFFF,t_70,g_se,x_16

?第一种分析:假如现在是第一次设置Adapter,这个时候mItem除了有当前位置一个元素外,没有其他。并且,循环一次都不会执行。

按照缓存策略,默认缓存为1,右边虽然我们不分析,但是可以知道这次populate之后会缓存第二个Item。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第二种分析:滑到下一页,这时候就到了第二个页面,当前的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,不符合循环条件,退出循环。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第三种分析:然后来到第三页,这个时候的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。然后循环不符合条件了,所以循环结束。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

?第四种分析:那什么时候执行第三个条件呢?这个时候在第三种分析的基础上,我们换方向,这个时候我们假设从第三页回到了第二页,这个时候由于我们之前删除了一个元素,所以我们回到之前确定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数组会删除前边的元素,导致错位,所以,这里在第三个条件里边做了矫正。也正因为上边的矫正,才会使得下边的右边缓存的计算才不会出现错误。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5a2f6Iqz6Iqz,size_20,color_FFFFFF,t_70,g_se,x_16

第五种分析:其实第五种分析是针对开发者在代码中调用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
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:32:44  更:2022-04-09 18:33:55 
 
开发: 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-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码