一文解決Android View滑動沖突 [復制鏈接]

2017-12-22 10:56
kengsirLi 閱讀:1012 評論:0 贊:1
Tag:  

當我們內外兩層View都可以滑動時候,就會產生滑動沖突!

常見的滑動沖突場景:

滑動沖突.png

  • 1.外層與內層滑動方向不一致,外層ViewGroup是可以橫向滑動的,內層View是可以豎向滑動的(類似ViewPager,每個頁面里面是ListView)
  • 2.外層與內層滑動方向一致,外層ViewGroup是可以豎向滑動的,內層View同樣也是豎向滑動的(類似ScrollView包裹ListView)

當然還有上面兩種組合起來,三層或者多層嵌套產生的沖突,然而不管是多么復雜,解決的思路都是一模一樣。所以遇到多層嵌套的小伙伴也不用驚慌,一層一層處理即可。

有小伙伴肯定有疑問,ViewPager帶ListView并沒有出現滑動沖突啊。
那是因為ViewPager已經為我們處理了滑動沖突!如果我們自己定義一個水平滑動的ViewGroup內部再使用ListView,那么是一定需要處理滑動沖突的。

針對上面第一種場景,由于外部與內部的滑動方向不一致,那么我們可以根據當前滑動方向,水平還是垂直來判斷這個事件到底該交給誰來處理。至于如何獲得滑動方向,我們可以得到滑動過程中的兩個點的坐標。一般情況下根據水平和豎直方向滑動的距離差就可以判斷方向,當然也可以根據滑動路徑形成的夾角(或者說是斜率如下圖)、水平和豎直方向滑動速度差來判斷。

ViewPager當斜率小于0.5時判斷為橫向滑動,攔截事件

有興趣的小伙伴可以看ViewPager源碼分析(2):滑動及沖突處理

針對第二種場景,由于外部與內部的滑動方向一致,那么不能根據滑動角度、距離差或者速度差來判斷。這種情況下必需通過業務邏輯來進行判斷。比較常見ScrollView嵌套了ListView。雖然需求不同,業務邏輯自然也不同,但是解決滑動沖突的方式都是一樣的。下面為大家截取了微博和天貓當中的同方向滑動沖突場景,方便大家更直觀的感受這個場景。

同方向,豎向滑動沖突

微博的這個是同方向,豎向滑動沖突的場景,可以看到發現布局整體是可以滾動的,而且下方的微博列表也是可以滾動的。根據業務邏輯,當熱門,榜單...這一行標簽欄滑動到頂部的時候微博列表才可以滾動。否則就是發現布局的整體滾動。這個場景是不是在很多app里面都能夠見到呢!

同方向,橫向滑動沖突

天貓的這個是同方向,橫向滑動沖突的場景,內外兩層都是可以橫向滾動的。它的處理邏輯也很明顯,根據用戶滑動的位置來判斷到底是那個View需要響應滑動。

上述兩種滑動沖突的場景區別只是在于攔截的邏輯處理上。第一種是根據水平還是豎直滑動來判斷誰來處理滑動,第二種是根據業務邏輯來判斷誰來處理滑動,但是處理的套路都是一樣的

滑動沖突解決套路


套路一 外部攔截法:

即父View根據需要對事件進行攔截。邏輯處理放在父View的onInterceptTouchEvent方法中。我們只需要重寫父View的onInterceptTouchEvent方法,并根據邏輯需要做相應的攔截即可。

    public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean intercepted = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                intercepted = false;
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (滿足父容器的攔截要求) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                intercepted = false;
                break;
            }
            default:
                break;
        }
        mLastXIntercept = x;
        mLastYIntercept = y;
        return intercepted;
    }

上面偽代碼表示外部攔截法的處理思路,需要注意下面幾點

  • 根據業務邏輯需要,在ACTION_MOVE方法中進行判斷,如果需要父View處理則返回true,否則返回false,事件分發給子View去處理。
  • ACTION_DOWN 一定返回false,不要攔截它,否則根據View事件分發機制,后續ACTION_MOVE 與 ACTION_UP事件都將默認交給父View去處理!
  • 原則上ACTION_UP也需要返回false,如果返回true,并且滑動事件交給子View處理,那么子View將接收不到ACTION_UP事件,子View的onClick事件也無法觸發。而父View不一樣,如果父View在ACTION_MOVE中開始攔截事件,那么后續ACTION_UP也將默認交給父View處理!

套路二 內部攔截法:

即父View不攔截任何事件,所有事件都傳遞給子View,子View根據需要決定是自己消費事件還是給父View處理。這需要子View使用requestDisallowInterceptTouchEvent方法才能正常工作。下面是子View的dispatchTouchEvent方法的偽代碼:

    public boolean dispatchTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                parent.requestDisallowInterceptTouchEvent(true);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                if (父容器需要此類點擊事件) {
                    parent.requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_UP: {
                break;
            }
            default:
                break;
        }

        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(event);
    }

父View需要重寫onInterceptTouchEvent方法:

    public boolean onInterceptTouchEvent(MotionEvent event) {

        int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            return false;
        } else {
            return true;
        }
    }

使用內部攔截法需要注意:

  • 內部攔截法要求父View不能攔截ACTION_DOWN事件,由于ACTION_DOWN不受FLAG_DISALLOW_INTERCEPT標志位控制,一旦父容器攔截ACTION_DOWN那么所有的事件都不會傳遞給子View。
  • 滑動策略的邏輯放在子View的dispatchTouchEvent方法的ACTION_MOVE中,如果父容器需要獲取點擊事件則調用 parent.requestDisallowInterceptTouchEvent(false)方法,讓父容器去攔截事件。

滑動沖突解決示例代碼


理論最終的落腳是在實踐,下面我通過一個例子來演示外部解決法和內部解決法解決滑動沖突,大家只要get到了精髓,那么今后遇到滑動沖突問題都將迎刃而解,不再是開發攔路虎!

我們一開始說過ViewPager已經默認給我們處理了滑動沖突,而它作為ViewGroup使用的是外部攔截法解決的沖突,即在onInterceptTouchEvent方法中進行判斷的。為了造成滑動沖突場景,那么我們自定義一個ViewPager,重寫onInterceptTouchEvent方法并默認返回false,如下所示:

public class BadViewPager extends ViewPager {
    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
}

這樣,一個好好的ViewPager就被我們玩壞了!


接下來新建一個ScrollConflicActivity用來測試滑動沖突。

public class ScrollConflictActivity extends BaseActivity {
    private BadViewPager mViewPager;
    private List<View> mViews;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_scroll_conflict);
        initViews();
        initData(false);
    }

    protected void initViews() {
        mViewPager = findAviewById(R.id.viewpager);
        mViews = new ArrayList<>();
    }

    protected void initData(final boolean isListView) {
        //初始化mViews列表
        Flowable.just("view1", "view2", "view3", "view4").subscribe(new Consumer<String>() {
            @Override
            public void accept(String s) throws Exception {
                //當前View
                View view;
                if (isListView) {
                    //初始化ListView
                    ListView listView = new ListView(mContext);
                    final ArrayList<String> datas = new ArrayList<>();
                    //初始化數據,datas ,data0 ...data49
                    Flowable.range(0, 50).subscribe(new Consumer<Integer>() {
                        @Override
                        public void accept(Integer integer) throws Exception {
                            datas.add("data" + integer);
                        }
                    });
                    //初始化adapter
                    ArrayAdapter<String> adapter = new ArrayAdapter<>
                            (mContext, android.R.layout.simple_list_item_1, datas);
                    //設置adapter
                    listView.setAdapter(adapter);
                    //將ListView賦值給當前View
                    view = listView;
                } else {
                    //初始化TextView
                    TextView textView = new TextView(mContext);
                    textView.setGravity(Gravity.CENTER);
                    textView.setText(s);
                    //將TextView賦值給當前View
                    view = textView;
                }
                //將當前View添加到ViewPager的ViewList中去
                mViews.add(view);
            }
        });
        //設置ViewPager的Adapter
        mViewPager.setAdapter(new BasePagerAdapter<>(mViews));
    }
}

注:Flowable是RxJava2的方法,這里其實用for循環也是一樣的
BasePagerAdapter是BaseProject里的方法

上面的代碼我們使用了BadViewPager,初始化了BadViewPager里面的子View。
initData(false);方法傳false表示里面的子View是一個TextView,傳true表示里面的子View是ListView。首先我們看BadViewPager里面子View是TextView是否可以滑動。

BadViewPager_bad_textview.gif

似乎對BadViewPager的滑動沒有任何影響。



大家別急,我們來分析一下,BadViewPager的onInterceptTouchEvent默認返回false則所有事件都會給子View去處理。大家是否還記得上一篇說到的View處理事件的原則?

View 的onTouchEvent 方法默認都會消費掉事件(返回true),除非它是不可點擊的(clickable和longClickable同時為false),View的longClickable默認為false,clickable需要區分情況,如Button的clickable默認為true,而TextView的clickable默認為false。

所以TextView默認并沒有消費事件,因為他是不可點擊的。事件會交由父View即BadViewPager的onTouchEvent方法去處理。所以它自然是可以滑動的。

我們將textview的Clickable設置成true,即讓它來消費事件。大家再看看呢

BadViewPager_bad_textview_clickable.gif

所以我們不難推測如果將TextView換成Button,將是一樣的無法滑動的效果。雖然這并不是常規的滑動沖突(子View不是滑動的),但是造成的原因其實是一樣的,沒有做滑動判斷導致父View不能正確響應滑動事件。

接下來稍稍修改一下代碼 initData(true);傳入true,即BadViewPager的子View使用ListView,顯然ListView是可以滑動的,BadViewPager是不能滑動的。我們分別通過外部攔截和內部攔截方法來對BadViewPager進行修復。

BadViewPager_bad_listview.gif

1.外部攔截法Fix BadViewPager:

public class BadViewPager extends ViewPager {
    private static final String TAG = "BadViewPager";

    private int mLastXIntercept;
    private int mLastYIntercept;

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                //調用ViewPager的onInterceptTouchEvent方法初始化mActivePointerId
                super.onInterceptTouchEvent(ev);
                break;
            case MotionEvent.ACTION_MOVE:
                //橫坐標位移增量
                int deltaX = x - mLastXIntercept;
                //縱坐標位移增量
                int deltaY = y - mLastYIntercept;
                if (Math.abs(deltaX)>Math.abs(deltaY)){
                    intercepted = true;
                }else{
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }

        mLastXIntercept = x;
        mLastYIntercept = y;

        LogUtil.e(TAG,"intercepted = "+intercepted);
        return intercepted;
    }
}

根據我們的外部攔截法的套路,需要重寫BadViewPager 的onInterceptTouchEvent方法,并且ACTION_DOWN 和 ACTION_UP返回為false。處理邏輯在ACTION_MOVE中,Math.abs(deltaX)>Math.abs(deltaY)表示橫向位移增量大于豎向位移增量,即水平滑動,則BadViewPager 攔截事件。

這里我們在ACTION_DOWN 當中還調用了super.onInterceptTouchEvent(ev);即ViewPager的onInterceptTouchEvent方法。主要是為了初始化ViewPager的成員變量mActivePointerId。mActivePointerId默認值為-1,在ViewPager的onTouchEvent方法的ACTION_MOVE中有這么一段:

class ViewPager
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        ...
        switch (action & MotionEventCompat.ACTION_MASK) {
            case MotionEvent.ACTION_MOVE:
                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;
                    }
                    //具體的滑動操作...
                }
                ...
                break;
                ...
        }
        ...
    }

假如mActivePointerId不進行初始化,ViewPager會認為這個事件已經被子View給消費了,然后break掉,接下來的滑動操作也就不執行了。

BadViewPager_fixed_listview.gif

2.內部攔截法Fix BadViewPager:

內部攔截法需要重寫ListView的dispatchTouchEvent方法,所以我們自定義一個ListView:

public class FixListView extends ListView {
    private static final String TAG = "FixListView";

    private int mLastX;
    private int mLastY;

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

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                //水平移動的增量
                int deltaX = x - mLastX;
                //豎直移動的增量
                int deltaY = y - mLastY;
                //當水平增量大于豎直增量時,表示水平滑動,此時需要父View去處理事件
                if (Math.abs(deltaX) > Math.abs(deltaY)){
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return super.dispatchTouchEvent(ev);
    }
}

再看BadViewPager,需要重寫攔截方法

public class BadViewPager extends ViewPager {
    private static final String TAG = "BadViewPager";

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction() & MotionEvent.ACTION_MASK;
        if (action == MotionEvent.ACTION_DOWN){
            super.onInterceptTouchEvent(ev);
            return false;
        }
        return true;
    }
}

可以看到和我們的套路代碼基本上一樣,只是ACTION_MOVE中有我們自己的邏輯處理,處理的方式與外部攔截法也是一致的,并且效果也一樣,在此不作贅述。

大家只用掌握上述滑動沖突的解決套路,不論場景是不同方向,還是同方向,還是亂七八糟的堆加在一起,就用套路去解決,萬變不離其宗。根據上述的外部攔截和內部攔截法,可以看出外部攔截法實現起來更加簡單,而且也符合View的正常事件分發機制,所以推薦使用外部攔截法(重寫父View的onInterceptTouchEvent,父View決定是否攔截)來處理滑動沖突



我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

时时彩改欢乐生肖 江苏十一选五走势图 310大赢家比分网 信阳做燃烧粿粒的厂赚钱吗 上海时时乐 南宁卖包子赚钱 倚天赚钱 捕鱼游戏上下分 新疆35选7开奖结果今天 工厂上班做什么生意最赚钱 中国福利彩票官方网站 安卓网球比分扳 娱乐场事故视频 上市公司的股东如何赚钱 彩都会彩票首页 亲朋棋牌个人中心登录 北京快3基本走势