Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-ViewPager嵌套使用探究

Android-ViewPager嵌套使用探究

編輯:關於Android編程

終於不再是實習生,走出了窮逼的實習生生活,正式開始稍微不那麼窮逼的正職員工生活,三天的啟航培訓還是挺歡樂的,學到一句話,保持饑渴,哈哈,感覺放到哪方面都說得通。

回歸正題:

ViewPager的嵌套使用是一個很常見的問題,然而,最近又一次遇到ViewPager的嵌套使用問題。

情景是這樣的,需求上給出了這樣的要求,需要實現內外兩個ViewPager嵌套的效果,外部ViewPager控制著4個Tab的切換滑動,內部ViewPager控制著若干個二級Tab的滑動切換(這裡可能是廣告欄,也可能是榜單等),另外,當內部ViewPager滑動到最左或者最右的時候,外部ViewPager恢復滑動。

需求剛一看的時候,確實不難,於是我立馬想出了方案一。

方案一

解決兩個ViewPager滑動沖突問題,首先我們要知道為什麼會產生滑動沖突,通過查看源碼我們大致發現,是由於ViewPager的onInteruptTouchEvent()方法攔截了傳遞給子View的觸摸事件。

那麼從這個點出發,解決方法就是,重寫內部ViewPager,調用getParent().requestDisallowInterceptTouchEvent(true)方法來禁止外部ViewPager攔截觸摸事件,使得內部ViewPager可以滑動。

代碼:

package com.zero.viewpagerdemo.way1;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;

/**
 * @author linzewu
 * @date 16-7-12
 */
public class InnerViewPager extends ViewPager {


    public InnerViewPager(Context context) {
        super(context);
    }

    public InnerViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        getParent().requestDisallowInterceptTouchEvent(true);
        return super.onInterceptTouchEvent(ev);
    }
}

這個方案實現了兩個ViewPager嵌套時,保證了內部ViewPager能夠正常滑動,同時外部ViewPager可以滑動,但是,當手指觸摸范圍在內部ViewPager內的時候,而且內部ViewPager滑動了最左或者最右邊的時候,理想狀態下是此時外部ViewPager恢復響應,可以滑動,如果按照以上代碼,就實現不了這一步。

方案二:

既然我們要監控內部ViewPager是否滑動最左或者最右再來判斷是否讓觸摸事件傳遞給內部ViewPager,顯然從內部ViewPager去實現似乎不太理想,那就從外部ViewPager著手處理,
我們去試著重寫外部ViewPager.

於是,我想了一個能夠解決問題,但不是很高級的方法.
讓外部ViewPager持有內部ViewPager的引用,這樣外部ViewPager就可以在onInterceptTouchEvent()方法裡通過判斷內部ViewPager是否滑動到了最左或者最右。

package com.zero.viewpagerdemo.way2;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.List;


/**
 * 嵌套ViewPager外部ViewPager滑動沖突解決
 * 
 * @author linzewu
 * @date 16-7-12
 */
public class OuterViewPager extends ViewPager {
    public OuterViewPager(Context context) {
        super(context);
    }

    public OuterViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private float mDownX;
    private float mMoveX;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = ev.getRawX();
                break;
            case MotionEvent.ACTION_MOVE:
                mMoveX = ev.getRawX();
                InnerViewPager currentViewPager = getCurrentInnerViewPager();
                if (currentViewPager != null) {
                    if (mMoveX - mDownX > 0 && !isViewPagerReachLeft(getCurrentInnerViewPager()
                            .mViewPager)) {
                        return false;
                    } else if (mMoveX - mDownX <= 0 && !isViewPagerReachRight
                            (getCurrentInnerViewPager().mViewPager)) {
                        return false;
                    }
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }


    /**
     * 內部ViewPager集合
     */
    private List mInnerViewPagers;

    public void setInnerViewPagers(List innerViewPagers) {
        this.mInnerViewPagers = innerViewPagers;
    }

    /**
     * 內部ViewPager類
     */
    public static class InnerViewPager {
        ViewPager mViewPager;
        int mIndex;  //內部viewPager在外部ViewPager的位置值
        public InnerViewPager(ViewPager viewPager, int index) {
            this.mViewPager = viewPager;
            this.mIndex = index;
        }

    }

    private InnerViewPager getCurrentInnerViewPager() {
        if (mInnerViewPagers == null) {
            return null;
        }
        for (InnerViewPager innerViewPager : mInnerViewPagers) {
            if (innerViewPager.mIndex == getCurrentItem()) {
                return innerViewPager;
            }
        }
        return null;
    }

    private boolean isViewPagerReachLeft(ViewPager viewPager) {
        return viewPager.getCurrentItem() == 0;
    }

    private boolean isViewPagerReachRight(ViewPager viewPager) {
        return viewPager.getCurrentItem() >= viewPager.getChildCount() - 1
                && viewPager.getChildCount() == 2;
    }
}

采用重寫外部ViewPager的方式,由於需求需要,當內部ViewPager滑動到最左或者最右的時候,外部ViewPager能夠恢復響應,缺點:需要保留內部ViewPager的引用,導致該控件拓展性不高,這是一種很不高級的重寫方式。

方案三:

通過以上方案,其實已經是可以解決ViewPager的滑動問題,但是有另一個問題一直困擾著我.因為一開始使用ViewPager嵌套出現滑動沖突的情況,只是在2.3的機器上出現,而在高版本的機器例如5.0,使用ViewPager嵌套ViewPager,是不會出現滑動沖突的情況,這就相當奇怪了,既然高版本不會出現滑動沖突,為什麼在低版本上反而會出現滑動沖突呢?

帶著這些疑問,我開始去翻查資料.

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

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {

    ...//省略不展示

    switch (action) {
            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.
                 */

                /*
                * Locally do absolute value. mLastMotionY is set to the y value
                * of the down event.
                */
                final int activePointerId = mActivePointerId;
                if (activePointerId == INVALID_POINTER) {
                    // If we don't have a valid id, the touch down wasn't on content.
                    break;
                }

                final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
                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);
                if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

                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;
                }
                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;
            }

     ...
     }       

從上面代碼我們可以看出,當判斷為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的時候,不會判斷自己的可滑動性。

    /**
     * Tests scrollability within child views of v given a delta of dx.
     *
     * @param v View to test for horizontal scrollability
     * @param checkV Whether the view v passed should itself be checked for scrollability (true),
     *               or just its children (false).
     * @param dx Delta scrolled in pixels
     * @param x X coordinate of the active touch point
     * @param y Y coordinate of the active touch point
     * @return true if child views of v can be scrolled by delta of dx.
     */
    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);
    }

而在上文中,我們知道ViewPager在onInterceptTouchEvent()方法中調用的代碼是

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

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

再進一步查看ViewCompat.canScrollHorizontally()的源碼


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

canScrollHorizontally()用來判斷當前View的水平滑動性,也就是這個是否還可以滑動。我們發現,這個方法在不同的版本,有不同的方法體。

    static final ViewCompatImpl IMPL;
    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 23) {
            IMPL = new MarshmallowViewCompatImpl();
        } else if (version >= 21) {
            IMPL = new LollipopViewCompatImpl();
        } else if (version >= 19) {
            IMPL = new KitKatViewCompatImpl();
        } else if (version >= 17) {
            IMPL = new JbMr1ViewCompatImpl();
        } else if (version >= 16) {
            IMPL = new JBViewCompatImpl();
        } else if (version >= 15) {
            IMPL = new ICSMr1ViewCompatImpl();
        } else if (version >= 14) {
            IMPL = new ICSViewCompatImpl();
        } else if (version >= 11) {
            IMPL = new HCViewCompatImpl();
        } else if (version >= 9) {
            IMPL = new GBViewCompatImpl();
        } else if (version >= 7) {
            IMPL = new EclairMr1ViewCompatImpl();
        } else {
            IMPL = new BaseViewCompatImpl();
        }
    }

當API 小於7 或者 小於9 或者 小於11 或者 小於14的時候,也就是IMPL的實例為BaseViewCompatImpl,GBViewCompatImpl, EclairMr1ViewCompatImpl,HCViewCompatImpl,它們的canScrollHorizontally方法體是一樣的,因為基礎類都是BaseViewCompatImpl.

public boolean canScrollHorizontally(View v, int direction) {
      return (v instanceof ScrollingView) &&
                canScrollingViewScrollHorizontally((ScrollingView) v, direction); 
  }

只有當View實現ScrollingView這個接口的時候,才不會返回false,但是我們都知道ViewPager是繼承ViewGroup而來,也沒有實現這個接口,因此這裡肯定是返回false了。

然而當API大於等於14,IMPL的實例為ICSViewCompatImpl,這時候復寫了canScrollHorizontally()方法。

@Override
public boolean canScrollHorizontally(View v, int direction) {
      return ViewCompatICS.canScrollHorizontally(v, direction);
}

我們繼續跟蹤進入,發現最後調用的是View.canScrollHorizontally()方法。

    /**
     * Check if this view can be scrolled horizontally in a certain direction.
     *
     * @param direction Negative to check scrolling left, positive to check scrolling right.
     * @return true if this view can be scrolled in the specified direction, false otherwise.
     */
    public boolean canScrollHorizontally(int direction) {
        final int offset = computeHorizontalScrollOffset();
        final int range = computeHorizontalScrollRange() - computeHorizontalScrollExtent();
        if (range == 0) return false;
        if (direction < 0) {
            return offset > 0;
        } else {
            return offset < range - 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一樣的效果。

package com.zero.viewpagerdemo.way3;

import android.content.Context;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

/**
 * @author linzewu
 * @date 16-7-12
 */
public class CompatibleViewPager extends ViewPager {
    public CompatibleViewPager(Context context) {
        super(context);
    }

    public CompatibleViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    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;
                }
            }
        }

        if (checkV) {
            if (v instanceof ViewPager) {
                return ((ViewPager)v).canScrollHorizontally(-dx);
            } else {
                return ViewCompat.canScrollHorizontally(v, -dx);
            }
        } else {
            return false;
        }
    }
}

最後,通過在demo的試驗,發現通過這種方式去重寫ViewPager,確實可以解決ViewPager嵌套使用的滑動沖突問題,而且暫時沒有發現一些其他的問題。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved