ViewPager源码解析

ViewPager的滑动手势冲突解决方案

所有解决滑动冲突的方法都避免不了要从
DispatchTouchEvent(MotionEvent ev); 事件分发
OnInterceptTouchEvent(MotionEvent ev); 事件拦截
OnTouchEvent(Motion ev); 事件消费
这三个回调方法入手,搞明白这个三个方法之间含义和关系,Android系统中的事件传递就基本明白了

1、OnInterceptTouchEvent(MotionEvent ev);

ViewPager里面没有复写DispatchTouchEvent方法,所以我们先从OnInterceptTouchEvent方法入手。

OnInterceptTouchEvent方法决定了ViewPager是否拦截当前的手势。如果返回true,则会调用OnTouchEvent方法,并在其中执行真正的滚动操作。

1.1 首先获取当前的手势动作,包括ACTION_DOWN、ACTION_MOVE、ACTION_CANCEL、ACTION_UP等。

final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

注:上述获得Action的方法中有一个按位与操作,目的是取得执行过的动作的掩码值,不包括点的索引值。

1.2 处理ACTION_CANCEL和ACTION_UP事件

if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            // Release the drag.
            if (DEBUG) Log.v(TAG, "Intercept done!");
            resetTouch();
            return false;
        }

首先判断当前手势是否是取消或抬起动作,如果是的话,则执行resetTouch()方法,并返回false。下面看下resetTouch()做了什么:

private boolean resetTouch() {
        boolean needsInvalidate;
        mActivePointerId = INVALID_POINTER;
        endDrag();
        needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease();
        return needsInvalidate;
    }

 private void endDrag() {
        mIsBeingDragged = false;
        mIsUnableToDrag = false;

        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

很简单,resetTouch()方法主要是做了一些变量重置、边界效果释放的操作。

加一个疑问:如果ViewPager处于横向滑动的中间,此时抬起手指,ViewPager会自动切换到上一页或下一页。这个操作并没有添加到resetTouch()方法。

1.3 处理非ACTION_DOWN事件

//如果不是ACTION_DOWN手势,检查当前拖动状态
if (action != MotionEvent.ACTION_DOWN) {
//如果正在被拖动,则返回true。Touch事件由ViewPager处理
            if (mIsBeingDragged) {
                if (DEBUG) Log.v(TAG, "Intercept returning true!");
                return true;
            }
            //如果不能被拖动,返回false。Touch由子视图处理
            if (mIsUnableToDrag) {
                if (DEBUG) Log.v(TAG, "Intercept returning false!");
                return false;
            }
        }

1.4 处理ACTION_DOWN事件

case MotionEvent.ACTION_DOWN: {
                //获得点击位置
                mLastMotionX = mInitialMotionX = ev.getX();
                mLastMotionY = mInitialMotionY = ev.getY();
                //获取指针id,并标记为活动态。这个id在整个触摸事件过程中是不变的
                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
                //标记为可拖拽
                mIsUnableToDrag = false;
                //标记开始滚动
                mIsScrollStarted = true;
                //通知Scroller计算需要滚动的偏移
                mScroller.computeScrollOffset();
                //SCROLL_STATE_SETTLING是一种趋向于最终位置过程的状态,也就是ViewPager正在向最终位置位移。
                //如果ViewPager正在向最终位置滚动,并且需要滚动的距离大于最小距离,就终止滚动动画。也就是说允许用户控制住ViewPager,尽管ViewPager处于动画的状态
                if (mScrollState == SCROLL_STATE_SETTLING &&
                        Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                    // Let the user 'catch' the pager as it animates.
                    //通知Scroller终止动画
                    mScroller.abortAnimation();
                    mPopulatePending = false;
                    //populate()是一个关键方法,下面会说到
                    populate();
                    //设置正在由用户拖拽标志位
                    mIsBeingDragged = true;
                   //设置父视图不要拦截触摸事件,由ViewPager处理
                  requestParentDisallowInterceptTouchEvent(true);
//设置滚动状态为“正在拖拽”                    setScrollState(SCROLL_STATE_DRAGGING);
                } else {
                //正常状态下应该走此逻辑。
                //
                    completeScroll(false);
                    mIsBeingDragged = false;
                }
                break;
            }

1.5 处理ACTION_MOVE手势

case MotionEvent.ACTION_MOVE: {
                 //基本动作就是检查用户手指是否移动了足够的距离,然后决定是否滚动ViewPager
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    //如果处于活动的触摸指针id是无效的,表明没有按压到内容上,直接返回。
                    break;
                }
//根据手指id,获取手指在多指触控列表中的索引
                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                //取到当前手指位移后的坐标,和ACTION_DOWN手势时获取原始坐标做对比,计算出x和y方向的差值的绝对值。
                final float x = MotionEventCompat.getX(ev, pointerIndex);
                final float dx = x - mLastMotionX;
                final float xDiff = Math.abs(dx);
                final float y = MotionEventCompat.getY(ev, pointerIndex);
                final float yDiff = Math.abs(y - mInitialMotionY);
                //接下来就是判断是否可拖拽

//dx!=0 横向滑动  
//mGutterSize ViewPager左右两边预留一定宽度的滑动无效区域,这个区域宽度是16dp和ViewPager宽度十分之一两者的最小值。
//!isGutterDrag(mLastMotionX, dx) 触摸点不在滑动无效区域内
//canScroll() 检查子视图横向可滚动性
//如果上述条件都满足,表明由嵌套的视图处理当前触摸事件。同时设置mIsUnableToDrag标志位为true
                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;
                }
                //xDiff > mTouchSlop 横向拖拽手势
                //xDiff * 0.5f > yDiff 横向移动距离的一半大于纵向移动距离
                //符合以上条件,表明用户想要横向滑动ViewPager,由ViewPager处理触摸事件
                if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {

                    mIsBeingDragged = true;
//禁止父视图处理事件                    requestParentDisallowInterceptTouchEvent(true);
//设置状态为正在拖拽                    、setScrollState(SCROLL_STATE_DRAGGING);
//修正原始触摸点坐标
                    mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                            mInitialMotionX - mTouchSlop;
                    mLastMotionY = y;
                    //这只子视图不缓存绘制数据
                    setScrollingCacheEnabled(true);
                } else if (yDiff > mTouchSlop) {
                   //用户纵向滑动,不能拖拽ViewPager
                    mIsUnableToDrag = true;
                }
                if (mIsBeingDragged) {
                  //performDrag(float x) 方法是处理拖拽的关键方法,该方法的返回值代表了是否需要进行由边界动画引起的页面刷新操作。
                    if (performDrag(x)) {
//如果拖拽到边界,会引起边界动画EdgeEffect.onPull(float deltaDistance),动画结束后一定要调用宿主View的invalidate()方法                        ViewCompat.postInvalidateOnAnimation(this);
                    }
                }
                break;
            }

1.6 为速度追踪器VelocityTracker添加手势事件

if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(ev);

       //经过上述几步,最终能判断出是否能够拖拽。只有在能够拖拽的情况下才拦截触摸手势
        return mIsBeingDragged;

总结:ViewPager属于只可以横向滚动的布局。通过分析其OnInterceptTouchEvent方法可以看出,横向拖拽手势肯定由ViewPager处理,通过设置requestParentDisallowInterceptTouchEvent(true)方法,禁止父视图处理触摸事件实现。纵向拖拽手势由子视图处理,OnInterceptTouchEvent方法直接返回false

最后,上面留下一个疑问:手指抬起时,ViewPager自动滚到上一页或下一页的操作没有放到resetTouch()方法中,而是在OnTouchEvent(MotionEvent ev)回调中对ACTION_UP和ACTION_CANCEL的判断分支中。

case MotionEvent.ACTION_UP:
                if (mIsBeingDragged) {
                    final VelocityTracker velocityTracker = mVelocityTracker;
                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                    int initialVelocity = (int) VelocityTrackerCompat.getXVelocity(
                            velocityTracker, mActivePointerId);
                    mPopulatePending = true;
                    final int width = getClientWidth();
                    final int scrollX = getScrollX();
                    final ItemInfo ii = infoForCurrentScrollPosition();
                    final int currentPage = ii.position;
                    final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor;
                    final int activePointerIndex =
                            MotionEventCompat.findPointerIndex(ev, mActivePointerId);
                    final float x = MotionEventCompat.getX(ev, activePointerIndex);
                    final int totalDelta = (int) (x - mInitialMotionX);
                    //根据当前页号、页面偏移、滑动速度、手指滑动总长度,计算出下一页的页号
                    int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity,
                            totalDelta);
                            //切换卡片
                    setCurrentItemInternal(nextPage, true, true, initialVelocity);

                    needsInvalidate = resetTouch();
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                if (mIsBeingDragged) {
                //如果是取消事件,则滚回到原始页号。scrollToItem方法是核心滚动方法
                    scrollToItem(mCurItem, true, 0, false);
                    needsInvalidate = resetTouch();
                }
                break;

ViewPager滚动动作分析

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)));
        }
        //判断是否是平滑的滚动
        //smoothScrollTo方法是由Scroller执行滚动
        //ScrollTo方法是由View类的onScrollChanged方法执行滚动
        if (smoothScroll) {
            smoothScrollTo(destX, 0, velocity);
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
        } else {
            if (dispatchSelected) {
                dispatchOnPageSelected(item);
            }
            completeScroll(false);
            scrollTo(destX, 0);
            pageScrolled(destX);
        }
    }

ViewPager拖拽动作分析

private boolean performDrag(float x) {
        boolean needsInvalidate = false;

        final float deltaX = mLastMotionX - x;
        mLastMotionX = x;

        float oldScrollX = getScrollX();
        float scrollX = oldScrollX + deltaX;
        final int width = getClientWidth();

        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);
        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;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android Launcher是Android系统上的一个应用程序,它是用户与系统交互的主要界面,也是用户启动应用程序的入口。它允许用户查看、操作和管理应用程序、小部件和系统功能。 Android Launcher的源码解析涉及到多个关键组件和类。其中最重要的是LauncherActivity、PackageManager、AppWidgetManager和DesktopPane。 LauncherActivity是应用程序启动的入口点。它负责显示主屏幕和响应用户的触摸事件。在LauncherActivity中,使用了ViewPager来创建多个屏幕来容纳应用程序和小部件。 PackageManager是应用程序的管理器。通过PackageManager,Launcher可以获取系统中安装的应用程序信息、启动应用程序和监听应用程序的安装、卸载等事件。 AppWidgetManager用于管理应用程序的小部件。Launcher通过AppWidgetManager注册、更新和删除小部件。它还负责接收小部件的更新事件。 DesktopPane是主屏幕的容器。它使用GridLayout将应用程序和小部件布局在主屏幕上。DesktopPane还处理用户在主屏幕上的拖放操作,允许用户重新排序应用程序和小部件。 在源码解析过程中,还需要了解Android应用程序交互的一些核心概念,如Intent、Broadcast和Service等。Intent用于在组件之间传递消息,Broadcast用于传递系统事件,Service用于在后台执行任务。 在分析Launcher源码时,还需要关注性能优化和用户体验。例如,使用异步加载和缓存来提高应用程序和小部件的加载速度,使用动画效果来增强界面的流畅性。 综上所述,Android Launcher源码解析涉及多个组件和类,需要了解Android应用程序交互的核心概念,同时需要关注性能优化和用户体验。这个过程可以帮助开发者深入理解和定制Android系统的启动器。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值