• Android-ViewPager嵌套使用探究


    回归正题:

    ViewPager的嵌套使用是一个很常见的问题,然而,最近又一次遇到ViewPager的嵌套使用问题。

    情景是这样的,需求上给出了这样的要求,需要实现内外两个ViewPager嵌套的效果,外部ViewPager控制着4个Tab的切换滑动,内部ViewPager控制着若干个二级Tab的滑动切换(这里可能是广告栏,也可能是榜单等),另外,当内部ViewPager滑动到最左或者最右的时候,外部ViewPager恢复滑动。

    需求刚一看的时候,确实不难,于是我立马想出了方案一。

    方案一

    解决两个ViewPager滑动冲突问题,首先我们要知道为什么会产生滑动冲突,通过查看源码我们大致发现,是由于ViewPager的onInteruptTouchEvent()方法拦截了传递给子View的触摸事件。

    那么从这个点出发,解决方法就是,重写内部ViewPager,调用getParent().requestDisallowInterceptTouchEvent(true)方法来禁止外部ViewPager拦截触摸事件,使得内部ViewPager可以滑动。

    代码:

    1. package com.zero.viewpagerdemo.way1;
    2. import android.content.Context;
    3. import android.support.v4.view.ViewPager;
    4. import android.util.AttributeSet;
    5. import android.view.MotionEvent;
    6. /**
    7. * @author linzewu
    8. * @date 16-7-12
    9. */
    10. public class InnerViewPager extends ViewPager {
    11. public InnerViewPager(Context context) {
    12. super(context);
    13. }
    14. public InnerViewPager(Context context, AttributeSet attrs) {
    15. super(context, attrs);
    16. }
    17. @Override
    18. public boolean onInterceptTouchEvent(MotionEvent ev) {
    19. getParent().requestDisallowInterceptTouchEvent(true);
    20. return super.onInterceptTouchEvent(ev);
    21. }
    22. }
    • 1

    这个方案实现了两个ViewPager嵌套时,保证了内部ViewPager能够正常滑动,同时外部ViewPager可以滑动,但是,当手指触摸范围在内部ViewPager内的时候,而且内部ViewPager滑动了最左或者最右边的时候,理想状态下是此时外部ViewPager恢复响应,可以滑动,如果按照以上代码,就实现不了这一步。

    方案二:

    既然我们要监控内部ViewPager是否滑动最左或者最右再来判断是否让触摸事件传递给内部ViewPager,显然从内部ViewPager去实现似乎不太理想,那就从外部ViewPager着手处理,
    我们去试着重写外部ViewPager.

    于是,我想了一个能够解决问题,但不是很高级的方法.
    让外部ViewPager持有内部ViewPager的引用,这样外部ViewPager就可以在onInterceptTouchEvent()方法里通过判断内部ViewPager是否滑动到了最左或者最右。

    1. package com.zero.viewpagerdemo.way2;
    2. import android.content.Context;
    3. import android.support.v4.view.ViewPager;
    4. import android.util.AttributeSet;
    5. import android.view.MotionEvent;
    6. import android.view.View;
    7. import java.util.List;
    8. /**
    9. * 嵌套ViewPager外部ViewPager滑动冲突解决
    10. *
    11. * @author linzewu
    12. * @date 16-7-12
    13. */
    14. public class OuterViewPager extends ViewPager {
    15. public OuterViewPager(Context context) {
    16. super(context);
    17. }
    18. public OuterViewPager(Context context, AttributeSet attrs) {
    19. super(context, attrs);
    20. }
    21. private float mDownX;
    22. private float mMoveX;
    23. @Override
    24. public boolean onInterceptTouchEvent(MotionEvent ev) {
    25. switch (ev.getAction()) {
    26. case MotionEvent.ACTION_DOWN:
    27. mDownX = ev.getRawX();
    28. break;
    29. case MotionEvent.ACTION_MOVE:
    30. mMoveX = ev.getRawX();
    31. InnerViewPager currentViewPager = getCurrentInnerViewPager();
    32. if (currentViewPager != null) {
    33. if (mMoveX - mDownX > 0 && !isViewPagerReachLeft(getCurrentInnerViewPager()
    34. .mViewPager)) {
    35. return false;
    36. } else if (mMoveX - mDownX <= 0 && !isViewPagerReachRight
    37. (getCurrentInnerViewPager().mViewPager)) {
    38. return false;
    39. }
    40. }
    41. break;
    42. case MotionEvent.ACTION_CANCEL:
    43. case MotionEvent.ACTION_UP:
    44. break;
    45. }
    46. return super.onInterceptTouchEvent(ev);
    47. }
    48. /**
    49. * 内部ViewPager集合
    50. */
    51. private List<InnerViewPager> mInnerViewPagers;
    52. public void setInnerViewPagers(List<InnerViewPager> innerViewPagers) {
    53. this.mInnerViewPagers = innerViewPagers;
    54. }
    55. /**
    56. * 内部ViewPager类
    57. */
    58. public static class InnerViewPager {
    59. ViewPager mViewPager;
    60. int mIndex; //内部viewPager在外部ViewPager的位置值
    61. public InnerViewPager(ViewPager viewPager, int index) {
    62. this.mViewPager = viewPager;
    63. this.mIndex = index;
    64. }
    65. }
    66. private InnerViewPager getCurrentInnerViewPager() {
    67. if (mInnerViewPagers == null) {
    68. return null;
    69. }
    70. for (InnerViewPager innerViewPager : mInnerViewPagers) {
    71. if (innerViewPager.mIndex == getCurrentItem()) {
    72. return innerViewPager;
    73. }
    74. }
    75. return null;
    76. }
    77. private boolean isViewPagerReachLeft(ViewPager viewPager) {
    78. return viewPager.getCurrentItem() == 0;
    79. }
    80. private boolean isViewPagerReachRight(ViewPager viewPager) {
    81. return viewPager.getCurrentItem() >= viewPager.getChildCount() - 1
    82. && viewPager.getChildCount() == 2;
    83. }
    84. }
    • 1

    采用重写外部ViewPager的方式,由于需求需要,当内部ViewPager滑动到最左或者最右的时候,外部ViewPager能够恢复响应,缺点:需要保留内部ViewPager的引用,导致该控件拓展性不高,这是一种很不高级的重写方式。

    方案三:

    通过以上方案,其实已经是可以解决ViewPager的滑动问题,但是有另一个问题一直困扰着我.因为一开始使用ViewPager嵌套出现滑动冲突的情况,只是在2.3的机器上出现,而在高版本的机器例如5.0,使用ViewPager嵌套ViewPager,是不会出现滑动冲突的情况,这就相当奇怪了,既然高版本不会出现滑动冲突,为什么在低版本上反而会出现滑动冲突呢?

    带着这些疑问,我开始去翻查资料.

    我们先找到ViewPager的onInterceptTouchEvent()方法

    1. @Override
    2. public boolean onInterceptTouchEvent(MotionEvent ev) {
    3. ...//省略不展示
    4. switch (action) {
    5. case MotionEvent.ACTION_MOVE: {
    6. /*
    7. * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
    8. * whether the user has moved far enough from his original down touch.
    9. */
    10. /*
    11. * Locally do absolute value. mLastMotionY is set to the y value
    12. * of the down event.
    13. */
    14. final int activePointerId = mActivePointerId;
    15. if (activePointerId == INVALID_POINTER) {
    16. // If we don't have a valid id, the touch down wasn't on content.
    17. break;
    18. }
    19. final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
    20. final float x = MotionEventCompat.getX(ev, pointerIndex);
    21. final float dx = x - mLastMotionX;
    22. final float xDiff = Math.abs(dx);
    23. final float y = MotionEventCompat.getY(ev, pointerIndex);
    24. final float yDiff = Math.abs(y - mInitialMotionY);
    25. if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);
    26. if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
    27. canScroll(this, false, (int) dx, (int) x, (int) y)) {
    28. // Nested view has scrollable area under this point. Let it be handled there.
    29. mLastMotionX = x;
    30. mLastMotionY = y;
    31. mIsUnableToDrag = true;
    32. return false;
    33. }
    34. if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
    35. if (DEBUG) Log.v(TAG, "Starting drag!");
    36. mIsBeingDragged = true;
    37. requestParentDisallowInterceptTouchEvent(true);
    38. setScrollState(SCROLL_STATE_DRAGGING);
    39. mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
    40. mInitialMotionX - mTouchSlop;
    41. mLastMotionY = y;
    42. setScrollingCacheEnabled(true);
    43. } else if (yDiff > mTouchSlop) {
    44. // The finger has moved enough in the vertical
    45. // direction to be counted as a drag... abort
    46. // any attempt to drag horizontally, to work correctly
    47. // with children that have scrolling containers.
    48. if (DEBUG) Log.v(TAG, "Starting unable to drag!");
    49. mIsUnableToDrag = true;
    50. }
    51. if (mIsBeingDragged) {
    52. // Scroll to follow the motion event
    53. if (performDrag(x)) {
    54. ViewCompat.postInvalidateOnAnimation(this);
    55. }
    56. }
    57. break;
    58. }
    59. ...
    60. }
    • 1

    从上面代码我们可以看出,当判断为ACTION_MOVE的时候,这里首先会判断dx不为0,如果为,0,同时再判断canScroll()方法返回是否为true,如果为true,则onInterceptTouchEvent整个方法体返回false,也就是当前ViewPager不拦截处理触摸滑动事件,那么这些一连串的触摸滑动事件则会传递给子ViewPager处理。

    再来看这个关键方法canScroll(),从官方的注释,我们可以知道这个方法的作用是测试某个View的可滑动性.参数checkV很重要,当checkV为ture,返回的是View本身加上子View的可滑动性,当checkV为false,返回的只是View的子View的可滑动性。

    我们可以看到代码中先判断v是否为ViewGroup,如果为ViewGroup,则依次递归调用canScroll()来判断子View的可滑动性,注意这里canScroll()的checkV的值为true.最后一行则是判断自己View本身的可滑动性,当然当checkV为false的时候,不会判断自己的可滑动性。

    1. /**
    2. * Tests scrollability within child views of v given a delta of dx.
    3. *
    4. * @param v View to test for horizontal scrollability
    5. * @param checkV Whether the view v passed should itself be checked for scrollability (true),
    6. * or just its children (false).
    7. * @param dx Delta scrolled in pixels
    8. * @param x X coordinate of the active touch point
    9. * @param y Y coordinate of the active touch point
    10. * @return true if child views of v can be scrolled by delta of dx.
    11. */
    12. protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    13. if (v instanceof ViewGroup) {
    14. final ViewGroup group = (ViewGroup) v;
    15. final int scrollX = v.getScrollX();
    16. final int scrollY = v.getScrollY();
    17. final int count = group.getChildCount();
    18. // Count backwards - let topmost views consume scroll distance first.
    19. for (int i = count - 1; i >= 0; i--) {
    20. // TODO: Add versioned support here for transformed views.
    21. // This will not work for transformed views in Honeycomb+
    22. final View child = group.getChildAt(i);
    23. if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
    24. y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
    25. canScroll(child, true, dx, x + scrollX - child.getLeft(),
    26. y + scrollY - child.getTop())) {
    27. return true;
    28. }
    29. }
    30. }
    31. return checkV && ViewCompat.canScrollHorizontally(v, -dx);
    32. }
    • 1

    而在上文中,我们知道ViewPager在onInterceptTouchEvent()方法中调用的代码是

    canScroll(this, false, (int) dx, (int) x, (int) y)
    • 1

    checkV的值为false,因此返回的是ViewPager的子View的可滑动性。

    再进一步查看ViewCompat.canScrollHorizontally()的源码

    1. /**
    2. * Check if this view can be scrolled horizontally in a certain direction.
    3. *
    4. * @param v The View against which to invoke the method.
    5. * @param direction Negative to check scrolling left, positive to check scrolling right.
    6. * @return true if this view can be scrolled in the specified direction, false otherwise.
    7. */
    8. public static boolean canScrollHorizontally(View v, int direction) {
    9. return IMPL.canScrollHorizontally(v, direction);
    10. }
    • 1

    canScrollHorizontally()用来判断当前View的水平滑动性,也就是这个是否还可以滑动。我们发现,这个方法在不同的版本,有不同的方法体。

    1. static final ViewCompatImpl IMPL;
    2. static {
    3. final int version = android.os.Build.VERSION.SDK_INT;
    4. if (version >= 23) {
    5. IMPL = new MarshmallowViewCompatImpl();
    6. } else if (version >= 21) {
    7. IMPL = new LollipopViewCompatImpl();
    8. } else if (version >= 19) {
    9. IMPL = new KitKatViewCompatImpl();
    10. } else if (version >= 17) {
    11. IMPL = new JbMr1ViewCompatImpl();
    12. } else if (version >= 16) {
    13. IMPL = new JBViewCompatImpl();
    14. } else if (version >= 15) {
    15. IMPL = new ICSMr1ViewCompatImpl();
    16. } else if (version >= 14) {
    17. IMPL = new ICSViewCompatImpl();
    18. } else if (version >= 11) {
    19. IMPL = new HCViewCompatImpl();
    20. } else if (version >= 9) {
    21. IMPL = new GBViewCompatImpl();
    22. } else if (version >= 7) {
    23. IMPL = new EclairMr1ViewCompatImpl();
    24. } else {
    25. IMPL = new BaseViewCompatImpl();
    26. }
    27. }
    • 1

    当API 小于7 或者 小于9 或者 小于11 或者 小于14的时候,也就是IMPL的实例为BaseViewCompatImpl,GBViewCompatImpl, EclairMr1ViewCompatImpl,HCViewCompatImpl,它们的canScrollHorizontally方法体是一样的,因为基础类都是BaseViewCompatImpl.

    1. public boolean canScrollHorizontally(View v, int direction) {
    2. return (v instanceof ScrollingView) &&
    3. canScrollingViewScrollHorizontally((ScrollingView) v, direction);
    4. }
    • 1

    只有当View实现ScrollingView这个接口的时候,才不会返回false,但是我们都知道ViewPager是继承ViewGroup而来,也没有实现这个接口,因此这里肯定是返回false了。

    然而当API大于等于14,IMPL的实例为ICSViewCompatImpl,这时候复写了canScrollHorizontally()方法。

    1. @Override
    2. public boolean canScrollHorizontally(View v, int direction) {
    3. return ViewCompatICS.canScrollHorizontally(v, direction);
    4. }
    • 1

    我们继续跟踪进入,发现最后调用的是View.canScrollHorizontally()方法。

    1. /**
    2. * Check if this view can be scrolled horizontally in a certain direction.
    3. *
    4. * @param direction Negative to check scrolling left, positive to check scrolling right.
    5. * @return true if this view can be scrolled in the specified direction, false otherwise.
    6. */
    7. public boolean canScrollHorizontally(int direction) {
    8. final int offset = computeHorizontalScrollOffset();
    9. final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
    10. if (range == 0) return false;
    11. if (direction < 0) {
    12. return offset > 0;
    13. } else {
    14. return offset < range - 1;
    15. }
    16. }
    • 1

    这样一来,我们可以做出总结了,当API小于14的时候,ViewPager的canScroll()方法无法获知子View的可滑动性(因为它只会默认返回false,总是告诉你子View不可滑动),当API大于等于14的时候,ViewPager的canScroll()方法能够最终调用到View.canScrollHorizontally(),即能够获知子View的可滑动性。

    同时,我们也知道了,为什么在2.3的机型上,使用ViewPager嵌套ViewPager会出现滑动冲突,而在高版本上,这种让人郁闷的现象却神奇地恢复了。

    那么,如果解决这个问题呢,既然canScroll()方法中,当API小于14的时候,默认返回false,我们可以试着让它最终去调用View.canScrollHorizontally(),实现和当API大于等于14一样的效果。

    1. package com.zero.viewpagerdemo.way3;
    2. import android.content.Context;
    3. import android.support.v4.view.ViewCompat;
    4. import android.support.v4.view.ViewPager;
    5. import android.util.AttributeSet;
    6. import android.view.View;
    7. import android.view.ViewGroup;
    8. /**
    9. * @author linzewu
    10. * @date 16-7-12
    11. */
    12. public class CompatibleViewPager extends ViewPager {
    13. public CompatibleViewPager(Context context) {
    14. super(context);
    15. }
    16. public CompatibleViewPager(Context context, AttributeSet attrs) {
    17. super(context, attrs);
    18. }
    19. @Override
    20. protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
    21. if (v instanceof ViewGroup) {
    22. final ViewGroup group = (ViewGroup) v;
    23. final int scrollX = v.getScrollX();
    24. final int scrollY = v.getScrollY();
    25. final int count = group.getChildCount();
    26. // Count backwards - let topmost views consume scroll distance first.
    27. for (int i = count - 1; i >= 0; i--) {
    28. // TODO: Add versioned support here for transformed views.
    29. // This will not work for transformed views in Honeycomb+
    30. final View child = group.getChildAt(i);
    31. if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() &&
    32. y + scrollY >= child.getTop() && y + scrollY < child.getBottom() &&
    33. canScroll(child, true, dx, x + scrollX - child.getLeft(),
    34. y + scrollY - child.getTop())) {
    35. return true;
    36. }
    37. }
    38. }
    39. if (checkV) {
    40. if (v instanceof ViewPager) {
    41. return ((ViewPager)v).canScrollHorizontally(-dx);
    42. } else {
    43. return ViewCompat.canScrollHorizontally(v, -dx);
    44. }
    45. } else {
    46. return false;
    47. }
    48. }
    49. }
    • 1
  • 相关阅读:
    vue3 ref 和 reactive 区别
    CG-68 冻土传感器浅谈工作原理
    1秒奇迹!你的桌面文件夹也可以瞬间整洁
    Celery框架从入门到精通
    【状态机模型】大盗阿福 买卖股票IV V
    [论文笔记]Prefix Tuning
    【已解决】设置SSH主机:VS Code-正在本地下载 VS Code 服务器
    Minio入门系列【1】Windows/Linux/K8S单机部署Minio
    echarts看板效果图:流光折线图、3d柱状图、3d饼图
    linux题带答案
  • 原文地址:https://blog.csdn.net/eeeeety6208/article/details/125401988