ViewPager源码解析

想要成为一名优秀的Android开发,你需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

  • PagerAdapter 介绍
  • ViwePager 缓存策略
  • ViewPager 布局处理
  • ViewPager 事件处理
  • 相关内容

PagerAdapter 介绍

ViewPager使用非常简单,看下面代码片段

viewPager.setAdapter(new Adapter());

private class Adapter extends PagerAdapter {
    // container 其实就是ViewPager
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        View itemView = LayoutInflater.from(context).inflate(R.layout.item_pager, null);
        container.addView(itemView);
        return itemView;
    }
    // object  为 instantiateItem返回的object对象
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        container.removeView((View) object);
    }
    // viewpager页数
    @Override
    public int getCount() {
        return 10;
    }
    // 判断view跟o是否存在对应关系,内部其实是通过view找到对应的object的关联关系(instantiateItem中返回的object)
    // 本例就返回view == o,因为instantiateItem方法直接返回的view
    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
        return view == o;
    }
}
  • 先看个对象ItemInfo,后面会用到它,它是ViewPager的一个内部类,包含了一个页面的基本信息,在调用Adapter的instantiateItem方法时,在ViewPager内部就会创建这个对象,但它不包含view,结构如下:
static class ItemInfo {
    Object object;      // 为adapter中instantiateItem方法返回的对象
    int position;       // 页面position
    boolean scrolling;  // 是否正在滑动
    float widthFactor;  // 当前页面宽度和ViewPager宽度的比例(默认是1,跟ViewPager宽度一致,可以通过重写adapter的 getPageWidth(int position) 方法自定义内容宽度)
    float offset;       // 当前页面在所有已加载的页面中的索引(用于页面布局,后面会做详细介绍)
}

Object instantiateItem(ViewGroup container, int position)

  • 这个方法是ViewPager需要加载某个页面时调用,container就是ViewPager自己,position页面索引;

  • 我们需要实现的是添加一个view到container中,然后返回一个跟这个view能够关联起来的对象,这个对象可以是view自身,也可以是其他对象(比如FragmentPagerAdapter返回的就是一个Fragment),关键是在isViewFromObject能够将view和这个object关联起来

void destroyItem(ViewGroup container, int position, Object object)

  • 当ViewPager需要销毁一个页面时调用,我们需要将position对应的view从container中移除;
    这时参数除了position就只有object,其实就是上面instantiateItem方法返回的对象,这时要通过object找到对应的View,然后将其移除掉,如果你的instantiateItem方法返回的就是View,这里就直接强转成View移除即可:container.removeView((View) object);如果不是,一般会自己创建一个List缓存view列表,然后根据position从List中找到对应的view移除;(当然你也可以不移除,内存泄漏)。
  • FragmentPagerAdapter的实现是:mCurTransaction.detach((Fragment)object),其实也就是将fragemnt的view从container中移除

isViewFromObject(View view, Object object)

  • 这个方法从名称理解起来像是判断view是否来自object,更近一步解释应该是上面instantiateItem方法中
    向container中添加的view和方法返回的对象两者之间一对一的关系;因为在ViewPager内部有个方法叫infoForChild,
    这个方法是通过view去找到对应页面信息缓存类ItemInfo(内部调用了isViewFromObject),如果找不到,说明这个view是个野孩子,ViewPager会认为不是Adapter提供的View,所以这个View不会显示出来;

  • 总结一下:isViewFromObject 方法是让view和object(内部为ItemInfo)一一对应起来

int getItemPosition(Object object)

  • 该方法是判断当前object对应的View是否需要更新,在调用notifyDataSetChanged时会间接触发该方法,
    如果返回POSITION_UNCHANGED表示该页面不需要更新,如果返回POSITION_NONE则表示该页面无效了,需要销毁并触发destroyItem方法(并且有可能调用instantiateItem重新初始化这个页面)

ViewPager缓存策略

是ViewPager的一个变量,表示ViewPager左右两边分别最大缓存的页面数量,可以通过ViewPager.setOffscreenPageLimit(int limit)方法设置,缓存页面的相关计算(创建,销毁)由populate函数完成,后面会详细说明

初始化缓存(mOffscreenPageLimit == 1)

  • 当初始化时,当前显示页面是第0页;mOffscreenPageLimit为1,所以预加载页面为第1页,再往后的页面就不需要加载了(这里的2, 3, 4页)

中间页面缓存(mOffscreenPageLimit == 1)

  • 当向右滑动到第2页时,左右分别需要缓存一页,第0页就需要销毁掉,第3页需要预加载,第4页不需要加载

 

ViewPager相关方法

ViewPager.setAdapter方法

销毁旧的Adapter数据,用新的Adaper更新UI

    public void setAdapter(@Nullable PagerAdapter adapter) {
        //清除旧的Adapter,对已加载的item调用destroyItem,
        if (mAdapter != null) {
            //清除数据监听器
            mAdapter.setViewPagerObserver(null);
            //开始更新
            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;
            //将自身滚动到初始位置this.scrollTo(0, 0)
            scrollTo(0, 0);
        }

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            //设置适配器数据监听,用于更新视图,外部类只能通过pagerAdapter.notifyDataSetChanged方法通知ViewPager更新视图
            mAdapter.setViewPagerObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                //计算并初始化View
                populate();
            } 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,进行回调

 

ViewPager.populate(int newCurrentItem)

该方法是ViewPager非常重要的方法,主要根据参数newCurrentItem和mOffscreenPageLimit计算出需要初始化的页面和需要销毁页面,然后通过调用Adapter的instantiateItem和destroyItem两个方法初始化新页面和销毁不需要的页面!

    void populate(int newCurrentItem) {
        ItemInfo oldCurInfo = null;
        //如果新选中的位置不是当前位置
        if (mCurItem != newCurrentItem) {
            // 获取旧元素信息
            oldCurInfo = infoForPosition(mCurItem);
            // 更新当前视图index
            mCurItem = newCurrentItem;
        }

        if (mAdapter == null) {
            // 视图位置重排
            sortChildDrawingOrder();
            return;
        }

        // Bail now if we are waiting to populate.  This is to hold off
        // on creating views from the time the user releases their finger to
        // fling to a new position until we have finished the scroll to
        // that position, avoiding glitches from happening at that point.
        // 若滑动未停止则暂停populate操作防止出现问题
        if (mPopulatePending) {
            if (DEBUG) Log.i(TAG, "populate is pending, skipping for now...");
            sortChildDrawingOrder();
            return;
        }

        // Also, don't populate until we are attached to a window.  This is to
        // avoid trying to populate before we have restored our view hierarchy
        // state and conflicting with what is restored.
        // 若视图未依附于窗口则暂停populate操作
        //在ViewRootImpl的performTraversals方法中对通过setView传入的view,调用了dispatchAttachedToWindow方法设置了WindowInfo,如果是ViewGroup,还会遍历调用子View的对应方法
        if (getWindowToken() == null) {
            return;
        }
        //开始更新
        mAdapter.startUpdate(this);
        // mOffscreenPageLimit为设定的预加载数,具体下边说
        // 根据当前视图位置和预加载数计算填充位置的起始点和终结点,不在这个位置区间的都销毁
        final int pageLimit = mOffscreenPageLimit;
        final int startPos = Math.max(0, mCurItem - pageLimit);
        final int N = mAdapter.getCount();
        final int endPos = Math.min(N - 1, mCurItem + pageLimit);

        if (N != mExpectedAdapterCount) {
            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());
        }

        // 在内存中定位所需视图元素,若不存在则重新添加
        //在mItems中查找是否已添加该位置的视图元素
        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;
            }
        }

        if (curItem == null && N > 0) {
            // 终于看到了addNewItem,若当前需填充的元素不在内存中则通过addNewItem调用instantiateItem加载
            curItem = addNewItem(mCurItem, curIndex);
        }

        // 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.
        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);
                        if (DEBUG) {
                            Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                    + " view: " + ((View) 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);
                            if (DEBUG) {
                                Log.i(TAG, "populate() - destroyItem() with pos: " + pos
                                        + " view: " + ((View) 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);
        }

        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则为空实现
        //如果是FragmentStatePagerAdapter或者FragmentPagerAdapter,则会调用FragmentTransaction的commitNowAllowingStateLoss方法提交fragemnt,进行detach或者attach之类的操作
        mAdapter.finishUpdate(this);

        // Check width measurement of current pages and drawing sort order.
        // Update LayoutParams as needed.
        // 遍历子视图,若宽度不合法则重绘
        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.
                //没有有效的宽度,则获取内存中保存的信息给子视图的LayoutParams
                final ItemInfo ii = infoForChild(child);
                if (ii != null) {
                    lp.widthFactor = ii.widthFactor;
                    lp.position = ii.position;
                }
            }
        }
        //重新将子绘图顺序排序
        sortChildDrawingOrder();


        //如果焦点在ViewPager上
        if (hasFocus()) {
            //找到焦点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;
                        }
                    }
                }
            }
        }
    }
  • 根据newCurrentItem和mOffscreenPageLimit计算要加载的page页面,计算出startPos和endPos
  • 根据startPos和endPos初始化页面ItemInfo,先从缓存里面获取,如果没有就调用addNewItem方法,实际调用mAdapter.instantiateItem
  • 将不需要的ItemInfo移除: mItems.remove(itemIndex),并调用mAdapter.destroyItem方法
  • 设置LayoutParams参数(包括position和widthFactor),根据position排序待绘制的View列表:mDrawingOrderedChildren,重写了getChildDrawingOrder方法
  • 最后一步获取当前显示View的焦点:child.requestFocus(View.FOCUS_FORWARD)

 

ViewPager.dataSetChanged()

当调用Adapter的notifyDataSetChanged时,会触发这个方法,该方法会重新计算当前页面的position,
移除需要销毁的页面的ItemInfo对象,然后再调用populate方法刷新页面

 

//PagerAdapter
   public void notifyDataSetChanged() {
        synchronized (this) {
            //这个监听器就是ViewPager内的PagerObserver,是在setAdapter的时候通过Adapter.setViewPagerObserver()传入的
            if (mViewPagerObserver != null) {
                mViewPagerObserver.onChanged();
            }
        }
        mObservable.notifyChanged();
    }

//ViewPager
    private class PagerObserver extends DataSetObserver {
        PagerObserver() {
        }

        @Override
        public void onChanged() {
            //调用ViewPager的dataSetChanged方法
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

 


    void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.

        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        //是否需要刷新页面,此处如果元素个数小于缓存页数、也小于适配器元素个数,则为true
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1
                && mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

        boolean isUpdating = false;
        //遍历容器中的元素
        for (int i = 0; i < mItems.size(); i++) {
            final ItemInfo ii = mItems.get(i);
            // 返回元素相应位置是否发生变化的标志
            // POSITION_UNCHANGED = -1; 表示当前页面不需要更新,不用销毁
            // POSITION_NONE = -2; 需要更新,销毁
            //可以在初始化时为页面设置tag,在getItemPosition方法中根据tag判断仅更新当前页面视图。
            final int newPos = mAdapter.getItemPosition(ii.object);
            // 若返回POSITION_UNCHANGED,跳过
            if (newPos == PagerAdapter.POSITION_UNCHANGED) {
                continue;
            }

            if (newPos == PagerAdapter.POSITION_NONE) {
                // 返回POSITION_NONE时移除元素并记录标志
                // 这里对元素先移除,后重新加载
                mItems.remove(i);
                i--;
                //开始更新
                if (!isUpdating) {
                    mAdapter.startUpdate(this);
                    isUpdating = true;
                }
                //销毁视图
                mAdapter.destroyItem(this, ii.position, ii.object);
                //设置为需要刷新页面
                needPopulate = true;
                //如果当前位置元素被删除,则重新选出新的当前元素位置
                if (mCurItem == ii.position) {
                    // Keep the current item in the valid range
                    newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1));
                    needPopulate = true;
                }
                continue;
            }
            //newPos不是默认的这两种的情况下,并且当前元素的position不等于它,则设置为它,需要刷新
            if (ii.position != newPos) {
                if (ii.position == mCurItem) {
                    // Our current item changed position. Follow it.
                    newCurrItem = newPos;
                }

                ii.position = newPos;
                needPopulate = true;
            }
        }
        //isUpdating=true,则结束更新操作
        if (isUpdating) {
            mAdapter.finishUpdate(this);
        }
        //重新排序
        Collections.sort(mItems, COMPARATOR);
        //如果需要刷新,遍历子view,重置页面宽度,在populate方法中将重新计算它们
        if (needPopulate) {
            // Reset our known page widths; populate will recompute them.
            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) {
                    lp.widthFactor = 0.f;
                }
            }
            //更新UI
            setCurrentItemInternal(newCurrItem, false, true);
            //请求布局
            requestLayout();
        }
    }
  • 循环mItems(每个page对应的ItemInfo对象),调用int newPos = mAdapter.getItemPosition方法
  • 当newPos等于PagerAdapter.POSITION_UNCHANGED表示当前页面不需要更新,不用销毁,当newPos等于PagerAdapter.POSITION_NONE时,需要更新,移除item,调用mAdapter.destroyItem
  • 循环完成后,最后计算出显示页面的newCurrItem,调用setCurrentItemInternal(newCurrItem, false, true)方法更新UI(实际调用populate方法重新计算页面信息)

ViewPager.scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected)

  • 滑动到指定页面,内部会触发OnPageChangeListener 

    private void scrollToItem(int item, boolean smoothScroll, int velocity,
            boolean dispatchSelected) {
        //拿到对应位置的元素信息
        final ItemInfo curInfo = infoForPosition(item);
        int destX = 0;
        //如果元素信息不为空,则计算偏移的目的地
        if (curInfo != null) {
            //获取子View的可用宽大小,即viewPager测量宽度-内边距
            final int width = getClientWidth();
            destX = (int) (width * Math.max(mFirstOffset,
                    Math.min(curInfo.offset, mLastOffset)));
        }
        //平稳的滑动 到目的地
        if (smoothScroll) {
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
        } else {
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            completeScroll(false);
            //直接滑动到目的地
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

ViewPager.calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo)

  • 这个方法主要用于计算每个页面对应ItemInfo的offset变量,这个变量用于记录当前view在所有缓存View中(包含当前显示页)的索引,用于布局的时候计算该View应该放在哪个位置
  • 在populate方法中更新完页面数据后,会调用该方法计算所有页面的offset

    private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) {
        //拿到适配器元素个数、实际可见宽度
        final int N = mAdapter.getCount();
        //获取子View的可用宽大小,即viewPager测量宽度-内边距
        final int width = getClientWidth();
        //mPageMargin是页面之间的间隔,marginOffset间隔比例,默认0
        final float marginOffset = width > 0 ? (float) mPageMargin / width : 0;
        
        //根据上一次展示的页面,来确认此次当前页面的offset。只有在使用ViewPager.setCurrentItem的方法直接跳转到指定页面时才条件成立,靠滑动切换页面不会成立。
        if (oldCurInfo != null) {
            //根据oldItem.position与curItem.position的大小关系,来确定curItem的offset值是等于oldItem.offset加上还是减去它们之间间隔的页面(页面宽度+ marginOffset)之和
            
            //oldItem.position<curItem.position,加上
            if (oldCurPosition < curItem.position) {
                int itemIndex = 0;
                ItemInfo ii = null;
                //根据 old页面的offset+old页面的宽比(0f-1f)+每个页面的间隔比例计算出old页面的下一个页面的offset值
                //例如假设marginOffset=0.2,widthFactor=1,一个5个页面,则每个页面的offset分别为0;1.2;2.4;3.6;4.8
                float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset;
                //从old页面的下一个页面开始遍历,一直到当前要展示的页面的位置。
                for (int pos = oldCurPosition + 1;
                        pos <= curItem.position && itemIndex < mItems.size(); pos++) {
                    ii = mItems.get(itemIndex);
                    //循环,直到获取到pos当前位置的元素
                    while (pos > ii.position && itemIndex < mItems.size() - 1) {
                        itemIndex++;
                        ii = mItems.get(itemIndex);
                    }
                    while (pos < ii.position) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset += mAdapter.getPageWidth(pos) + marginOffset;
                        pos++;
                    }
                    //设置当前页面的offset
                    ii.offset = offset;
                    //计算下一个页面的offset
                    offset += ii.widthFactor + marginOffset;
                }
            } else if (oldCurPosition > curItem.position) {
                //oldItem.position>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) {
                        // We don't have an item populated for this,
                        // ask the adapter for an offset.
                        offset -= mAdapter.getPageWidth(pos) + marginOffset;
                        pos--;
                    }
                    offset -= ii.widthFactor + marginOffset;
                    ii.offset = offset;
                }
            }
        }

        // 根据当前元素,再次计算缓存列表中所有元素的偏移量
        //获取元素数量
        final int itemCount = mItems.size();
        //当前要展示的页面的偏移量
        float offset = curItem.offset;
        //要展示的页面的前一个页面的位置
        int pos = curItem.position - 1;
        //如果当前要展示的页面是第0个位置,则设置mFirstOffset=curItem.offset
        mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE;
        //如果当前要展示的页面是最后一个位置,则设置mLastOffset =curItem.offset
        mLastOffset = curItem.position == N - 1
                ? curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE;
        // 计算缓存列表中当前页面前面页面的偏移量(根据当前页面计算)
        for (int i = curIndex - 1; i >= 0; i--, pos--) {
            final ItemInfo ii = mItems.get(i);
            //如果pos跟ii.position之间有间隔页面,则减去间隔偏移量
            while (pos > ii.position) {
                offset -= mAdapter.getPageWidth(pos--) + marginOffset;
            }
            //ii元素的偏移量=它的下一个元素的偏移量-ii页面的宽比-每个页面的间隔比例
            offset -= ii.widthFactor + marginOffset;
            //赋值
            ii.offset = offset;
            //如果ii是第一个元素,则设置mFirstOffset值
            if (ii.position == 0) mFirstOffset = offset;
        }
        //令offset=当前元素的下一个元素的偏移量
        offset = curItem.offset + curItem.widthFactor + marginOffset;
        //下一个元素的pos
        pos = curItem.position + 1;
        // 计算缓存列表中当前页面后面页面的偏移量(根据当前页面计算)
        for (int i = curIndex + 1; i < itemCount; i++, pos++) {
            final ItemInfo ii = mItems.get(i);
            //同上
            while (pos < ii.position) {
                offset += mAdapter.getPageWidth(pos++) + marginOffset;
            }
            //如果ii是最后一个元素,则设置mLastOffset值
            if (ii.position == N - 1) {
                mLastOffset = offset + ii.widthFactor - 1;
            }
            ii.offset = offset;
            offset += ii.widthFactor + marginOffset;
        }

        mNeedCalculatePageOffsets = false;
    }

到目前为止,ViewPager和Adapter相关调用关系差不多分析完了,下面看ViewPager内部对页面的布局,滑动事件监听相关操作!

ViewPager 布局处理

  • ViewPager将子View分为两种,一种是@ViewPager.DecorView注解的View用于装饰ViewPager,它需要占用一些空间;另一种是普通的子View,也就是Adapter创建的View。
  • ViewPager布局处理主要是两个方法onMeasure和onLayout

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

该方法主要测量上述两种子View,第一种@ViewPager.DecorView注解的View就不多说了,用得很少(例如PagerTitleStrip),但要注意的是它会占用一部分ViewPager空间,剩下的留给普通子View;

主要说下普通子View测量,还记得上面提到的ItemInfo中widthFactor变量,它是通过Adapter.getPageWidth方法得来的(可以重写),决定了页面View的宽度,所以这里测量就用到了它;还有一点就是在测量之前会调用populate方法初始化需要显示的页面,然后再测量,看下面代码片段


    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //设置尺寸信息,默认大小为0  
        setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
                getDefaultSize(0, heightMeasureSpec));
        //获取viewper的测量宽度
        final int measuredWidth = getMeasuredWidth();
        final int maxGutterSize = measuredWidth / 10;
        //获取mGutterSize的值,即页面边缘大小(16dp跟1/10测量宽度,两者取最小值)
        mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize);

        // 获取子View的可用宽高的大小,即viewpager宽高除去内边距
        int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight();
        int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom();

        //遍历viewPager的子view,@ViewPager.DecorView注解的View进行测量(例如PagerTitleStrip,将layout_gravity设置为TOP或BOTTOM,以将其固定到ViewPager的顶部或底部)
        int size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //如果该View是DecorView,对Decor进行测量
                if (lp != null && lp.isDecor) {
                    //获取Decor View的在水平方向和竖直方向上的Gravity
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    //默认DedorView模式对应的宽高是wrap_content  
                    int widthMode = MeasureSpec.AT_MOST;
                    int heightMode = MeasureSpec.AT_MOST;
                    //判断DecorView是在垂直方向上还是在水平方向上占用空间 
                    boolean consumeVertical = vgrav == Gravity.TOP || vgrav == Gravity.BOTTOM;
                    boolean consumeHorizontal = hgrav == Gravity.LEFT || hgrav == Gravity.RIGHT;
                    
                    //如果是在垂直方向上占用空间,那么水平方向就是match_parent,即EXACTLY  
                    //而垂直方向上具体占用多少空间,即wrap_content ,还得由DecorView自己决定
                    //如果是水平方向上占用空间同理 
                    if (consumeVertical) {
                        widthMode = MeasureSpec.EXACTLY;
                    } else if (consumeHorizontal) {
                        heightMode = MeasureSpec.EXACTLY;
                    }
                    //DecorView宽高大小,初始化为ViewPager子view可用宽高
                    int widthSize = childWidthSize;
                    int heightSize = childHeightSize;
                    //如果DecorView宽度不是wrap_content,那么width的测量模式就是EXACTLY  
                    //如果宽度既不是wrap_content又不是match_parent,那么说明是用户  
                    //在布局文件写的具体的尺寸,直接将widthSize设置为这个具体尺寸
                    if (lp.width != LayoutParams.WRAP_CONTENT) {
                        widthMode = MeasureSpec.EXACTLY;
                        if (lp.width != LayoutParams.MATCH_PARENT) {
                            widthSize = lp.width;
                        }
                    }
                    //同宽度一样
                    if (lp.height != LayoutParams.WRAP_CONTENT) {
                        heightMode = MeasureSpec.EXACTLY;
                        if (lp.height != LayoutParams.MATCH_PARENT) {
                            heightSize = lp.height;
                        }
                    }
                    //确定宽高的测量规格MeasureSpec(包含尺寸和模式的整数)  
                    final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
                    final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode);
                    //DecorView进行测量
                    child.measure(widthSpec, heightSpec);
                    
                    //如果DecorView占用了ViewPager的垂直方向的空间,那么竖直方向可用空间将减去DecorView的高度
                    //水平方向上同理
                    if (consumeVertical) {
                        childHeightSize -= child.getMeasuredHeight();
                    } else if (consumeHorizontal) {
                        childWidthSize -= child.getMeasuredWidth();
                    }
                }
            }
        }
        //确定非DecorView宽高的测量规格MeasureSpec(包含尺寸和模式的整数) 
        //也就是adapter的view可以占用的宽高
        mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY);
        mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY);

        //通过Adapter获取childView,需要确保我们已经创建了所有需要显示的片段
        mInLayout = true;
        populate();
        mInLayout = false;

        // 测量非DecorView
        size = getChildCount();
        for (int i = 0; i < size; ++i) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                if (DEBUG) {
                    Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec);
                }

                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //只针对非DecorView测量
                if (lp == null || !lp.isDecor) {
                    //LayoutParams的widthFactor是取值为[0,1]的浮点数,  
                    // 用于表示子view占ViewPager显示区域可用宽度的比例,  
                    // 即(childWidthSize * lp.widthFactor)表示子view的实际宽度  
                    final int widthSpec = MeasureSpec.makeMeasureSpec(
                            (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY);
                    //非DecorView的子view进行测量 
                    child.measure(widthSpec, mChildHeightMeasureSpec);
                }
            }
        }
    }

这里用到了LayoutParams.widthFactor,就是ItemInfo.widthFactor,是在populate方法中设置给LayoutParams的,测量就到这里,比较简单,下面看下onLayout方法;

onLayout(boolean changed, int l, int t, int r, int b)

跟onMeasure方法差不多

第一步先layout DecorView,它会占用一定空间,计算出四个padding值(paddingLeft、paddingTop、paddingRight、paddingBottom) 提供给后面layout普通子View使用;

第二步就是利用第一步得出的padding值得到一个可用的空间对子View进行布局,这里就涉及到对多个子View横向排列顺序的问题,这里就根据ItemInfo中的offset值来决定的,通过offset计算每个子View的Left值,关键代码如下:


    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;

        
        //先对DecorView进行layout,再对Adapter的View进行layout
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                //左边和顶部的边距初始化为0  
                int childLeft = 0;
                int childTop = 0;
                if (lp.isDecor) {
                    final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
                    final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
                    //根据水平方向上的Gravity,确定childLeft的值
                    switch (hgrav) {
                        default:
                            childLeft = paddingLeft;
                            break;
                        case Gravity.LEFT:
                            childLeft = paddingLeft;
                            //累加左内边距(多个DecorView都居左边,肯定要累加啦)
                            paddingLeft += child.getMeasuredWidth();
                            break;
                        case Gravity.CENTER_HORIZONTAL:
                            //计算居中时的左边距=(viewPager可见宽-child测量宽)/2
                            childLeft = Math.max((width - child.getMeasuredWidth()) / 2,
                                    paddingLeft);
                            break;
                        case Gravity.RIGHT:
                            //计算居右侧时的左边距=(viewPager可见宽-右边距-child测量宽)
                            childLeft = width - paddingRight - child.getMeasuredWidth();
                            //累加右内边距
                            paddingRight += child.getMeasuredWidth();
                            break;
                    }
                    //与上面水平方向的同理,据水平方向上的Gravity,确定childTop的值
                    switch (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方向已经滑动的距离scrollX  
                    childLeft += scrollX;
                    //对DecorView布局
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                    decorCount++;
                }
            }
        }
        //普通页面(Adapter的View)可用宽度
        final int childWidth = width - paddingLeft - paddingRight;
        //下面针对普通页面布局,在此onLayout之前已经得到正确的偏移量offset了
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                ItemInfo ii;
                //调用infoForChild(child)通过view获取ItemInfo,得到关于这个子view的position,offset等信息
                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方法来布局自己
                    child.layout(childLeft, childTop,
                            childLeft + child.getMeasuredWidth(),
                            childTop + child.getMeasuredHeight());
                }
            }
        }
        mTopPageBounds = paddingTop;
        mBottomPageBounds = height - paddingBottom;
        mDecorChildCount = decorCount;
        //因为是初始化,所以mFirstLayout为true,调用scrollToItem()滑动到当前的页面位置
        if (mFirstLayout) {
            scrollToItem(mCurItem, false, 0, false);
        }
        //标记已经布局过了,即不再是第一次布局了
        mFirstLayout = false;
    }

最后的onDraw方法就是绘制各个页面之间间隔和viewpager的边缘效应效果


    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        //如有需要,在页与页之间绘制drawable
        if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) {
            //获取当前X轴方向滚动偏移量
            final int scrollX = getScrollX();
            //viewPager的测量宽度
            final int width = getWidth();
            //页面间距比例
            final float marginOffset = (float) mPageMargin / width;
            int itemIndex = 0;
            ItemInfo ii = mItems.get(0);
            float offset = ii.offset;
            final int itemCount = mItems.size();
            final int firstPos = ii.position;
            final int lastPos = mItems.get(itemCount - 1).position;
            //遍历元素
            for (int pos = firstPos; pos < lastPos; pos++) {
                while (pos > ii.position && itemIndex < itemCount) {
                    ii = mItems.get(++itemIndex);
                }

                float drawAt;
                //计算绘制区域的left,偏移量累加用于下一个元素
                if (pos == ii.position) {
                    drawAt = (ii.offset + ii.widthFactor) * width;
                    offset = ii.offset + ii.widthFactor + marginOffset;
                } else {
                    float widthFactor = mAdapter.getPageWidth(pos);
                    drawAt = (offset + widthFactor) * width;
                    offset += widthFactor + marginOffset;
                }
                //mTopPageBounds为顶部top,mBottomPageBounds为底部bottom,即普通View可用高度区间
                //如果绘制区域在可见范围内,根据计算出来的区域,绘制页面间隔drawable
                if (drawAt + mPageMargin > scrollX) {
                    mMarginDrawable.setBounds(Math.round(drawAt), mTopPageBounds,
                            Math.round(drawAt + mPageMargin), mBottomPageBounds);
                    mMarginDrawable.draw(canvas);
                }
                //绘制区域超出 滚动偏移量+viewPager的宽,结束后序没意义的绘制(不可见)
                if (drawAt > scrollX + width) {
                    break; // No more visible, no sense in continuing
                }
            }
        }
    }

整体来说ViewPager的布局流程也是非常简单的,下面看事件处理

ViewPager 事件处理

  • ViewPager事件处理内容不多,主要就是左右翻页滑动的事件拦截,滑动事件又只需要拦截横向滑动。
  • 事件拦截处理相关的几个方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent;ViewPager这里只重写了onInterceptTouchEvent和onTouchEvent

ViewPager.onInterceptTouchEvent

该方法主要计算是否拦截滑动事件变量:mIsBeingDragged,满足下面2个主要条件才拦截:

  1. 当xDiff * 0.5f > yDiff,简单说就是X轴上滑动的距离要大于Y轴上的2倍才拦截
  2. 并且canScroll(View v, boolean checkV, int dx, int x, int y)方法返回false,该方法是判断子View能不能横向滑动,如果子View能滑动ViewPager就不拦截滑动

ViewPager.onTouchEvent

该方法主要是通过上面计算出的mIsBeingDragged变量,判断是否需要滑动操作,下面看下在不同MotionEvent中处理的内容:

  • ACTION_MOVE:如果mIsBeingDragged = fasle,这里会重新计算,这里的判断条件是xDiff > yDiff就能滑动了,然后调用performDrag方法完成具体滑动操作
  • ACTION_UP:调用setCurrentItemInternal滑动到最终的页面
  • ACTION_CANCEL:跟ACTION_UP差不多,调用scrollToItem完事
  • 其他滑动Event事件就不描述了,很简单

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值