ViewPager源码解析之拖动和滑动
来源:互联网 发布:mac os关闭窗口快捷键 编辑:程序博客网 时间:2024/06/07 15:03
概述
一个ViewGroup的滑动肯定和它的dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()有关。ViewPager重写了后两者,我们一个个来看。首先说明一下,ViewPager根据手势产生视图移动的方式有两种,一种是MOVE的时候随手指的拖动,一种是UP之后滑动到指定页面,而滑动是通过Scroller + computeScroll()实现的。
onInterceptTouchEvent()
Viewpager有一个mScrollState成员维护着ViewPager当前页面的状态,它可能被赋三个值。
/** * Indicates that the pager is in an idle, settled state. The current page * is fully in view and no animation is in progress. */ //当前page空闲,没有动画 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;
正如onInterceptTouchEvent()注释所说的,方法只是判断我们是否应该拦截这个Touch事件,scrolling都交给onTouchEvent()去做。因为在switch中每一个分支都有break,所以我调换了一下源码的顺序把DOWN放在了MOVE前面,这样更加清晰。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { /* * This method JUST determines whether we want to intercept the motion. * If we return true, onMotionEvent will be called and we do the actual * scrolling there. */ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; // Always take care of the touch gesture being complete. //如果一套手势结束,返回false if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { // Release the drag. if (DEBUG) Log.v(TAG, "Intercept done!"); resetTouch(); return false; } // Nothing more to do here if we have decided whether or not we // are dragging. if (action != MotionEvent.ACTION_DOWN) { //如果正在被drag,拦截 if (mIsBeingDragged) { if (DEBUG) Log.v(TAG, "Intercept returning true!"); return true; } //不允许drag,不拦截 if (mIsUnableToDrag) { if (DEBUG) Log.v(TAG, "Intercept returning false!"); return false; } } switch (action) { case MotionEvent.ACTION_DOWN: { /* * Remember location of down touch. * ACTION_DOWN always refers to pointer index 0. */ //重新给这四个变量赋值,表示一套手势的开始 mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); //获取第一个触摸点的id mActivePointerId = ev.getPointerId(0); //设置允许拖拽为false mIsUnableToDrag = false; //标记为开始滚动 mIsScrollStarted = true; //这个Scroller是在initViewPager()中创建的,这里手动调用计算一下 //Scroller中的x,y值 mScroller.computeScrollOffset(); //如果此时正在向终态靠拢,并且离最终位置还有一定距离 if (mScrollState == SCROLL_STATE_SETTLING && Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { // Let the user 'catch' the pager as it animates. //让用户抓住这个Pager //停止滚动 mScroller.abortAnimation(); //??? mPopulatePending = false; //更新缓存page信息(滚动的时候mCurItem会改变?) populate(); //表示在拖动 mIsBeingDragged = true; //不允许父ViewGroup拦截 requestParentDisallowInterceptTouchEvent(true); //设置新的状态 setScrollState(SCROLL_STATE_DRAGGING); } else { //这里我理解的是正在向终态靠近且距离足够小了,所以不能干涉移动 completeScroll(false); mIsBeingDragged = false; } .... break; } case MotionEvent.ACTION_MOVE: { /* * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check * whether the user has moved far enough from his original down touch. */ //注释的很清楚,如果能进入这个地方说明mIsBeingDragged == false //这里要检查用户是否已经从原始位置移动的够远,以给mIsBeingDragged赋值 //第一个触摸点的id final int activePointerId = mActivePointerId; if (activePointerId == INVALID_POINTER) { // If we don't have a valid id, the touch down wasn't on content. break; } //不太懂,貌似对第一个触摸点id做了一个转换 final int pointerIndex = ev.findPointerIndex(activePointerId); //触摸点横坐标 final float x = ev.getX(pointerIndex); //横向偏移 final float dx = x - mLastMotionX; //横向偏移绝对值 final float xDiff = Math.abs(dx); //纵向 final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mInitialMotionY); ... //这里是说如果在这个区域子View可以滑动,交给子View处理,不拦截 //canScroll的源码贴在后面,在子View中寻找可以滑动的 if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && canScroll(this, false, (int) dx, (int) x, (int) y)) { // Nested view has scrollable area under this point. Let it be handled there. mLastMotionX = x; mLastMotionY = y; mIsUnableToDrag = true; return false; } //如果横向偏移绝对值大于最小值 且 yDiff/xDiff < 0.5f if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); //拦截! mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); setScrollState(SCROLL_STATE_DRAGGING); //保存当前位置 mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollingCacheEnabled(true); } else if (yDiff > mTouchSlop) { //如果在纵向移动了足够的距离,不拦截 // The finger has moved enough in the vertical // direction to be counted as a drag... abort // any attempt to drag horizontally, to work correctly // with children that have scrolling containers. if (DEBUG) Log.v(TAG, "Starting unable to drag!"); mIsUnableToDrag = true; } // if (mIsBeingDragged) { // Scroll to follow the motion event //如果能拖拽,这里产生拖拽,很重要,后面分析。 if (performDrag(x)) { ViewCompat.postInvalidateOnAnimation(this); } } break; } case MotionEventCompat.ACTION_POINTER_UP: onSecondaryPointerUp(ev); break; } ... return mIsBeingDragged; }
遍历子View,触点在子View的边界内,且子View可以滑动,返回true
protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ViewGroup) { final ViewGroup group = (ViewGroup) v; final int scrollX = v.getScrollX(); final int scrollY = v.getScrollY(); final int count = group.getChildCount(); // Count backwards - let topmost views consume scroll distance first. for (int i = count - 1; i >= 0; i--) { // TODO: Add versioned support here for transformed views. // This will not work for transformed views in Honeycomb+ final View child = group.getChildAt(i); if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && canScroll(child, true, dx, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) { return true; } } } return checkV && ViewCompat.canScrollHorizontally(v, -dx); }
performDrag()
前面onInterceptTouchEvent()判断了很多情况,基本都是根据情况判断能不能drag,然后给mIsBeingDragged这个变量赋值,代表最终是否拦截接下来的一串手势。在MOVE的末尾,在可以drag的情况下,我们会进入这个方法来让页面跟随手指的手势。
private boolean performDrag(float x) { boolean needsInvalidate = false; final float deltaX = mLastMotionX - x; mLastMotionX = x; float oldScrollX = getScrollX(); //ViewPager的视图横坐标 float scrollX = oldScrollX + deltaX; final int width = getClientWidth(); //子View左边界和子View右边界 float leftBound = width * mFirstOffset; float rightBound = width * mLastOffset; boolean leftAbsolute = true; boolean rightAbsolute = true; //当前第一个和最后一个页面信息 final ItemInfo firstItem = mItems.get(0); final ItemInfo lastItem = mItems.get(mItems.size() - 1); //如果第一个页面信息不是数据的第0项,更新一下leftBound if (firstItem.position != 0) { leftAbsolute = false; leftBound = firstItem.offset * width; } //同理 if (lastItem.position != mAdapter.getCount() - 1) { rightAbsolute = false; rightBound = lastItem.offset * width; } //边界条件 if (scrollX < leftBound) { if (leftAbsolute) { float over = leftBound - scrollX; needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); } scrollX = leftBound; } else if (scrollX > rightBound) { if (rightAbsolute) { float over = scrollX - rightBound; needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); } scrollX = rightBound; } // Don't lose the rounded component mLastMotionX += scrollX - (int) scrollX; //滑动视图 scrollTo((int) scrollX, getScrollY()); //重要方法 pageScrolled((int) scrollX); return needsInvalidate; }
pageScrolled()
performDrag()方法让ViewPager的视图滑动了(通过scrollTo()方法),并且调用了这个方法,现在我们来看一下这个方法。从整体上来看,这个方法做了这么几件事:
1.根据视图的scrollX获得了当前的页面信息。
2.计算了视图滑动距离的比例和像素。
3.onPageScrolled(currentPage, pageOffset, offsetPixels)
这里说一下,如果滑动造成ViewPager显示区域内有两个Page可以显示,infoForCurrentScrollPosition()返回的是左边那个的ItemInfo。
private boolean pageScrolled(int xpos) { //传进来的参数指的是ViewPager视图滑动的距离。 ... //根据scrollX获取当前应该显示的页面信息 final ItemInfo ii = infoForCurrentScrollPosition(); final int width = getClientWidth(); final int widthWithMargin = width + mPageMargin; final float marginOffset = (float) mPageMargin / width; //当前页面的position(这个position是指在数据列表中的位置) final int currentPage = ii.position; //这里计算的是 当前页面视图滑动距离的比例 和 当前页面宽度比例 的比 //如果第二项是1的话,也就代表了 当前页面视图滑动距离(和页面宽度)的比例 final float pageOffset = (((float) xpos / width) - ii.offset) / (ii.widthFactor + marginOffset); //表示视图滑动的像素 final int offsetPixels = (int) (pageOffset * widthWithMargin); mCalledSuper = false; onPageScrolled(currentPage, pageOffset, offsetPixels); if (!mCalledSuper) { throw new IllegalStateException( "onPageScrolled did not call superclass implementation"); } return true; }
onPageScrolled(currentPage, pageOffset, offsetPixels)
这个方法在很多滚动的地方都会被调用,我们可以重写这个方法实现一些动画效果。纵观这个方法,做了这么几件事:
1.对DecorView进行滚动
2.回调接口的onPageScrolled(),就是我们自己添加的接口。
3.实现动画
protected void onPageScrolled(int position, float offset, int offsetPixels) { // Offset any decor views if needed - keep them on-screen at all times. //1 if (mDecorChildCount > 0) { 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); } } } //2 dispatchOnPageScrolled(position, offset, offsetPixels); //3 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; final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); mPageTransformer.transformPage(child, transformPos); } } mCalledSuper = true; }
回过头来小结一下,onInterceptTouchEvent()中会根据不同情况对mIsBeingDragged进行赋值,在MOVE中如果是可滑动的,就调用scrollTo对视图进行滑动形成拖拽效果,接着pageScrolled()中获得当前页面的信息和偏移量传入onPageScrolled(),onPageScrolled()中对Decor View进行位移,回调接口,产生动画。
onInterceptTouchEvent()产生了拖动效果,但主要还是对是否拦截作出判断,接下来我们看一下onTouchEvent()。
onTouchEvent()
纵观整个方法,MOVE中依旧是拖动,而UP的时候会根据 当前页面、当前页面的offset、速度、横向移动距离 计算出下一个应该显示的页面nextPage,接着调用setCurrentItemInternal()产生滑动。
@Override public boolean onTouchEvent(MotionEvent ev) { //一些情况的判断,省略 ... mVelocityTracker.addMovement(ev); final int action = ev.getAction(); boolean needsInvalidate = false; switch (action & MotionEventCompat.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { //立即将Scroller中的x,y设为终值 mScroller.abortAnimation(); mPopulatePending = false; //根据mCurIndex更新需要缓存的页面信息 populate(); // Remember where the motion event started //记录 mLastMotionX = mInitialMotionX = ev.getX(); mLastMotionY = mInitialMotionY = ev.getY(); mActivePointerId = ev.getPointerId(0); break; } case MotionEvent.ACTION_MOVE: //如果不在drag(这里有可能是因为没有消耗手势的子View,所以返回来让ViewPager处理) if (!mIsBeingDragged) { final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) { // A child has consumed some touch events and put us into an inconsistent // state. needsInvalidate = resetTouch(); break; } //计算横向和纵向偏移 final float x = ev.getX(pointerIndex); final float xDiff = Math.abs(x - mLastMotionX); final float y = ev.getY(pointerIndex); final float yDiff = Math.abs(y - mLastMotionY); //如果横向偏移足够大 且 横向偏移大于纵向偏移则可以开始drag if (xDiff > mTouchSlop && xDiff > yDiff) { if (DEBUG) Log.v(TAG, "Starting drag!"); mIsBeingDragged = true; requestParentDisallowInterceptTouchEvent(true); mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop; mLastMotionY = y; setScrollState(SCROLL_STATE_DRAGGING); setScrollingCacheEnabled(true); // Disallow Parent Intercept, just in case ViewParent parent = getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } // Not else! Note that mIsBeingDragged can be set above. //如果可以drag if (mIsBeingDragged) { // Scroll to follow the motion event final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); //实现拖动,performDrag上面已经分析过了 needsInvalidate |= performDrag(x); } break; case MotionEvent.ACTION_UP: //如果是拖动的时候up if (mIsBeingDragged) { //计算x速度 final VelocityTracker velocityTracker = mVelocityTracker; velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( velocityTracker, mActivePointerId); mPopulatePending = true; //ViewPager显示宽度 final int width = getClientWidth(); //视图横向滑动距离 final int scrollX = getScrollX(); //根据scrollX计算出当前的页面信息。 final ItemInfo ii = infoForCurrentScrollPosition(); //边缘占比 final float marginOffset = (float) mPageMargin / width; //当前页面在数据列表中的位置 final int currentPage = ii.position; //计算当前页面偏移 final float pageOffset = (((float) scrollX / width) - ii.offset) / (ii.widthFactor + marginOffset); //横向偏移 final int activePointerIndex = ev.findPointerIndex(mActivePointerId); final float x = ev.getX(activePointerIndex); final int totalDelta = (int) (x - mInitialMotionX); //确定up后终态所在的页面 int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, totalDelta); //重要,这里是产生滑动的关键 setCurrentItemInternal(nextPage, true, true, initialVelocity); needsInvalidate = resetTouch(); } break; ... case MotionEventCompat.ACTION_POINTER_DOWN: { final int index = MotionEventCompat.getActionIndex(ev); final float x = ev.getX(index); //多点触摸,换了另外一个手指过后更新mLastMotionX和mActivePointerId mLastMotionX = x; mActivePointerId = ev.getPointerId(index); break; } case MotionEventCompat.ACTION_POINTER_UP: //貌似是多点触摸下一个手指抬起了,要更新mLastMotionX onSecondaryPointerUp(ev); mLastMotionX = ev.getX(ev.findPointerIndex(mActivePointerId)); break; } if (needsInvalidate) { ViewCompat.postInvalidateOnAnimation(this); } return true; }
setCurrentItemInternal()
这个方法的作用是判断应该交给onMeasure()还是scrollToItem()去完成页面的set。
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { //一些健壮性判断,省略 ... final int pageLimit = mOffscreenPageLimit; //有关跳跃性滑动,跳跃的过程中我们不会让涉及到的页面被更新 if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { // We are doing a jump by more than one page. To avoid // glitches, we want to keep all current pages in the view // until the scroll ends. for (int i = 0; i < mItems.size(); i++) { mItems.get(i).scrolling = true; } } final boolean dispatchSelected = mCurItem != item; //还记得我们上一篇开始的时候我们会进入到这个分支,但是这里不会了。 if (mFirstLayout) { mCurItem = item; if (dispatchSelected) { dispatchOnPageSelected(item); } requestLayout(); } else {//这里我们更新页面信息,并且滑动到目标页面 populate(item); scrollToItem(item, smoothScroll, velocity, dispatchSelected); } }
scrollToItem()
根据是否为smoothScroll来进行不同的滑动,smoothScrollTo()或者直接scrollTo()。
private void scrollToItem(int item, boolean smoothScroll, int velocity, boolean dispatchSelected) { final ItemInfo curInfo = infoForPosition(item); int destX = 0; if (curInfo != null) { 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); } //直接用scrollTo的方式结束滑动 completeScroll(false); scrollTo(destX, 0); pageScrolled(destX); } }
smoothScrollTo()
void smoothScrollTo(int x, int y, int velocity) { ... //x轴滑动起始位置 int sx; boolean wasScrolling = (mScroller != null) && !mScroller.isFinished(); //如果此时在滚动 if (wasScrolling) { //更新起始位置 sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX(); mScroller.abortAnimation(); setScrollingCacheEnabled(false); } else { sx = getScrollX(); } //y轴滑动起始位置 int sy = getScrollY(); int dx = x - sx; int dy = y - sy; if (dx == 0 && dy == 0) { completeScroll(false); populate(); setScrollState(SCROLL_STATE_IDLE); return; } setScrollingCacheEnabled(true); setScrollState(SCROLL_STATE_SETTLING); final int width = getClientWidth(); final int halfWidth = width / 2; //滑动距离,距离影响时间 final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio); //滑动时间 int duration; velocity = Math.abs(velocity); if (velocity > 0) { duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); } else { final float pageWidth = width * mAdapter.getPageWidth(mCurItem); final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); duration = (int) ((pageDelta + 1) * 100); } duration = Math.min(duration, MAX_SETTLE_DURATION); // Reset the "scroll started" flag. It will be flipped to true in all places // where we call computeScrollOffset(). mIsScrollStarted = false; //重要的地方,就是利用Scroller产生弹性滑动 mScroller.startScroll(sx, sy, dx, dy, duration); //重绘以便回调 ViewCompat.postInvalidateOnAnimation(this); }
computeScroll()
通过前面的分析我们知道了ViewPager是利用Scroller产生谈性滑动,Scroller产生弹性滑动的关键在于onDraw()中会回调computeScroll(),然后在这个方法里用scrollTo()滑动并再次申请重绘。ViewPager重写了这个方法,在调用scrollTo()之后还调用了pageScrolled(x)对Decor View进行更新、回调接口、产生动画。之后申请重绘。
@Override public void computeScroll() { mIsScrollStarted = true; if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { int oldX = getScrollX(); int oldY = getScrollY(); int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); if (oldX != x || oldY != y) { scrollTo(x, y); if (!pageScrolled(x)) { mScroller.abortAnimation(); scrollTo(0, y); } } // Keep on drawing until the animation has finished. ViewCompat.postInvalidateOnAnimation(this); return; } // Done with scroll, clean up state. completeScroll(true); }
小结
ViewPager的拖动和滑动都看完了,onInterceptTouchEvent()和onTouchEvent()的Move中都会对drag进行响应,通过scrollTo方法形成视图的移动,期间通过pageScrolled()完成相关事情的处理,包括Decor View、接口方法回调、动画;onTouchEvent()的Up中可能会产生平滑滑动,利用的是初始化时候定义的Scroller。
到这里对ViewPager的视图的移动流程就有了一个整体上的了解,一些细节我们就可以很快的定位了,接下来一篇想看一下ViewPager和Fragment的交互。
- ViewPager源码解析之拖动和滑动
- ViewPager源码解析之FragmentPagerAdapter和FragmentStatePagerAdapter
- ViewPager源码解析之ViewPager如何呈现
- android之scrollview滑动和地图拖动冲突
- ViewPager源码解析
- RecyclerView的拖动和滑动
- RecyclerView实现拖动和滑动
- ViewPager动态变换效果之SCViewPager源码解析
- Android之禁止ViewPager滑动
- ViewPager的滑动禁止和滑动启动
- ViewPager的滑动和滑动特效
- viewpager和滑动下游标
- ViewPager和SlidingPaneLayout滑动冲突
- 横向滑动ViewPager和Fragment
- tabLayout 和Viewpager 实现滑动
- viewpager和滑动下游标
- viewpager适配器和滑动监听
- 利用viewPager 制作滑动页面 源码
- 【JavaWeb_Part03】上位的小三之使用 JDBC 操纵 MySQL
- POJ 2329 Nearest number
- 设置文本文件数据
- atom及其插件安装(windwos平台)
- Android自定义View的事件分发机制(一)
- ViewPager源码解析之拖动和滑动
- 迅雷极速版下载Win10一周年正式版99.9%崩溃的两个解决方法
- Python中步长索引解析
- oracle命令分析3
- Java集合类一概述
- koa2实现mysql数据库分页
- PG的ALTER常用操作
- Qt中操作Excel
- ServerSocket和Socket建立通信(客户端发送消息服务器接收并返回到客户端接收输出)