View的滑動沖突及解決方案

當(dāng)父容器和子元素都可以滑動時渣磷,就會產(chǎn)生滑動沖突,比如ScrollView里面套一個ListView或者GridView,或者ListView中嵌套了ViewPager等等霹娄。

常見的滑動沖突場景有以下幾種:

  • 外部ViewGroup的滑動方向和內(nèi)部元素滑動方向不一致
  • 外部滑動方向和內(nèi)部滑動方法一致
  • 以上兩種情況的嵌套

第一種場景比如講ViewPager和Fragment配置使用所組成的頁面滑動效果觉啊。這種效果通過左右滑動來切換頁面拣宏,而每個頁面的內(nèi)部往往又是一個豎直滑動的ListView,本來這種情況是有滑動沖突的杠人,但是ViewPager內(nèi)部處理了這種沖突勋乾。如果才使用ScrollView而不是ViewPager,那么我們就必須手動來處理滑動沖突了搜吧,否則出現(xiàn)的后果就是內(nèi)外兩層只有一層能夠滑動市俊,

第二種場景當(dāng)內(nèi)外兩層在同一方向滑動時,如果手指觸摸開始滑動滤奈,但是系統(tǒng)無法知道到底要讓哪一層滑動摆昧,所以會出現(xiàn)這時候會出問題。

滑動沖突的處理規(guī)則

對于場景1的處理規(guī)則:當(dāng)手指左右滑動時蜒程,需要讓外部的元素?cái)r截點(diǎn)擊事件绅你,當(dāng)手指上下滑動時,需要讓內(nèi)部的元素?cái)r截點(diǎn)擊事件昭躺,這時候就可以根據(jù)元素的特性來解決沖突問題忌锯。具體一點(diǎn)就是根據(jù)滑動方向的不同來判斷到底由誰來攔截事件。如果是斜著滑動的领炫,那我們可以計(jì)算出兩點(diǎn)之間的水平距離和豎直距離偶垮,距離較大的那個方向判定為滑動方向,然后決定是攔截對應(yīng)的元素。

對于場景2和場景3的處理規(guī)則:這種情況無法根據(jù)滑動的角度距離差等因素來判斷似舵。但是我們可以根據(jù)不同的狀態(tài)來決定滑動哪個元素脚猾。

解決滑動沖突的方法:

攔截父容器

外部攔截是指所有的點(diǎn)擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要這個事件就攔截砚哗,不需要就不攔截龙助。外部攔截需要重寫父容器的onInterceptTouchEvent方法。

public boolean onInterceotTouchEvent(MotionEvent event) {
    
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) evnet.getY();

    switch(event.getAction) {

        case MotionEvent.ACTION_DOWN:
            intercepted = false;
            break;

        case MotionEvent.ACTION_MOVE:

            if (父容器是否需要處理當(dāng)前點(diǎn)擊事件) {

                intercepted = true;
            } else {
                intercepted = false;
            }

            break;

        case MotionEvent.ACTION_UP:

            intercepted = false;
            break;
    }

    mLastX = x;
    mLastY = y;
    return intercepted;

}

以上就是外部攔截的偽代碼蛛芥,在ACTION_DOWN事件中提鸟,父容器必須返回false,因?yàn)楦溉萜饕坏r截了ACTION_DOWN事件仅淑,那么后續(xù)一系列的ACTION_MOVE和ACTION_UP事件都會直接交給父容器處理称勋,此時事件就沒辦法再傳遞給子元素了,ACTION_UP一般也不需要攔截漓糙,本身也沒多大意義铣缠,ACTION_MOVE根據(jù)需要來決定是否攔截。

攔截子控件

內(nèi)部攔截是指父容器不攔截任何事件昆禽,所有的事件都交給子元素蝗蛙,如果子元素需要此事件就直接消耗,否則就交給父容器來處理醉鳖,這種方法和Android中事件分發(fā)機(jī)制不一致髓梅,需要配合requestDisallowInterceptTouchEvent方法才能正常工作泛鸟。內(nèi)部攔截需要重寫子元素的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
    
    int x = (int) event.getX();
    int y = (int) evnet.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 (父容器需要此類點(diǎn)擊事件) {
                parent.requestDisallowInterceptTouchEvent(false);
            }

            break;

        case MotionEvent.ACTION_UP:

            break;
    }

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

}

除了子元素需要做上述所示的修改外粥鞋,父元素也要默認(rèn)攔截除了ACTION_DOWN以外的其它事件尚辑,這樣當(dāng)子元素調(diào)用parent.requestDisallowInterceptTouchEvent(false);時父容器才能繼續(xù)攔截所需要的事件。

之所以父容器不能攔截ACTION_DOWN纹因,是因?yàn)锳CTION_DOWN事件不受FLAG_DISALLOW_INTERCEPT這個標(biāo)記位的影響喷屋,一旦父容器攔截了ACTION_DOWN,那么所有的事件都無法傳遞到子元素瞭恰,屯曹,內(nèi)部攔截就無效了。父容器要做的修改如下代碼所示:

public boolean onInterceptTouchEvent(MotionEvent event) {
    if(evnet.getAction == MotionEvent.ACTION_DOWN) {
        return false;    
    } else {
        return true;
    }
}

接下來通過一個demo來分別實(shí)現(xiàn)通過外部攔截和內(nèi)部攔截來解決滑動沖突惊畏。

我們先定義一個可以水平滑動的ViewGroup恶耽,其實(shí)就是仿ViewPager的功能,然后在里面添加若干個豎直滑動的ListView颜启,這就模擬了一個典型的滑動沖突的場景偷俭。根據(jù)滑動策略,我們可以選擇水平和豎直滑動距離差來解決滑動沖突缰盏。

public class CustomViewPager extends ViewGroup {

    private Scroller mScroller;
    private VelocityTracker mVelocityTracker;

    private int mChildWidth;// 子View的寬度
    private int mChildIndex;// 子View的位置索引
    private int mChildrenSize;// 子View個數(shù)

    private int mLastXIntercept = 0;
    private int mLastYIntercept = 0;

    private int mLastX = 0;
    private int mLastY = 0;


    public CustomViewPager(Context context) {
        this(context, null);
    }

    public CustomViewPager(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        mScroller = new Scroller(getContext());
        mVelocityTracker = VelocityTracker.obtain();
    }

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

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                if (!mScroller.isFinished()) {
                    // 如果父容器滑動沒有結(jié)束涌萤,那么下一個事件仍然交給父容器來處理淹遵,
                    // 避免當(dāng)父容器水平滑動沒有結(jié)束時,用戶立即豎直滑動负溪,而造成界面停留在中間狀態(tài)這個不好的體驗(yàn)合呐。
                    mScroller.abortAnimation();
                    intercepted = true;
                }
                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;
        }
        mLastX = x;
        mLastY = y;
        mLastXIntercept = x;
        mLastYIntercept = y;

        return intercepted;
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mVelocityTracker.addMovement(event);

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:

                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                scrollBy(-deltaX, 0);

                break;
            case MotionEvent.ACTION_UP:

                int scrollX = getScrollX();

                mVelocityTracker.computeCurrentVelocity(1000);
                // 計(jì)算水平速度
                float xVelocity = mVelocityTracker.getXVelocity();
                // 這里了是模擬ViewPager快速滑動時笙以,即使只滑動了一小段距離,也可以滑到下一頁去
                if (Math.abs(xVelocity) >= 50) {
                    mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
                } else {
                    mChildIndex = (scrollX + mChildWidth / 2) /mChildWidth;
                }
                mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));

                int dx = mChildIndex * mChildWidth - scrollX;

                mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
                invalidate();

                mVelocityTracker.clear();

                break;
        }

        mLastX = x;
        mLastY = y;

        return true;
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int measuredWidth = 0;
        int measuredHeight = 0;
        final int childCount = getChildCount();

        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        if (childCount == 0) {
            setMeasuredDimension(0, 0);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(widthSpaceSize, childView.getMeasuredHeight());
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            setMeasuredDimension(measuredWidth, heightSpaceSize);
        } else {
            final View childView = getChildAt(0);
            measuredWidth = childView.getMeasuredWidth() * childCount;
            measuredHeight = childView.getMeasuredHeight();
            setMeasuredDimension(measuredWidth, measuredHeight);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childLeft = 0;
        final int childCount = getChildCount();
        mChildrenSize = childCount;

        for (int i = 0; i < childCount; i++) {
            final View childView = getChildAt(i);
            if (childView.getVisibility() != View.GONE) {
                final int childWidth = childView.getMeasuredWidth();
                mChildWidth = childWidth;
                childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }


    @Override
    protected void onDetachedFromWindow() {
        mVelocityTracker.recycle();
        super.onDetachedFromWindow();
    }


}

content_layout布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="5dp"
        android:layout_marginTop="5dp"
        android:textColor="@android:color/white" />

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#fff4f7f9"
        android:cacheColorHint="#00000000"
        android:divider="#dddbdb"
        android:dividerHeight="1px"
        android:listSelector="@android:color/transparent" />

</LinearLayout>

在Activity的layout中加入我們自定義的ViewGroup:

<com.shenhuniurou.viewdemo.CustomViewPager
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_light">

</com.shenhuniurou.viewdemo.CustomViewPager>

Activity中的主要代碼:

private void initView() {
    LayoutInflater inflater = getLayoutInflater();
    mListContainer = (CustomViewPager) findViewById(R.id.container);
    int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;

    // 往ViewGroup中添加ListView冻辩,這里是把含有ListView的一整個布局加進(jìn)去
    for (int i = 0; i < 5; i++) {
        ViewGroup layout = (ViewGroup) inflater.inflate(R.layout.content_layout, mListContainer, false);
        layout.getLayoutParams().width = screenWidth;
        TextView textView = (TextView) layout.findViewById(R.id.title);
        textView.setText("這是第 " + (i + 1) + "頁");
        layout.setBackgroundColor(Color.rgb(255 / (i + 10), 255 / (i + 10), 10));
        addListView(layout);
        mListContainer.addView(layout);
    }
}

private void addListView(ViewGroup layout) {
    ListView listView = (ListView) layout.findViewById(R.id.list);
    List<String> datas = new ArrayList<>();
    for (int i = 0; i < 50; i++) {
        datas.add("item " + i);
    }

    ArrayAdapter<String> adapter = new ArrayAdapter<>(this, R.layout.content_list_item, R.id.name, datas);
    listView.setAdapter(adapter);
    listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @Override
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Toast.makeText(MainActivity.this, "click item", Toast.LENGTH_SHORT).show();
        }
    });
}

效果如圖:

攔截父容器解決滑動沖突效果示例

上面介紹了使用攔截父容器的方法猖腕,如果要從攔截子控件入手,又該怎么做呢恨闪?其實(shí)也很簡單倘感,原理就是上面所說的重寫ListView的dispatchTouchEvent方法,讓父容器攔截掉ACTION_MOVE事件咙咽,判斷水平方向和豎直方向滑動的距離誰大老玛,如果是水平方向滑動距離大,父容器就攔截MOVE事件钧敞。

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

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN: {
            // 讓父容器不攔截ACTION_DOWN事件
            customViewPager.requestDisallowInterceptTouchEvent(true);
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            int deltaX = x - mLastX;
            int deltaY = y - mLastY;
            if (Math.abs(deltaX) > Math.abs(deltaY)) {
                // 如果水平方向滑動的距離多一點(diǎn)蜡豹,那就表示讓父容器水平滑動,子控件不滑動溉苛,讓父容器攔截事件
                customViewPager.requestDisallowInterceptTouchEvent(false);
            }
            break;
        }
        case MotionEvent.ACTION_UP: {
            break;
        }
        default:
            break;
    }

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

另外為了避免父容器水平方向滑動還未停止但用戶立即開始豎直滑動時镜廉,父容器里面的子控件可能會停留在一個中間狀態(tài)的情況,當(dāng)水平方法還在滑動時愚战,讓父容器攔截事件去處理娇唯,所以這里還是要重寫父容器的onInterceptTouchEvent方法:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
            return true;
        }
        return false;
    } else {
        return true;
    }
}

以上就是解決外部ViewGroup的滑動方向和內(nèi)部元素滑動方向不一致這種場景的兩種方法了。如果是內(nèi)外元素滑動方向一致呢寂玲,又該怎么解決塔插?

這種情況解決辦法和上面的場景一樣,只是滑動規(guī)則不同而已拓哟,比如當(dāng)使用攔截父容器的方法時想许,就是在MOTIONEVENT.ACTION_MOVE事件中的判斷條件不同,根據(jù)我們自己的需求來定制彰檬,看當(dāng)前事件要交給父容器還是子元素來處理伸刃。也就是說上面那兩段偽代碼基本就是解決滑動沖突的通用方案了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逢倍,一起剝皮案震驚了整個濱河市捧颅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌较雕,老刑警劉巖碉哑,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件挚币,死亡現(xiàn)場離奇詭異,居然都是意外死亡扣典,警方通過查閱死者的電腦和手機(jī)妆毕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贮尖,“玉大人笛粘,你說我怎么就攤上這事∈酰” “怎么了薪前?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長关斜。 經(jīng)常有香客問我示括,道長,這世上最難降的妖魔是什么痢畜? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任垛膝,我火速辦了婚禮,結(jié)果婚禮上丁稀,老公的妹妹穿的比我還像新娘吼拥。我一直安慰自己,他們只是感情好线衫,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布扔罪。 她就那樣靜靜地躺著,像睡著了一般桶雀。 火紅的嫁衣襯著肌膚如雪矿酵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天矗积,我揣著相機(jī)與錄音全肮,去河邊找鬼。 笑死棘捣,一個胖子當(dāng)著我的面吹牛辜腺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乍恐,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼评疗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了茵烈?” 一聲冷哼從身側(cè)響起百匆,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎呜投,沒想到半個月后加匈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體存璃,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年雕拼,在試婚紗的時候發(fā)現(xiàn)自己被綠了纵东。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡啥寇,死狀恐怖偎球,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情辑甜,我是刑警寧澤甜橱,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站栈戳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏难裆。R本人自食惡果不足惜子檀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望乃戈。 院中可真熱鬧褂痰,春花似錦、人聲如沸症虑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谍憔。三九已至匪蝙,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間习贫,已是汗流浹背逛球。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苫昌,地道東北人颤绕。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像祟身,于是被迫代替她去往敵國和親奥务。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容