解析ViewPager(二)——ViewPager源码解析
来源:互联网 发布:xsplit直播软件 编辑:程序博客网 时间:2024/06/03 14:53
前言
前一篇博客介绍了ViewPager的简单使用,这篇博客主要从源码的角度来解析ViewPager。
ViewPager的一些变量
ViewPager是一组视图,那么它的父类必然是ViewGroup,也就是说ViewPager继承了ViewGroup的所有属性。我们先看一下部分源码:
public class ViewPager extends ViewGroup { private static final String TAG = "ViewPager"; private static final boolean DEBUG = false; private static final boolean USE_CACHE = false; private static final int DEFAULT_OFFSCREEN_PAGES = 1; private static final int MAX_SETTLE_DURATION = 600; // ms private static final int MIN_DISTANCE_FOR_FLING = 25; // dips private static final int DEFAULT_GUTTER_SIZE = 16; // dips private static final int MIN_FLING_VELOCITY = 400; // dips static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity }; /** * Used to track what the expected number of items in the adapter should be. * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. *用于监测项目中我们需要适配器的期望的页卡数 */ private int mExpectedAdapterCount; /** * 该类用于保存页面信息 */ static class ItemInfo { Object object;//页面展示的页卡对象 int position;//页卡下标(页码) boolean scrolling;//是否滚动 float widthFactor;//表示加载的页面占ViewPager所占的比例[0~1](默认返回1) ,这个值可以设置一个屏幕显示多少个页面 float offset;//页卡偏移量 } //页卡排序 private static final Comparator<ItemInfo> COMPARATOR = new Comparator<ItemInfo>(){ @Override public int compare(ItemInfo lhs, ItemInfo rhs) { return lhs.position - rhs.position;}};//插值器:他的作用就是根据不同的时间控制滑动的速度。private static final Interpolator sInterpolator = new Interpolator() {@Overridepublic float getInterpolation(float t) {t -= 1.0f;return t * t * t * t * t + 1.0f;}};//表示已经缓存的页面信息private final ArrayList<ItemInfo> mItems = new ArrayList<ItemInfo>();private final ItemInfo mTempItem = new ItemInfo();PagerAdapter mAdapter;//页卡适配器int mCurItem; // Index of currently displayed page.当前页面的下标// Offsets of the first and last items, if known.// Set during population, used to determine if we are at the beginning// or end of the pager data set during touch scrolling.private float mFirstOffset = -Float.MAX_VALUE;//第一个页卡的滑动偏移量private float mLastOffset = Float.MAX_VALUE;//最后一个页卡的滑动偏移量。。。。。。省略部分代码。。。。。。/** * Position of the last motion event. 最后页卡滑动事件的位置 */private float mLastMotionX;private float mLastMotionY;private float mInitialMotionX;private float mInitialMotionY;/** * ID of the active pointer. This is used to retain consistency during * drags/flings if multiple pointers are used. */private int mActivePointerId = INVALID_POINTER;//活动指针标示 如果使用多个指针,这用于保持拖动/ flings期间的一致性。/** * Sentinel value for no current active pointer. * Used by {@link #mActivePointerId}. */private static final int INVALID_POINTER = -1;//没有活动的当前指针的哨兵值/** * Determines speed during touch scrolling *这个速度追踪器用于触摸滑动时追踪滑动速度 */private VelocityTracker mVelocityTracker;private int mMinimumVelocity;private int mMaximumVelocity;private int mFlingDistance;private int mCloseEnough;// If the pager is at least this close to its final position, complete the scroll// on touch down and let the user interact with the content inside instead of// "catching" the flinging pager.//如果页面至少接近它的最终位置,完成向下滚动,让用户与内容中的内容进行交互,而不是“捕获”flinging页面。private static final int CLOSE_ENOUGH = 2; // dpprivate boolean mFakeDragging;private long mFakeDragBeginTime;private EdgeEffectCompat mLeftEdge;private EdgeEffectCompat mRightEdge;private boolean mFirstLayout = true;private boolean mNeedCalculatePageOffsets = false;private boolean mCalledSuper;private int mDecorChildCount;private List<OnPageChangeListener> mOnPageChangeListeners;private OnPageChangeListener mOnPageChangeListener;private OnPageChangeListener mInternalPageChangeListener;private List<OnAdapterChangeListener> mAdapterChangeListeners;private PageTransformer mPageTransformer;private int mPageTransformerLayerType;private Method mSetChildrenDrawingOrderEnabled;private static final int DRAW_ORDER_DEFAULT = 0;private static final int DRAW_ORDER_FORWARD = 1;private static final int DRAW_ORDER_REVERSE = 2;private int mDrawingOrder;private ArrayList<View> mDrawingOrderedChildren;private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator();/** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */public static final int SCROLL_STATE_IDLE = 0;//空闲/** * Indicates that the pager is currently being dragged by the user. */public static final int SCROLL_STATE_DRAGGING = 1;//滑动/** * Indicates that the pager is in the process of settling to a final position. */public static final int SCROLL_STATE_SETTLING = 2;//滑动结束
这段代码中我们我们看到ViewPager继承自ViewGroup,主要我们看上面注释的几个变量:
- mExpectedAdapterCount:这个变量用于监测项目中我们需要适配器的期望的页卡数,如果APP改变了它,当我们不期望它的时候,会抛出一个异常!
- ItemInfo:这个内部类是用来保存页卡信息的
- sInterpolator:插值器,它的主要作用是根据不同的时间来控制滑动速度。
- ArrayList<ItemInfo> mItems:表示已经缓存的页面信息(通常会缓存当前显示页面以前当前页面前后页面,不过缓存页面的数量由mOffscreenPageLimit决定)
- PagerAdapter mAdapter:页卡适配器
- int mCurItem:当前页面的下标
- mFirstOffset/mLastOffset 第/最后一个页卡的滑动偏移量
- mActivePointerId:活动指针标示如果使用多个指针,这用于保持拖动/ flings期间的一致性。
- mVelocityTracker:速度追踪器用于触摸滑动时追踪滑动速度
- SCROLL_STATE_IDLE = 0:表示ViewPager处于空闲,建立状态。 当前页面完全在视图中,并且没有正在进行动画。
- SCROLL_STATE_DRAGGING = 1:表示用户当前正在拖动ViewPager。
- SCROLL_STATE_SETTLING = 2:表示ViewPager正在设置到最终位置。
ViewPager的几个重要方法
1、initViewPager()
initViewPager 是初始化ViewPager,其实还是比较简单的,不难理解,源码如下:
void initViewPager() { setWillNotDraw(false); setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); setFocusable(true); final Context context = getContext(); mScroller = new Scroller(context, sInterpolator);//创建Scroller对象 final ViewConfiguration configuration = ViewConfiguration.get(context);//一个标准常量 final float density = context.getResources().getDisplayMetrics().density;//获取屏幕密度 mTouchSlop = configuration.getScaledPagingTouchSlop();//获取TouchSlop:系统所能识别的被认为是滑动的最小距离 mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density);//最小速度 mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();//获取允许执行一个fling手势的最大速度值 mLeftEdge = new EdgeEffectCompat(context); mRightEdge = new EdgeEffectCompat(context); mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); mCloseEnough = (int) (CLOSE_ENOUGH * density); mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); if (ViewCompat.getImportantForAccessibility(this) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } //ViewCompat一个安卓官方实现兼容的帮助类 ViewCompat.setOnApplyWindowInsetsListener(this, new android.support.v4.view.OnApplyWindowInsetsListener() { private final Rect mTempRect = new Rect(); @Override public WindowInsetsCompat onApplyWindowInsets(final View v, final WindowInsetsCompat originalInsets) { // First let the ViewPager itself try and consume them... final WindowInsetsCompat applied = ViewCompat.onApplyWindowInsets(v, originalInsets); if (applied.isConsumed()) { // If the ViewPager consumed all insets, return now return applied; } // Now we'll manually dispatch the insets to our children. Since ViewPager // children are always full-height, we do not want to use the standard // ViewGroup dispatchApplyWindowInsets since if child 0 consumes them, // the rest of the children will not receive any insets. To workaround this // we manually dispatch the applied insets, not allowing children to // consume them from each other. We do however keep track of any insets // which are consumed, returning the union of our children's consumption final Rect res = mTempRect; res.left = applied.getSystemWindowInsetLeft(); res.top = applied.getSystemWindowInsetTop(); res.right = applied.getSystemWindowInsetRight(); res.bottom = applied.getSystemWindowInsetBottom(); for (int i = 0, count = getChildCount(); i < count; i++) { final WindowInsetsCompat childInsets = ViewCompat .dispatchApplyWindowInsets(getChildAt(i), applied); // Now keep track of any consumed by tracking each dimension's min // value res.left = Math.min(childInsets.getSystemWindowInsetLeft(), res.left); res.top = Math.min(childInsets.getSystemWindowInsetTop(), res.top); res.right = Math.min(childInsets.getSystemWindowInsetRight(), res.right); res.bottom = Math.min(childInsets.getSystemWindowInsetBottom(), res.bottom); } // Now return a new WindowInsets, using the consumed window insets return applied.replaceSystemWindowInsets( res.left, res.top, res.right, res.bottom); } }); }
2、onLayout()
ViewPager继承了ViewGroup那么肯定就要重写onLayout()方法,该方法的主要作用是布局,那么当然也复写了onMeasure()方法测量。关于View的原理可以看看View的工作原理(三)--View的Layout和Draw过程。ViewPager的子View是水平摆放的,所以在onLayout中,大部分工作的就是计算childLeft,即子View的左边位置,而顶部位置基本上是一样的。Viewpager的onlayout其实就根据populate()方法中计算出的当前页面的offset来绘制当前页面,和其他页面.下面我们仔细去研究内部滑动源码或者setCurrentPage源码都可以发现实际上是调用了populate()方法。当我们需要有View更新的时候比如addView()、removeView()都会进行requestLayout()重新布局、以及invalidate()重新绘制界面。
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) { final int count = getChildCount(); int width = r - l; int height = b - t; int paddingLeft = getPaddingLeft(); int paddingTop = getPaddingTop(); int paddingRight = getPaddingRight(); int paddingBottom = getPaddingBottom(); final int scrollX = getScrollX(); //DecorView 数量 int decorCount = 0; //首先对DecorView进行layout,再对普通页卡进行layout,之所以先对DecorView布局,是为了让普通页卡(页卡)能有合适的偏移 //下面循环主要是针对DecorView for (int i = 0; i < count; i++) { final View child = getChildAt(i); //visibility不为GONE才layout if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //左边和顶部的边距初始化为0 int childLeft = 0; int childTop = 0; if (lp.isDecor) {//只针对Decor View //获取水平或垂直方向上的Gravity final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; //根据水平方向上的Gravity,确定childLeft以及paddingRight switch (hgrav) { default://没有设置水平方向Gravity时(左中右),childLeft就取paddingLeft childLeft = paddingLeft; break; case Gravity.LEFT://水平方向Gravity为left,DecorView往最左边靠 childLeft = paddingLeft; paddingLeft += child.getMeasuredWidth(); break; case Gravity.CENTER_HORIZONTAL://将DecorView居中摆放 childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT://将DecorView往最右边靠 childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } //与上面水平方向的同理,据水平方向上的Gravity,确定childTop以及paddingTop 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()); //将DecorView数量+1 decorCount++; } } } //普通页卡的宽度 final int childWidth = width - paddingLeft - paddingRight; // Page views. Do this once we have the right padding offsets from above. //下面针对普通页卡布局,在此之前我们已经得到正确的偏移量了 for (int i = 0; i < count; i++) { final View child = getChildAt(i); if (child.getVisibility() != GONE) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //ItemInfo 是ViewPager静态内部类,前面介绍过它保存了普通页卡(也就是页卡)的position、offset等信息,是对普通页卡的一个抽象描述 ItemInfo ii; //infoForChild通过传入View查询对应的ItemInfo对象 if (!lp.isDecor && (ii = infoForChild(child)) != null) { //计算当前页卡的左边偏移量 int loff = (int) (childWidth * ii.offset); //将左边距+左边偏移量得到最终页卡左边位置 int childLeft = paddingLeft + loff; int childTop = paddingTop; //如果当前页卡需要进行测量(measure),当这个页卡是在Layout期间新添加新的, // 那么这个页卡需要进行测量,即needsMeasure为true if (lp.needsMeasure) { //标记已经测量过了 lp.needsMeasure = false; //下面过程跟onMeasure类似 final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidth * lp.widthFactor), MeasureSpec.EXACTLY); final int heightSpec = MeasureSpec.makeMeasureSpec( (int) (height - paddingTop - paddingBottom), MeasureSpec.EXACTLY); child.measure(widthSpec, heightSpec); } if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + "x" + child.getMeasuredHeight()); //对普通页卡进行layout child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); } } } //将部分局部变量保存到实例变量中 mTopPageBounds = paddingTop; mBottomPageBounds = height - paddingBottom; mDecorChildCount = decorCount; //如果是第一次layout,则将ViewPager滑动到第一个页卡的位置 if (mFirstLayout) { scrollToItem(mCurItem, false, 0, false); } //标记已经布局过了,即不再是第一次布局了 mFirstLayout = false;}
3,onMeasure()
前面,onLayout()方法中布局,用到了measure测量的结果,下面我们就来看下ViewPager的onMeasure()。该方法主要做了四件事:
- 测量DecorView
- 确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数)
- 确保我们需要显示的fragment已经被我们创建好了
- 再对子View进行测量
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { //根据布局文件,设置尺寸信息,默认大小为0 setMeasuredDimension(getDefaultSize(0, widthMeasureSpec), getDefaultSize(0, heightMeasureSpec)); final int measuredWidth = getMeasuredWidth(); final int maxGutterSize = measuredWidth / 10; //设置mGutterSize的值,后面再讲mGutterSize mGutterSize = Math.min(maxGutterSize, mDefaultGutterSize); // ViewPager的显示区域只能显示对于一个View //childWidthSize和childHeightSize为一个View的可用宽高大小 //即去除了ViewPager内边距后的宽高 int childWidthSize = measuredWidth - getPaddingLeft() - getPaddingRight(); int childHeightSize = getMeasuredHeight() - getPaddingTop() - getPaddingBottom(); //先对DecorView进行测量 //下面这个循环是只针对DecorView的,即用于装饰ViewPager的View 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,即用于装饰ViewPager的View 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; //consumeHorizontal:如果是在垂直方向上占用空间, // 那么水平方向就是match_parent,即EXACTLY //而垂直方向上具体占用多少空间,还得由DecorView决定 //consumeHorizontal也是同理 if (consumeVertical) { widthMode = MeasureSpec.EXACTLY; } else if (consumeHorizontal) { heightMode = MeasureSpec.EXACTLY; } //宽高大小,初始化为ViewPager可视区域中页卡可用空间 int widthSize = childWidthSize; int heightSize = childHeightSize; //如果宽度不是wrap_content,那么width的测量模式就是EXACTLY //如果宽度既不是wrap_content又不是match_parent,那么说明是用户 //在布局文件写的具体的尺寸,直接将widthSize设置为这个具体尺寸 if (lp.width != LayoutParams.WRAP_CONTENT) { widthMode = MeasureSpec.EXACTLY; if (lp.width != LayoutParams.FILL_PARENT) { widthSize = lp.width; } } if (lp.height != LayoutParams.WRAP_CONTENT) { heightMode = MeasureSpec.EXACTLY; if (lp.height != LayoutParams.FILL_PARENT) { heightSize = lp.height; } } //确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数) final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode); final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, heightMode); //对DecorView进行测量 child.measure(widthSpec, heightSpec); //如果Decor View占用了ViewPager的垂直方向的空间 //需要将页卡的竖直方向可用的空间减去DecorView的高度, //同理,水平方向上也做同样的处理 if (consumeVertical) { childHeightSize -= child.getMeasuredHeight(); } else if (consumeHorizontal) { childWidthSize -= child.getMeasuredWidth(); } } } } //确定页卡默认宽高的测量规格MeasureSpec(包含尺寸和模式的整数) mChildWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidthSize, MeasureSpec.EXACTLY); mChildHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeightSize, MeasureSpec.EXACTLY); //确保我们需要显示的fragment已经被我们创建好了 mInLayout = true; populate();//后面再详细介绍 mInLayout = false; //再对页卡进行测量 size = getChildCount(); for (int i = 0; i < size; ++i) { final View child = getChildAt(i); //visibility为GONE的无需测量 if (child.getVisibility() != GONE) { if (DEBUG) Log.v(TAG, "Measuring #" + i + " " + child + ": " + mChildWidthMeasureSpec); //获取页卡的LayoutParams final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //只针对页卡而不对Decor View测量 if (lp == null || !lp.isDecor) { //LayoutParams的widthFactor是取值为[0,1]的浮点数, // 用于表示页卡占ViewPager显示区域中页卡可用宽度的比例, // 即(childWidthSize * lp.widthFactor)表示当前页卡的实际宽度 final int widthSpec = MeasureSpec.makeMeasureSpec( (int) (childWidthSize * lp.widthFactor), MeasureSpec.EXACTLY); //对当前页卡进行测量 child.measure(widthSpec, mChildHeightMeasureSpec); } } }}
4、populate()
前面多处用到了populate()方法,下面我们就来研究一下populate()方法,看这个方法我看得有点懵逼!!!主要是之前一直没将重点放在PagerAdapter,与PagerAdapter联系起来后发现原来还是比较容易的,populate()方法主要的作用是:根据制定的页面缓存大小,做了页面的销毁和重建。
1.更新items,将items中的内容换成当前展示页面以及预缓存页面。我们从下面的源码中可以看到,这里会调用PagerAdapter的startUpdate()、instantiateItem()、destroyItem()、setPrimaryItem()、finishUpdate()等方法,基本是把PagerAdapter的所有生命周期从头走到尾。
2.计算每个items的off(偏移量),这个就是布局时onLayout()方法中起作用的。
2.计算每个items的off(偏移量),这个就是布局时onLayout()方法中起作用的。
void populate(int newCurrentItem) { ItemInfo oldCurInfo = null; if (mCurItem != newCurrentItem) { oldCurInfo = infoForPosition(mCurItem); mCurItem = newCurrentItem; } if (mAdapter == null) { //对页卡的绘制顺序进行排序,优先绘制DecorView //再按照position从小到大排序 sortChildDrawingOrder(); return; } //如果我们正在等待populate,那么在用户手指抬起切换到新的位置期间应该推迟创建页卡, // 直到滚动到最终位置再去创建,以免在这个期间出现差错 if (mPopulatePending) { if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); //对页卡的绘制顺序进行排序,优先绘制Decor View //再按照position从小到大排序 sortChildDrawingOrder(); return; } //同样,在ViewPager没有attached到window之前,不要populate. // 这是因为如果我们在恢复View的层次结构之前进行populate,可能会与要恢复的内容有冲突 if (getWindowToken() == null) { return; } //回调PagerAdapter的startUpdate函数, // 告诉PagerAdapter开始更新要显示的页面 mAdapter.startUpdate(this); final int pageLimit = mOffscreenPageLimit; //确保起始位置大于等于0,如果用户设置了缓存页面数量,第一个页面为当前页面减去缓存页面数量 final int startPos = Math.max(0, mCurItem - pageLimit); //保存数据源中的数据个数 final int N = mAdapter.getCount(); //确保最后的位置小于等于数据源中数据个数-1, // 如果用户设置了缓存页面数量,第一个页面为当前页面加缓存页面数量 final int endPos = Math.min(N - 1, mCurItem + pageLimit); //判断用户是否增减了数据源的元素,如果增减了且没有调用notifyDataSetChanged,则抛出异常 if (N != mExpectedAdapterCount) { //resName用于抛异常显示 String resName; try { resName = getResources().getResourceName(getId()); } catch (Resources.NotFoundException e) { resName = Integer.toHexString(getId()); } throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + " contents without calling PagerAdapter#notifyDataSetChanged!" + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + " Pager id: " + resName + " Pager class: " + getClass() + " Problematic adapter: " + mAdapter.getClass()); } //定位到当前获焦的页面,如果没有的话,则添加一个 int curIndex = -1; ItemInfo curItem = null; //遍历每个页面对应的ItemInfo,找出获焦页面 for (curIndex = 0; curIndex < mItems.size(); curIndex++) { final ItemInfo ii = mItems.get(curIndex); //找到当前页面对应的ItemInfo后,跳出循环 if (ii.position >= mCurItem) { if (ii.position == mCurItem) curItem = ii; break; } } //如果没有找到获焦的页面,说明mItems列表里面没有保存获焦页面, // 需要将获焦页面加入到mItems里面 if (curItem == null && N > 0) { curItem = addNewItem(mCurItem, curIndex); } //默认缓存当前页面的左右两边的页面,如果用户设定了缓存页面数量, // 则将当前页面两边都缓存用户指定的数量的页面 //如果当前没有页面,则我们啥也不需要做 if (curItem != null) { float extraWidthLeft = 0.f; //左边的页面 int itemIndex = curIndex - 1; //如果当前页面左边有页面,则将左边页面对应的ItemInfo取出,否则左边页面的ItemInfo为null ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; //保存显示区域的宽度 final int clientWidth = getClientWidth(); //算出左边页面需要的宽度,注意,这里的宽度是指实际宽度与可视区域宽度比例, // 即实际宽度=leftWidthNeeded*clientWidth final float leftWidthNeeded = clientWidth <= 0 ? 0 : 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; //从当前页面左边第一个页面开始,左边的页面进行遍历 for (int pos = mCurItem - 1; pos >= 0; pos--) { //如果左边的宽度超过了所需的宽度,并且当前当前页面位置比第一个缓存页面位置小 //这说明这个页面需要Destroy掉 if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { //如果左边已经没有页面了,跳出循环 if (ii == null) { break; } //将当前页面destroy掉 if (pos == ii.position && !ii.scrolling) { mItems.remove(itemIndex); //回调PagerAdapter的destroyItem mAdapter.destroyItem(this, pos, ii.object); if (DEBUG) { Log.i(TAG, "populate() - destroyItem() with pos: " + pos + " view: " + ((View) ii.object)); } //由于mItems删除了一个元素 //需要将索引减一 itemIndex--; curIndex--; ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } else if (ii != null && pos == ii.position) { //如果当前位置是需要缓存的位置,并且这个位置上的页面已经存在 //则将左边宽度加上当前位置的页面 extraWidthLeft += ii.widthFactor; //mItems往左遍历 itemIndex--; //ii设置为当前遍历的页面的左边一个页面 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } else {//如果当前位置是需要缓存,并且这个位置没有页面 //需要添加一个ItemInfo,而addNewItem是通过PagerAdapter的instantiateItem获取对象 ii = addNewItem(pos, itemIndex + 1); //将左边宽度加上当前位置的页面 extraWidthLeft += ii.widthFactor; //由于新加了一个元素,当前的索引号需要加1 curIndex++; //ii设置为当前遍历的页面的左边一个页面 ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; } } //同理,右边需要添加缓存的页面 /*........................* * * * 省略右边添加缓存页面代码 * * * *........................*/ calculatePageOffsets(curItem, curIndex, oldCurInfo); } if (DEBUG) { Log.i(TAG, "Current page list:"); for (int i = 0; i < mItems.size(); i++) { Log.i(TAG, "#" + i + ": page " + mItems.get(i).position); } } //回调PagerAdapter的setPrimaryItem,告诉PagerAdapter当前显示的页面 mAdapter.setPrimaryItem(this, mCurItem, curItem != null ? curItem.object : null); //回调PagerAdapter的finishUpdate,告诉PagerAdapter页面更新结束 mAdapter.finishUpdate(this); //检查页面的宽度是否测量,如果页面的LayoutParams数据没有设定,则去重新设定好 final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); lp.childIndex = i; if (!lp.isDecor && lp.widthFactor == 0.f) { // 0 means requery the adapter for this, it doesn't have a valid width. final ItemInfo ii = infoForChild(child); if (ii != null) { lp.widthFactor = ii.widthFactor; lp.position = ii.position; } } } //重新对页面排序 sortChildDrawingOrder(); //如果ViewPager被设定为可获焦的,则将当前显示的页面设定为获焦 if (hasFocus()) { View currentFocused = findFocus(); ItemInfo ii = currentFocused != null ? infoForAnyChild(currentFocused) : null; if (ii == null || ii.position != mCurItem) { for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); ii = infoForChild(child); if (ii != null && ii.position == mCurItem) { if (child.requestFocus(View.FOCUS_FORWARD)) { break; } } } } }}
5、setAdapter()
这个方法很容易理解,就是设置ViewPager所需要的适配器。我们看下面的源码:
/** * Set a PagerAdapter that will supply views for this pager as needed. * * @param adapter Adapter to use */public void setAdapter(PagerAdapter adapter) { //如果已经设置过PagerAdapter,即mAdapter != null,做一些清理工作 if (mAdapter != null) { //清除观察者 mAdapter.setViewPagerObserver(null); //回调startUpdate函数,告诉PagerAdapter开始更新要显示的页面 mAdapter.startUpdate(this); //4如果之前保存有页面,则将之前所有的页面destroy掉 for (int i = 0; i < mItems.size(); i++) { final ItemInfo ii = mItems.get(i); mAdapter.destroyItem(this, ii.position, ii.object); } //回调finishUpdate,告诉PagerAdapter结束更新 mAdapter.finishUpdate(this); //将所有的页面清除 mItems.clear(); //将所有的非Decor View移除,即将页面移除 removeNonDecorViews(); //当前的显示页面重置到第一个 mCurItem = 0; //滑动重置到(0,0)位置 scrollTo(0, 0); } //保存上一次的PagerAdapter final PagerAdapter oldAdapter = mAdapter; //设置mAdapter为新的PagerAdapter mAdapter = adapter; //设置期望的适配器中的页面数量为0个 mExpectedAdapterCount = 0; //如果设置的PagerAdapter不为null if (mAdapter != null) { //确保观察者不为null,观察者主要是用于监视数据源的内容发生变化 if (mObserver == null) { mObserver = new PagerObserver(); } //将观察者设置到PagerAdapter中 mAdapter.setViewPagerObserver(mObserver); mPopulatePending = false; //保存上一次是否是第一次Layout final boolean wasFirstLayout = mFirstLayout; //设定当前为第一次Layout mFirstLayout = true; //更新期望的数据源中页面个数 mExpectedAdapterCount = mAdapter.getCount(); //如果有数据需要恢复 if (mRestoredCurItem >= 0) { //回调PagerAdapter的restoreState函数 mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); setCurrentItemInternal(mRestoredCurItem, false, true); //标记无需再恢复 mRestoredCurItem = -1; mRestoredAdapterState = null; mRestoredClassLoader = null; } else if (!wasFirstLayout) {//如果在此之前不是第一次Layout //由于ViewPager并不是将所有页面作为页卡, // 而是最多缓存用户指定缓存个数*2(左右两边,可能左边或右边没有那么多页面) //因此需要创建和销毁页面,populate主要工作就是这些 populate(); } else { //重新布局(Layout) requestLayout(); } } //如果PagerAdapter发生变化,并且设置了OnAdapterChangeListener监听器 //则回调OnAdapterChangeListener的onAdapterChanged函数 if (mAdapterChangeListener != null && oldAdapter != adapter) { mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); }}
6、onPageScrolled()
当滚动当前页时,将调用此方法,作为程序启动的平滑滚动或用户启动的触摸滚动的一部分。如果你重写这个方法,你必须调用到超类实现(例如,在onPageScrolled之前的super.onPageScrolled(position,offset,offsetPixels))。这段代码也比较好理解,就是控制ViewPager的滚动,将我们需要的内容显示在屏幕上,比如滑动到中间时,一半是position另一半是position+1.同时这个方法也是非常重要的,我们如若改造优化ViewPager,就需要重写该方法。
/** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * If you override this method you must call through to the superclass implementation * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled * returns. * * @param position 表示当前是第几个页面 * * @param offset 表示当前页面移动的距离,其实就是个相对实际宽度比例值,取值为[0,1)。0表示整个页面在显示区域,1表示整个页面已经完全左移出显示区域。 * @param offsetPixels 表示当前页面左移的像素个数。 */ @CallSuperprotected void onPageScrolled(int position, float offset, int offsetPixels) { // Offset any decor views if needed - keep them on-screen at all times. //如果有DecorView,则需要使得它们时刻显示在屏幕中,不移出屏幕 if (mDecorChildCount > 0) { //根据Gravity将DecorView摆放到指定位置。 //这部分代码与onMeasure()方法中的原理一样,这里就不做解释了 final int scrollX = getScrollX(); int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); final int width = getWidth(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (!lp.isDecor) continue; final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; int childLeft = 0; switch (hgrav) { default: childLeft = paddingLeft; break; case Gravity.LEFT: childLeft = paddingLeft; paddingLeft += child.getWidth(); break; case Gravity.CENTER_HORIZONTAL: childLeft = Math.max((width - child.getMeasuredWidth()) / 2, paddingLeft); break; case Gravity.RIGHT: childLeft = width - paddingRight - child.getMeasuredWidth(); paddingRight += child.getMeasuredWidth(); break; } childLeft += scrollX; final int childOffset = childLeft - child.getLeft(); if (childOffset != 0) { child.offsetLeftAndRight(childOffset); } } } //分发页面滚动事件,类似于事件的分发 dispatchOnPageScrolled(position, offset, offsetPixels); //如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数 if (mPageTransformer != null) { final int scrollX = getScrollX(); final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); final LayoutParams lp = (LayoutParams) child.getLayoutParams(); //只针对页面进行处理 if (lp.isDecor) continue; //计算child位置 final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); //调用transformPage mPageTransformer.transformPage(child, transformPos); } } //标记ViewPager的onPageScrolled函数执行过 mCalledSuper = true;}
ViewPager的重点是滑动,那么我们来看一下ViewPager的触摸事件,我们主要看事件的拦截onInterceptTouchEvent(),以及事件的消耗onTouchEvent()。我们这里就只看onInterceptTouchEvent(),明白了这段代码,onTouchEvent()也就很容易理解了。
7、onInterceptTouchEvent()
关于ViewPager对于事件的拦截,我们只有当拖动ViewPager时ViewPager才会变化,也就是只有当我们拖拽ViewPager时,才会拦截该触摸事件。
@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) { // 触摸动作 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; // 时刻要注意触摸是否已经结束 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { //Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); //重置一些跟判断是否拦截触摸相关变量 resetTouch(); //触摸结束,无需拦截 return false; } // 如果当前不是按下事件,我们就判断一下,是否是在拖拽切换页面 if (action != MotionEvent.ACTION_DOWN) { //如果当前是正在拽切换页面,直接拦截掉事件,后面无需再做拦截判断 if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } //如果标记为不允许拖拽切换页面,我们就不处理一切触摸事件 if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } //根据不同的动作进行处理 switch (action) { //如果是手指移动操作 case MotionEvent.ACTION_MOVE: { //代码能执行到这里,就说明mIsBeingDragged==false,否则的话,在第7个注释处就已经执行结束了 //使用触摸点Id,主要是为了处理多点触摸 final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { //如果当前的触摸点id不是一个有效的Id,无需再做处理 break; } //根据触摸点的id来区分不同的手指,我们只需关注一个手指就好 final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); //根据这个手指的序号,来获取这个手指对应的x坐标 final float x = MotionEventCompat.getX(ev, pointerIndex); //在x轴方向上移动的距离 final float dx = x - mLastMotionX; //x轴方向的移动距离绝对值 final float xDiff = Math.abs(dx); //与x轴同理 final float y = MotionEventCompat.getY(ev, pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); //判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理 //isGutterDrag是判断是否在两个页面之间的缝隙内移动 //canScroll是判断页面是否可以滑动 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { mLastMotionX = x; mLastMotionY = y; //标记ViewPager不去拦截事件 mIsUnableToDrag = true; return false; } //如果x移动距离大于最小距离,并且斜率小于0.5,表示在水平方向上的拖动 if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); //水平方向的移动,需要ViewPager去拦截 mIsBeingDragged = true; //如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager requestParentDisallowInterceptTouchEvent(true); //设置滚动状态 setScrollState(SCROLL_STATE_DRAGGING); //保存当前位置 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; //启用缓存 setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) {//27.否则的话,表示是竖直方向上的移动 if (DEBUG) Log.v(TAG, "Starting unable to drag!"); //竖直方向上的移动则不去拦截触摸事件 mIsUnableToDrag = true; } if (mIsBeingDragged) { //跟随手指一起滑动 if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } //如果手指是按下操作 case MotionEvent.ACTION_DOWN: { //记录按下的点位置 mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); //第一个ACTION_DOWN事件对应的手指序号为0 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); //重置允许拖拽切换页面 mIsUnableToDrag = false; //标记开始滚动 mIsScrollStarted = true; //手动调用计算滑动的偏移量 mScroller.computeScrollOffset(); //如果当前滚动状态为正在将页面放置到最终位置, //且当前位置距离最终位置足够远 if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { //如果此时用户手指按下,则立马暂停滑动 mScroller.abortAnimation(); mPopulatePending = false; populate(); mIsBeingDragged = true; //如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager requestParentDisallowInterceptTouchEvent(true); //设置当前状态为正在拖拽 setScrollState(SCROLL_STATE_DRAGGING); } else { //结束滚动 completeScroll(false); mIsBeingDragged = false; } if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + " mIsBeingDragged=" + mIsBeingDragged + "mIsUnableToDrag=" + mIsUnableToDrag); break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } //添加速度追踪 if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } mVelocityTracker.addMovement(ev); //只有在当前是拖拽切换页面时我们才会去拦截事件 return mIsBeingDragged;}
总结
ViewPager的主要原理我的理解就是,保存缓存的数组mItems的大小永远都在[0,mOffscreenPageLimit*2+1]范围内,我们滑动下一页卡时,它将前一页卡移出数组,将下一页卡加入缓存。本来打算一片文章写完ViewPager的结果写的时候发现,我对ViewPager的认识还是不足,ViewPager比我想象的要强大许多。以上有什么不准确的地方,希望大家多多指正。
1 0
- 解析ViewPager(二)——ViewPager源码解析
- ViewPager源码解析
- ViewPager源码解析之ViewPager如何呈现
- ViewPager解析
- ViewPager的小知识点及源码解析
- ViewPager源码解析之拖动和滑动
- ViewPager源码解析之FragmentPagerAdapter和FragmentStatePagerAdapter
- ViewPager全面解析
- ViewPager用法详细解析
- Android ViewPager用法解析
- Viewpager加ListView解析
- ViewPager之setOffscreenPageLimit()解析
- Toolbar、TabLayout、ViewPager(Json解析版)
- ViewPager 源码分析(二) —— 关于 notifyDataSetChanged()
- ViewPager动态变换效果之SCViewPager源码解析
- 全连接层解析(二)——源码解析
- ViewPager,Bundle,类的解析
- ViewPager与PagerAdapter深度解析
- eclipse使用egit插件管理git库
- VB调用摄像头录像,拍照,保存
- Eclipse使用入门教程
- unity接sdk,微信支付的时候重启游戏
- How to Use Input-type Variable
- 解析ViewPager(二)——ViewPager源码解析
- 创建ACCESS数据库和数据表
- html属性大全
- 51nod 1640 天气晴朗的魔法【二分+最大生成树】
- 微信开发六:JSSDK-微信分享
- hammerJs-v2.0.4详解
- svn常见问题,报错,命令及我的总结
- How to Use Output-type Variable
- WebSocket+MSE——HTML5直播技术解析