View的滑動沖突

一娃循、引言

上一篇文章中,我們講到了View的事件分發(fā) 機制斗蒋,明白了點擊事件是如何從根View一步步地傳遞到目標View的捌斧。

而在日常開發(fā)中,使用一些復雜的View嵌套時捞蚂,經(jīng)常會遇到滑動沖突的問題妇押。有時候單單是為了解決這樣一個滑動沖突就消耗了不少時間。

既然問題已經(jīng)被拋出來了柱嫌,那有什么通用的辦法解決呢?本文主要通過分析滑動沖突抑片,到解決滑動沖突,并給出相關的demo作為例子蚓峦,讓View的滑動沖突不再是一件難事很泊。

二、常見的滑動沖突

常見的滑動沖突可以分為以下三種:

2.1 場景一


外部滑動方向和內(nèi)部滑動方向不一致。這里主要表現(xiàn)為ViewPager和Fragment配合使用所組成的頁面滑動效果定罢。
在這種效果中耕皮,可以通過左右滑動來切換頁面,而每一個頁面內(nèi)部又是一個ListView秩彤。
本身這種情況是有滑動沖突的叔扼,但是ViewPager內(nèi)部處理了這種滑動沖突事哭,因此采用ViewPager時無須關注這個問題。但是瓜富,如果我們采用的不是ViewPager鳍咱,而是ScrollView,則必須手動解決滑動沖突与柑。否則造成的后果是內(nèi)外兩層只有一層是可以滑動的谤辜,而且會顯得很卡頓。當然价捧,還有其他情況丑念,比如說外部上下滑動、內(nèi)部左右滑動都屬于同一種類型的滑動沖突干旧。

當用戶左右滑動時渠欺,需要讓外部的View攔截點擊事件;而當用戶上下滑動時椎眯,需要讓內(nèi)部View攔截點擊事件挠将。
在滑動過程中,通過兩個點的坐標就可以得到目前的左右滑動還是水平滑動编整。當豎直方向的滑動距離差大于水平方向的滑動距離差時舔稀,就判斷為豎直滑動,否則判斷為水平滑動掌测。

2.2 場景二


外部滑動方向和內(nèi)部滑動方向一致内贮。當內(nèi)外兩層都在同一個方向可以滑動時,是存在邏輯上問題的汞斧。因為當手指開始滑動的時候夜郁,系統(tǒng)無法知道用戶到底是想讓哪一層滑動。

場景二無法像場景一那樣通過滑動的角度粘勒、距離差以及速度差來做判斷竞端,而一般這個時候則需要從業(yè)務上找到突破點,比如業(yè)務上有規(guī)定:當處于某種狀態(tài)時庙睡,需要外部View響應用戶的滑動事富,而處于另外一種狀態(tài)時則需要內(nèi)部View來響應View的滑動。根據(jù)這種業(yè)務上的需求我們可以得到響應的處理規(guī)則乘陪。這樣的描述還是挺抽象的统台,在下一節(jié)我們會通過實際的例子來演示這種情況的解決方案,這里先有個概念即可啡邑。

2.3 場景三


場景三是場景一和場景二的嵌套贱勃,因此場景三的滑動沖突看起來更加復雜了。許多應用都有這樣一個效果:內(nèi)層有一個場景一的滑動效果,然后外層又有一個場景二的滑動效果募寨。舉個例子族展,外部有一個SlideMenu效果,然后內(nèi)部有一個ViewPager拔鹰,ViewPager的每一個頁面又是一個ListView。雖然說場景三的滑動沖突看起來更復雜贵涵,但是它是幾個單一的滑動沖突的疊加列肢,因此只需要分別處理內(nèi)層、中層和外層之間的滑動沖突即可宾茂。具體的處理方法其實和場景一瓷马、場景二相同的。

場景三和場景二一樣跨晴,無法直接通過滑動的角度欧聘、距離差以及速度差來做判斷,同樣還是只能從業(yè)務上找到突破點端盆,具體方法和場景二一樣怀骤,都是從業(yè)務的需求上得出響應的處理規(guī)則,在下一節(jié)將會通過實際的例子來演示這種情況的解決方案焕妙。

三蒋伦、滑動沖突的解決方式

1、外部攔截法

所謂的外部攔截法是指點擊事件都先經(jīng)過父容器的攔截處理焚鹊,如果父容器需要此事件痕届,那就攔截,如果不需要此事件就不攔截末患,這樣就可以解決滑動沖突的問題研叫,這種方法比較符合點擊事件的分發(fā)機制。
外部攔截法需要重寫父容器的onInterceptTouchEvent方法璧针,然后在內(nèi)部做相應的攔截即可嚷炉。這種方法的偽代碼如下所示:

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;
            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_DOWN事件時走搁,必須返回false独柑,即不攔截該事件。因為父容器一旦攔截了該事件私植,那么后續(xù)的ACTION_MOVE和ACTION_UP事件都會直接交給父容器處理忌栅,這個時候事件就沒法傳遞給子元素了。
當事件為ACTION_MOVE事件時,可以根據(jù)需要來決定是否要攔截索绪,如果父容器需要攔截就返回true湖员,否則返回false。
當事件是ACTION_UP事件時瑞驱,也要返回false娘摔,因為ACTION_UP事件本身已經(jīng)沒有多大意義了。
可以考慮這樣的一種情況唤反,假設事件交由子元素處理凳寺,如果父容器在ACTION_UP時返回了true,那么會導致子元素無法收到ACTION_UP事件彤侍,這個時候子元素的onClick事件就無法觸發(fā)肠缨,因為當子元素為View時,必須接收到ACTION_UP事件才能觸發(fā)onClick事件盏阶。即使父容器的ACTION_UP事件返回了false晒奕,但父容器決定要攔截任何一個事件時,那么后續(xù)的事件也會交給它來處理名斟。

2脑慧、內(nèi)部攔截法

內(nèi)部攔截法是指父容器不攔截任何事件,所有的事件都傳遞到子元素蒸眠,如果子元素需要此事件就直接消耗掉漾橙,否則交給父容器進行處理。
這種方法和Android的事件分發(fā)機制不一樣楞卡,需要配合requestDisallowInterceptTouchEvent 方法才能正常工作霜运,相較于外部攔截法稍顯復雜。
內(nèi)部攔截法需要重寫子元素的dispatchTouchEvent方法蒋腮。這種方法的偽代碼如下所示:

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

上面代碼是內(nèi)部攔截法的典型代碼淘捡,但面對不同的滑動策略時,只需要修改父容器需要此類點擊事件的條件即可池摧,其他不需要修改也不能修改焦除。
當然,這里除了子元素需要做處理之外作彤,父元素也要默認攔截除ACTION_DOWN之外的其他事件膘魄,這樣當子元素調(diào)用parent. requestDisallowInterceptTouchEvent(false)方法時,父元素才能繼續(xù)攔截所需的事件竭讳。
父容器不攔截ACTION_DOWN事件的原因和上面的一樣的创葡,如果攔截了ACTION_DOWN事件,那么所有的事件都無法傳遞到子元素中去绢慢,這樣內(nèi)部攔截法就不起作用了灿渴。父元素所做的修改如下所示:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    int action = ev.getAction();
    if (action == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

四、實例演示

下面來看一個例子,分別說明如何利用外部攔截法和內(nèi)部攔截法來解決View的滑動沖突骚露。
下面的例子最外層是HorizontalListView蹬挤,這是一個可以水平滑動的ListView,是我從Github上拉過來的棘幸。出處是MeetMe/Android-HorizontalListView焰扳,HorizontalListView的里面是多個ItemView,而ItemView是可以上下滑動的ListView误续。
下面是Activity的代碼:

public class TestActivity extends Activity {
    
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_activity);

        HorizontalListView horizontalView = findViewById(R.id.horizontal_list_view);
        horizontalView.setAdapter(new MyListAdapter());
    }

    class MyListAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return 3;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            convertView = LayoutInflater.from(TestActivity.this).inflate(R.layout.horizontal_item, parent, false);
            ListView listView = convertView.findViewById(R.id.list_view);
            listView.setAdapter(new ArrayAdapter<String>(TestActivity.this, android.R.layout.simple_list_item_1, new String[]{"1", "2", "3", "4", "5", " 6", "7", "8", "9", "10", "1", "2", "3", "4", "5", " 6", "7", "8", "9", "10"}));
            return convertView;
        }
    }
}

Activity的代碼邏輯挺簡單的蓝翰,這里創(chuàng)建了三個子Item,并將其加入了HorizontalListView中女嘲,而三個子Item每一個又是由ListView構成。

horizontal_item的代碼如下:

<?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">

    <com.mucfc.myapplication.CustomListView
        android:id="@+id/list_view"
        android:layout_width="180dp"
        android:layout_height="match_parent"/>

</LinearLayout>

CustomListView的代碼如下:

public class CustomListView extends ListView {
    public CustomListView(Context context) {
        super(context);
    }

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

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

HorizontalListView的完整代碼由于比較長诞帐,所以這里省略欣尼。
如果運行上面的例子,則會出現(xiàn)滑動沖突的情況停蕉。具體的表現(xiàn)是愕鼓,左右滑動和上下滑動不流暢,給人的感覺就是很卡頓慧起。

下面就從外部攔截法和內(nèi)部攔截法這兩種方法著手菇晃,看一下如何解決上述沖突。

外部攔截法

如果采用外部攔截法蚓挤,則需要在HorizontalListView中重寫onInterceptTouchEvent方法磺送,如下所示:

private int mLastX;
private int mLastY;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    int x = (int) (ev.getX());
    int y = (int) (ev.getY());
    boolean intercept = false;
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            intercept = false;
            break;
        case MotionEvent.ACTION_MOVE:
            if (Math.abs(x - mLastX) > Math.abs(y - mLastY)) {
                intercept = true;
            } else {
                intercept = false;
            }
            break;
        case MotionEvent.ACTION_UP:
            intercept = false;
            break;
    }
    mLastX = x;
    mLastY = y;
    return intercept;
}

從上述的代碼看,它和外部攔截法的偽代碼差別很小灿意,只是將父容器的攔截條件換成了具體的邏輯估灿。這里的判斷是,當水平滑動方向的距離大于豎直滑動方向的距離時缤剧,則判斷為水平滑動馅袁,此時需要讓父容器進行攔截事件;而當豎直滑動方向的距離大于水平滑動方向的距離時荒辕,則判斷為豎直滑動汗销。

內(nèi)部攔截法

如果采用內(nèi)部攔截法,需要在HorizontalListView中重寫onInterceptTouchEvent方法抵窒,并在CustomListView中重寫dispatchOnTouchEvent方法弛针,具體代碼如下所示:

## CustomListView ##

private int mLastX;
private int mLastY;

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    int x = (int) ev.getX();
    int y = (int) ev.getY();
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            if (Math.abs(x - mLastX) > Math.abs(y - mLastY)) {
                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
        case MotionEvent.ACTION_UP:
            break;
    }
    mLastX = x;
    mLastY = y;
    return super.dispatchTouchEvent(ev);
}

## HorizontalListView ##

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        return false;
    } else {
        return true;
    }
}

相比于外部攔截法,內(nèi)部攔截法需要修改CustomListView和HorizontalListView兩個類的代碼估脆,復雜度相對來說比較高钦奋。但也能夠?qū)崿F(xiàn)滑動沖突的解決。至于在實際應用中要使用哪種方法,完全看個人的愛好付材,只要能解決問題朦拖,方法沒有好壞之分。

五厌衔、參考文章

本文主要參考 《Android開發(fā)藝術探索》 一書中的《View事件體系》這一章的內(nèi)容璧帝,這本書深入淺出的講解了Android開發(fā)中的知識點,是開發(fā)進階的必備利器富寿。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末睬隶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子页徐,更是在濱河造成了極大的恐慌苏潜,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件变勇,死亡現(xiàn)場離奇詭異恤左,居然都是意外死亡,警方通過查閱死者的電腦和手機搀绣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門飞袋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人链患,你說我怎么就攤上這事巧鸭。” “怎么了麻捻?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵纲仍,是天一觀的道長。 經(jīng)常有香客問我芯肤,道長巷折,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任崖咨,我火速辦了婚禮锻拘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘击蹲。我一直安慰自己署拟,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布歌豺。 她就那樣靜靜地躺著腌闯,像睡著了一般望抽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天师崎,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛娃殖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播议谷,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼炉爆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卧晓?” 一聲冷哼從身側(cè)響起芬首,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎逼裆,沒想到半個月后郁稍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡胜宇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年艺晴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掸屡。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖然评,靈堂內(nèi)的尸體忽然破棺而出仅财,到底是詐尸還是另有隱情,我是刑警寧澤碗淌,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布盏求,位于F島的核電站,受9級特大地震影響亿眠,放射性物質(zhì)發(fā)生泄漏碎罚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一纳像、第九天 我趴在偏房一處隱蔽的房頂上張望荆烈。 院中可真熱鬧,春花似錦竟趾、人聲如沸憔购。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽玫鸟。三九已至,卻和暖如春犀勒,著一層夾襖步出監(jiān)牢的瞬間屎飘,已是汗流浹背妥曲。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留钦购,地道東北人檐盟。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像肮雨,于是被迫代替她去往敵國和親遵堵。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

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