ViewPager 源码分析(一) —— setAdapter() 与 populate()

写在前面



做安卓也有一定时间了,虽然常用控件都已大致掌握,然而随着 Android N 的发布,不自觉的愈发焦虑起来。说来惭愧,Android L 的 Material Design 库里的许多控件都还没用过,照这样下去迟早要被新技术所淘汰。那该怎么办呢,偶然间我看到一篇博文如此说到:“不要觉得 android 里边控件繁杂多样,官方或第三方新控件层出不穷,其实真正的控件就只有两个ViewViewGroup。一旦有了它们的基础,不管来什么新控件,TabLayout也好,CoordinatorLayout也罢,花上一下午翻翻源码基本就掌握了(不仅仅是会用而已)。”

我明白了:新技术的精华还在新技术之外。抛开追寻新技术的浮躁,我决定补一补基础,这也是我写这篇文章的初衷。希望它能开一个好头,勉励自己沉下心来,read the fucking source code!

知识点



之所以选择 ViewPager 是因为它常常用到,大家对它足够熟悉。同时它有些难度,却又是自定义View的官方经典例子,涵盖了不少知识点:

  • PagerAdapter、DataSetObserver 与观察者模式
  • View 的生命周期(measure -> layout -> draw)
  • View 的事件分发(滑动冲突的解决)
  • View 滑动的工具类 (Scroller、VelocityTracker 等)

阅读下文需要您已经有 ViewPager 、PagerAdapter 的使用经验,同时对 View 的绘制和事件分发流程有一定的了解。由于篇幅有限,本文只写到第一点;后几点回以续章的形式呈现。

源码分析


Adapter、DataSetObserver 与观察者模式

我们使用 ViewPager,通常需要定义一个PagerAdapter,然后setAdapter(),用法上和ListView很像。如图: 

我们看到,PagerAdapter持有数据集DataSetObservable,同时包含一些回调。

setAdapter()

那么很自然的,我们从ViewPagersetAdapter开始分析把。

public void setAdapter(PagerAdapter adapter) {
    if (mAdapter != null) { // 1: 清空旧的 Adapter, 做一些初始化处理
        mAdapter.unregisterDataSetObserver(mObserver);
        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();
        removeNonDecorViews();
        mCurItem = 0;
        scrollTo(0, 0);
    }

    // 2: 更新 mAdapter 字段
    final PagerAdapter oldAdapter = mAdapter;
    mAdapter = adapter;
    mExpectedAdapterCount = 0;

    // 3: 给 mAdapter 添加数据 mObserver,恢复状态
    if (mAdapter != null) {
        if (mObserver == null) {
            mObserver = new PagerObserver();
        }
        // 3.1: 给 mAdapter 添加数据 mObserver
        mAdapter.registerDataSetObserver(mObserver);
        mPopulatePending = false;
        final boolean wasFirstLayout = mFirstLayout;
        mFirstLayout = true;
        mExpectedAdapterCount = mAdapter.getCount();
        if (mRestoredCurItem >= 0) { // 3.2: 之前有状态保存下来,恢复状态
            mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
            setCurrentItemInternal(mRestoredCurItem, false, true);
            mRestoredCurItem = -1;
            mRestoredAdapterState = null;
            mRestoredClassLoader = null;
        } else if (!wasFirstLayout) {
            // 3.3: 没状态保存,且不是第一次被 Layout 出来 -> populate() 不知道要干嘛。。
            populate();
        } else { // 3.4: 没状态保存,且是第一次被 Layout 出来 -> 重新布局
            requestLayout();
        }
    }
    // 4: 回调监听器
    if (mAdapterChangeListener != null && oldAdapter != adapter) {
        mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter);
    }
}

前面都好理解,其中ItemInfo 保存了每一项的信息。然后,mItems其实是页面的缓存,adapter变更的时候要先清空之前缓存。主要看 3.2 和 3.3 两处,有两个全局变量mRestoredCurItemmFirstLayout 不好理解,而且源码没有注释。。。

1. mRestoredCurItem

如代码所示,在onRestoreInstanceState的时候保存了当前选中状态。

private int mRestoredCurItem = -1;

@Override
public void onRestoreInstanceState(Parcelable state) {
    ...
    if (mAdapter != null) { ...
    } else {
        mRestoredCurItem = ss.position;
        ...
    }
}

2. mFirstLayout 
ctrl + F了一下,发现mFirstLayout在这些地方被赋值。

private boolean mFirstLayout = true;

public void setAdapter(PagerAdapter adapter) {
    ...
    mFirstLayout = true;
}

@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    mFirstLayout = true;
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    ...
    mFirstLayout = false;
}

这基本上是说,初始化为trueonLayout()之后变为false,使得在setAdapter()里: 
如果已经onLayout()过一次,可以用populate()代替requestLayout()然后又重置了这个mFirstLayout。 
其实到这里还是一头雾水,这个populate()到底要干嘛,源码一点注释都没有,想要答案还得继续分析。

populate()

先别急着看源码,这段比较长,要怎么分析呢。一个函数200多行,一开始我也懵逼了,多亏这片博客点醒了我:viewpager源码分析 
要关注PagerAdapter!!!是啊,绕来绕去怎么把这茬忘了,我们就是从setAdapter()入手的,它才是我们的主角啊。这就好办了,抓住它发现populate()几乎把mAdapter的生命周期走了个遍。我用注释 // —— A~F做了标记:

  • startUpdate()
  • getCount()
  • instantiateItem()
  • destroyItem()
  • setPrimaryItem()
  • finishUpdate()

这样,populate()的职能便呼之欲出了。它主要根据制定的页面缓存大小(mOffscreenPageLimit),做了页面的销毁和重建。除了,A~F这条线,还标注了0~2这条线。其中2部分有一些复杂的计算,主要做了页面销毁这项工作。本来还想分析一下calculatePageOffsets(),现在想来没必要了。我们的主要目标Adapter已经被我们搞定,想必对于PageAdapter中页面如何创建也有了进一步的认识。

void populate(int newCurrentItem) {
    ...
    mAdapter.startUpdate(this); // ------ A

    // 0: 设置页数限制,[startPos, endPos]=>[mCurItem - pageLimit, mCurItem + pageLimit]
    // 对应 public void setOffscreenPageLimit(int limit);
    final int pageLimit = mOffscreenPageLimit;
    final int startPos = Math.max(0, mCurItem - pageLimit);
    final int N = mAdapter.getCount(); // ------ B
    final int endPos = Math.min(N-1, mCurItem + pageLimit);

    // 1: Locate the currently focused item or add it if needed.
    int curIndex = -1;
    ItemInfo curItem = null;
    for (curIndex = 0; curIndex < mItems.size(); curIndex++) {
        final ItemInfo ii = mItems.get(curIndex);
        if (ii.position >= mCurItem) { // 1.1: 便利找到第一个大于 mCurItem 的位置
            if (ii.position == mCurItem) curItem = ii;
            break;
        }
    }

    // 1.2: 由于步骤0 处设置了缓存的页数限制,mItems 中可能会找不到 curItem,
    // 需要 addNewItem
    if (curItem == null && N > 0) {
        curItem = addNewItem(mCurItem, curIndex); // C: addNewItem()里边调用了 mAdapter.instantiateItem()
    }

    // Fill 3x the available width or up to the number of offscreen
    // pages requested to either side, whichever is larger.
    // If we have no current item we have no work to do.
    // 2: (译)根据 mOffscreenPageLimit 这个参数(默认为1),
    // 决定保留的页面范围,即[startPos, endPos]
    if (curItem != null) {
        // 左边范围
        float extraWidthLeft = 0.f;
        int itemIndex = curIndex - 1;
        ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
        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--) {
            // 2.1: 逆序遍历左边,累加 extraWidthLeft,并与 leftWidthNeeded 比较
            // 同时,如果 pos 超出边界[startPos, endPos], 则销毁 view
            // 这里的参数计算比较复杂,只看了个大概。。。
            if (extraWidthLeft >= leftWidthNeeded && pos < startPos) {
                if (ii == null) {
                    break;
                }
                if (pos == ii.position && !ii.scrolling) {
                    mItems.remove(itemIndex);
                    // ------ D
                    mAdapter.destroyItem(this, pos, ii.object);  // 2.2: 回调销毁 view
                    ...
                    itemIndex--;
                    curIndex--;
                    ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
                }
            } else if (ii != null && pos == ii.position) {
                extraWidthLeft += ii.widthFactor; // 2.3: 累加 extraWidthLeft
                itemIndex--;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            } else {
                ii = addNewItem(pos, itemIndex + 1);
                extraWidthLeft += ii.widthFactor; // 2.4: 累加 extraWidthLeft
                curIndex++;
                ii = itemIndex >= 0 ? mItems.get(itemIndex) : null;
            }
        }

        // 右边情况与左边完全对偶,不再详细贴出
        ...

        // 2.6: 计算页面偏移
        calculatePageOffsets(curItem, curIndex, oldCurInfo);
    }

    mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); // ------ E
    mAdapter.finishUpdate(this); // ------ F

    // 下面两部分分别是 LayoutParams 和 Focus 处理,
    // 感觉不太重要,已省略
}

总结



还是小看它了,ViewPager比我想像的要复杂。这一长篇才只分析到PagerAdapter,连DataSetObservable都没引入。然而我已有些困意,未完待续。。。

时间: 2024-11-15 19:11:16

ViewPager 源码分析(一) —— setAdapter() 与 populate()的相关文章

ViewPager源码分析

1.问题 由于Android Framework源码很庞大,所以读源码必须带着问题来读!没有问题,创造问题再来读!否则很容易迷失在无数的方法与属性之中,最后无功而返. 那么,关于ViewPager有什么问题呢? 1. setOffsreenPageLimit()方法是如何实现页面缓存的? 2. 在布局文件中,ViewPager布局内部能否添加其他View? 3. 为什么ViewPager初始化时,显示了一个页面却不会触发onPageSelected回调? 问题肯定不止这三个,但是有这三个问题基本

ViewPager源码分析——滑动切换页面处理过程

上周客户反馈Contacts快速滑动界面切换tab有明显卡顿,让优化. 自己验证又没发现卡顿现象,但总得给客户一个技术性的回复,于是看了一下ViewPager源码中处理滑动切换tab的过程. ViewPager  源码位置: android\frameworks\support\v4\java\android\support\v4\view\ViewPager.java ViewPager其实就是一个重写的ViewGroup,使用ViewPager可以参考SDK中的demo:sdk\extras

【Android 界面效果45】ViewPager源码分析

ViewPager概述: Layout manager that allows the user to flip left and right through pages of data. You supply an implementation of a PagerAdapter to generate the pages that the view shows. Note this class is currently under early design and development.

仿网易新闻导航栏PagerSlidingTabStrip源码分析

转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103),谢谢支持!   前言 最近工作比较忙,所以现在才更新博文,对不住大家了~!言归正传,我们来说说这个PagerSlidingTabStrip,它是配合ViewPager使用的导航栏,网易新闻就是用的这个导航,我们仔细观察这个导航栏不仅他是跟着ViewPager滑动而滑动,而且指示器还会随着标题的长度而动态的变化长度. · 下载地址: Github:https://github.com/astuet

ym——Android仿网易新闻导航栏PagerSlidingTabStrip源码分析

转载请注明本文出自Cym的博客(http://blog.csdn.net/cym492224103),谢谢支持! 前言 最近工作比较忙,所以现在才更新博文,对不住大家了~!言归正传,我们来说说这个PagerSlidingTabStrip,它是配合ViewPager使用的导航栏,网易新闻就是用的这个导航,我们仔细观察这个导航栏不仅他是跟着ViewPager滑动而滑动,而且指示器还会随着标题的长度而动态的变化长度,还可以改变多种样式哦~! · 下载地址: Github:https://github.

Eoe客户端源码分析---SlidingMenu的使用

Eoe客户端源码分析及代码注释 使用滑动菜单SlidingMenu,单击滑动菜单的不同选项,可以通过ViewPager和PagerIndicator显示对应的数据内容. 0  BaseSlidingFragmentActivity.java 主要函数: (1)showMenu() /** * Opens the menu and shows the menu view.*/ public void showMenu() { showMenu(true); } (2)showContent() /

redis源码分析4---结构体---跳跃表

redis源码分析4---结构体---跳跃表 跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的: 跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点.性能上和平衡树媲美,因为事先简单,常用来代替平衡树. 在redis中,只在两个地方使用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构. 1 跳跃表节点 1.1 层 层的数量越多,访问其他节点的速度越快: 1.2 前进指针 遍历举例

[Android]Fragment源码分析(一) 构造

Fragment是Android3.0之后提供的api,被大家广泛所熟知的主要原因还是因为随即附带的ViewPager控件.虽然我并不喜欢用它,但是它确实是一个相对不错的控件.还是我的一贯作风,我将从源码上向大家展示什么是Fragment.我们先写一个简单的代码对Fragment有个直观的认识:(为了保证我们方便调试,我们可以直接使用V4提供的源码包) FragmentTransaction t = getSupportFragmentManager().beginTransaction();

Tomcat7.0源码分析——请求原理分析(中)

前言 在<TOMCAT7.0源码分析--请求原理分析(上)>一文中已经介绍了关于Tomcat7.0处理请求前作的初始化和准备工作,请读者在阅读本文前确保掌握<TOMCAT7.0源码分析--请求原理分析(上)>一文中的相关知识以及HTTP协议和TCP协议的一些内容.本文重点讲解Tomcat7.0在准备好接受请求后,请求过程的原理分析. 请求处理架构 在正式开始之前,我们先来看看图1中的Tomcat请求处理架构. 图1 Tomcat请求处理架构 图1列出了Tomcat请求处理架构中的主