View滑動(dòng)事件沖突的解決方案

前言

上篇文章我們分析了View的事件是怎樣分發(fā)的褪尝,本文我們主要說明一下View的事件沖突解決驳规。

事件沖突是如何產(chǎn)生的

一般來說惑折,滑動(dòng)沖突事件一般有兩種情況术裸,父控件與子View的滑動(dòng)方向一致(父:ScrollView旭蠕,子:RecyclerView)停团,父View與子View的滑動(dòng)方向不一致(父:ViewPager旷坦,子:RecyclerView),你可能會(huì)問我平常使用的時(shí)候沒有出現(xiàn)滑動(dòng)沖突坝映怼秒梅?這是因?yàn)閂iewPager和ScrollView已經(jīng)對(duì)滑動(dòng)沖突做了處理。


滑動(dòng)沖突.png

解決滑動(dòng)沖突的思路

我們知道事件是由父控件傳遞到子控件的舌胶,
父控件調(diào)用dispatchTouchEvent方法捆蜀,該方法首先會(huì)判斷子View是否允許父View攔截該事件(子View通過調(diào)用父控件的requestDisallowInterceptTouchEvent方法,默認(rèn)false不攔截)幔嫂,如果子View不攔截則父控件調(diào)用onInterceptTouchEvent判斷事件是否攔截辆它,如果攔截則調(diào)用自身的onTouchEvent進(jìn)行處理,如則分發(fā)給子View進(jìn)行處理履恩。該過程中父控件會(huì)調(diào)用dispatchTransformTouchEvent锰茉,所不同的是如果是分發(fā)給子View處理會(huì)在該方法中調(diào)用子View的dispatchTouchEvent再次進(jìn)行分發(fā),如果是父控件自己解決則會(huì)調(diào)用view的dispatchTouchEvent方法(該方法最終會(huì)調(diào)用到View的onTouchEvent方法)嘗試消費(fèi)事件切心。如果你對(duì)事件分發(fā)的流程不太清楚飒筑,可以閱讀我的上一篇文章。
站在父控件的角度上處理與子控件的滑動(dòng)沖突绽昏,可以重寫onInterceptTouchEvent去攔截那些事件是父控件需要處理的协屡,其余的事件則交由子控件進(jìn)行處理。這就是外部攔截法而涉。

站在子控件的角度上來說著瓶,需要父控件不能攔截子控件需要消耗的事件,這就需要主動(dòng)調(diào)用父控件的requestDisallowInterceptTouchEvent方法來主動(dòng)改變事件分發(fā)的流程啼县,何時(shí)調(diào)用requestDisallowInterceptTouchEvent方法呢?沸久,當(dāng)然是分發(fā)事件給子控件的時(shí)候(dispatchTouchEvent方法)季眷,但是這里需要注意一點(diǎn)的是,父控件如果處理了按下事件(ACTION_DOWN)卷胯,后續(xù)的事件子View都無法獲取到子刮,所以還需要重寫父控件的攔截方法(onInterceptTouchEvent),不攔截按下事件窑睁。這就是內(nèi)部攔截法

外部攔截法和內(nèi)部攔截法的模板方法如下:

//外部攔截法
fun onInterceptTouchEvent(ev){
    var intercept=false
    when(ev.action){
        MotionEvent.ACTION_DOWN->{
            ....
        }    
        MotionEvent.ACTION_MOVE->{
            if(滿足攔截條件){
               intercept=true
             }else{
                intercept=false
             }
        }
        MotionEvent.ACTION_UP->{
            ......
        }
    }
    return intercept
}

//內(nèi)部攔截法
//子控件
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val x = ev.x
        val y = ev.y
        when (ev.actionMasked) {
            //down事件
            MotionEvent.ACTION_DOWN -> {
                //不允許父控件攔截按下事件
                parent.requestDisallowInterceptTouchEvent(true)
            }
            //move事件
            MotionEvent.ACTION_MOVE -> {
                if (判斷條件) {
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
        mLastX = x.toInt()
        mLastY = y.toInt()
        return super.dispatchTouchEvent(ev)
      }
//父控件
fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    //不攔截按下事件
    if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
        return false
    } else {
        return true
    }
}

下面以ViewPager和Recyclerview的滑動(dòng)沖突來實(shí)際講解滑動(dòng)沖突挺峡。

實(shí)例說明

activity布局如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.zxl.handledispatchdemo.MyViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

里面我們自定義了一個(gè)自定義的ViewPager,MyViewPager的代碼如下

class MyViewPager @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null) :
    ViewPager(context, attributeSet) {

    private var mLastX = 0
    private var mLastY = 0
    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
       //返回fasle默認(rèn)不攔截,如果子View消費(fèi)事件的話担钮,就不會(huì)走ViewPager解決滑動(dòng)沖突的相關(guān)代碼
        return false
    }

}

ViewPager的Adapter相關(guān)如下:

class MyViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_SET_USER_VISIBLE_HINT) {
    val list = mutableListOf<Fragment>(MyFragment(), MyFragment(), MyFragment())
    override fun getCount(): Int = list.size

    override fun getItem(position: Int): Fragment = list[position]

}

MainActivity代碼

package com.zxl.handledispatchdemo

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.viewpager.widget.ViewPager

class MainActivity : AppCompatActivity() {
    private lateinit var mVp: ViewPager
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mVp = findViewById(R.id.vp)
        mVp.adapter = MyViewPagerAdapter(supportFragmentManager)
    }
}


class MyViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm, BEHAVIOR_SET_USER_VISIBLE_HINT) {
    val list = mutableListOf<Fragment>(MyFragment(), MyFragment(), MyFragment())
    override fun getCount(): Int = list.size

    override fun getItem(position: Int): Fragment = list[position]

}

ViewPager里面嵌套的Fragment代碼及布局如下:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.zxl.handledispatchdemo.MyRecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</androidx.constraintlayout.widget.ConstraintLayout>

MyFragment代碼

package com.zxl.handledispatchdemo

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView

class MyFragment : Fragment() {
    private lateinit var mRv: RecyclerView
    private val mList = mutableListOf<String>()
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        val view = inflater.inflate(R.layout.item_fragment, container, false)
        mRv = view.findViewById(R.id.rv)
        repeat(100) {
            mList.add(it.toString())
        }
        mRv.adapter = MyAdapter(mList)
        return view
    }
}

class MyViewHolder(view: View) : RecyclerView.ViewHolder(view)

class MyAdapter(val list: List<String>) : RecyclerView.Adapter<MyViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val inflate =
            LayoutInflater.from(parent.context).inflate(R.layout.item_string, parent, false)
        return MyViewHolder(inflate)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val tv = holder.itemView.findViewById<TextView>(R.id.tv)
        tv.text = list[position]
    }

    override fun getItemCount(): Int = list.size
}

現(xiàn)在應(yīng)該ViewPager是無法進(jìn)行左右滑動(dòng)的橱赠。RecyclerView可以正常滑動(dòng)


未處理滑動(dòng)事件前.jpg

現(xiàn)在我們使用內(nèi)部攔截法進(jìn)行事件沖突的處理箫津,修改MyRecyclerView的dispatchTouchEvent如下

  override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        val x = ev.x
        val y = ev.y
        when (ev.actionMasked) {
            //down事件
            MotionEvent.ACTION_DOWN -> {
                //不允許父控件攔截按下事件
                parent.requestDisallowInterceptTouchEvent(true)
            }
            //move事件
            MotionEvent.ACTION_MOVE -> {
                //橫縱滑動(dòng)大于縱向滑動(dòng)
                if (abs(x - mLastX) > abs(y - mLastY)) {
                    parent.requestDisallowInterceptTouchEvent(false)
                }
            }
        }
        mLastX = x.toInt()
        mLastY = y.toInt()
        return super.dispatchTouchEvent(ev)
    }

MyViewPager的onInterceptTouchEvent修改如下

   override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        //如果是按下事件則不攔截事件狭姨,否則攔截事件
        return if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            false
        } else {
            true
        }
    }

做出如上修改后還是無法實(shí)現(xiàn)ViewPager的滑動(dòng)宰啦,這是因?yàn)閂iewPager重寫了ViewGroup的onInterceptTouchEvent。

 public boolean onInterceptTouchEvent(MotionEvent ev) {
    
       ........

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                .......
                break;
            }

            case MotionEvent.ACTION_DOWN: {
                ......
                //初始化成員mActivePointerId 
                mActivePointerId = ev.getPointerId(0);
               ......
                break;
            }

            case MotionEvent.ACTION_POINTER_UP:
             ......
                break;
        }

        ....
        return mIsBeingDragged;
    }

在按下時(shí)會(huì)初始化成員mActivePointerId饼拍,而ViewPager的onTouchEvent方法中赡模,會(huì)對(duì)這個(gè)值進(jìn)行判斷,日過該值為null,則默認(rèn)該事件被子View消費(fèi)了,從而影響到ViewPager的橫向滑動(dòng)洋只。

public boolean onTouchEvent(MotionEvent ev) {
    

        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                //由于我們?cè)趏nInterceptTouchEvent對(duì)MotionEvent.ACTION_DOWN咖城,進(jìn)行了處理
                //將按下事件分發(fā)給了子View,此時(shí)在ViewPager的onTouchEvent不會(huì)執(zhí)行到該代碼塊
                mActivePointerId = ev.getPointerId(0);
                break;
            }
            case MotionEvent.ACTION_MOVE:
               //由于mActivePointerId在此時(shí)是默認(rèn)值棚瘟,調(diào)用findPointerIndex會(huì)返回-1,而返回-1導(dǎo)致跳出了switch,無法執(zhí)行后面的滾動(dòng)邏輯
               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;
            case MotionEvent.ACTION_UP:
             
                 ...........
                break;
            case MotionEvent.ACTION_CANCEL:
                .........
                break;
            case MotionEvent.ACTION_POINTER_DOWN: {
                ........
                break;
            }
            case MotionEvent.ACTION_POINTER_UP:
                .......
                break;
        }
        ........
        return true;
    }

則就是使用內(nèi)部攔截法后ViewPager無法滑動(dòng)的原因谚殊,那么如何解決呢?答案是在自定義ViewPager的onInterceptTouchEvent中的按下事件調(diào)用父類的onInterceptTouchEvent方法蛤铜。代碼如下

 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        //如果是按下事件則不攔截事件嫩絮,否則攔截事件
        return if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
            //調(diào)用父類的onInterceptTouchEvent,確保mActivePointerId被初始化
            super.onInterceptTouchEvent(ev)
            false
        } else {
            true
        }
    }

現(xiàn)在ViewPager可以正澄Х剩滑動(dòng)了

可以看出當(dāng)控件重寫了ViewGroup的onInterceptTouchEvent方法后剿干,我們?cè)俅沃貙憃nInterceptTouchEvent時(shí)要關(guān)注事件分發(fā)對(duì)他的影響,直到實(shí)現(xiàn)自己需要的效果為止穆刻。
運(yùn)行效果如下:

內(nèi)部攔截法運(yùn)行效果.gif

至此置尔,內(nèi)部攔截法說明完畢。

外部攔截法

外部攔截法只需要重寫父控件的onInterceptToucheEvent方法氢伟,所以MyFragment中的MyRecyclerView直接使用google原生的RecyclerView即可榜轿。修改MyViewPager的onInterceptToucheEvent如下

  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {

        /**
         * 內(nèi)部攔截法
         */
//        if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
//            super.onInterceptTouchEvent(ev)
//            return false
//        } else {
//            return true
//        }


        /**
         * 外部攔截法
         */

        when (ev.actionMasked) {
            //記錄按下時(shí)的x,y
            MotionEvent.ACTION_DOWN -> {
                mLastX = ev.x.toInt()
                mLastY = ev.y.toInt()
            }
            MotionEvent.ACTION_MOVE -> {
                //當(dāng)縱向滑動(dòng)距離大于橫向滑動(dòng)距離朵锣,不攔截事件谬盐,分發(fā)給子view
                if (abs(ev.x - mLastX) < abs(ev.y - mLastY)) {
                    return false
                }
            }

        }
        //調(diào)用ViewPager的onInterceptTouchEvent方法作為返回結(jié)果,mActivePointerId 在
       //ACTION_DOWN 的處理中已經(jīng)被賦值
        return super.onInterceptTouchEvent(ev)
    }

按下時(shí)就錄了按下點(diǎn)的x,y诚些,在滑動(dòng)時(shí)比較當(dāng)前觸摸點(diǎn)與按下點(diǎn)的橫縱坐標(biāo)距離的絕對(duì)值飞傀,如果y軸的移動(dòng)距離大于X軸的移動(dòng)距離,說明是縱向滑動(dòng)诬烹,需要交給子View進(jìn)行處理砸烦,否則說明是橫向滑動(dòng),則交由ViewPager處理绞吁。
運(yùn)行效果如下:


外部攔截法運(yùn)行效果.gif

自此外部攔截法說明結(jié)束幢痘。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市掀泳,隨后出現(xiàn)的幾起案子雪隧,更是在濱河造成了極大的恐慌西轩,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,548評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脑沿,死亡現(xiàn)場(chǎng)離奇詭異藕畔,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)庄拇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門注服,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人措近,你說我怎么就攤上這事溶弟。” “怎么了瞭郑?”我有些...
    開封第一講書人閱讀 167,990評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵辜御,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我屈张,道長(zhǎng)擒权,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評(píng)論 1 296
  • 正文 為了忘掉前任阁谆,我火速辦了婚禮碳抄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘场绿。我一直安慰自己剖效,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評(píng)論 6 397
  • 文/花漫 我一把揭開白布焰盗。 她就那樣靜靜地躺著璧尸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪熬拒。 梳的紋絲不亂的頭發(fā)上逗宁,一...
    開封第一講書人閱讀 52,246評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音梦湘,去河邊找鬼。 笑死件甥,一個(gè)胖子當(dāng)著我的面吹牛捌议,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播引有,決...
    沈念sama閱讀 40,819評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼瓣颅,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了譬正?” 一聲冷哼從身側(cè)響起宫补,我...
    開封第一講書人閱讀 39,725評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤檬姥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粉怕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體健民,經(jīng)...
    沈念sama閱讀 46,268評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評(píng)論 3 340
  • 正文 我和宋清朗相戀三年贫贝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了秉犹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,488評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡稚晚,死狀恐怖崇堵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情客燕,我是刑警寧澤鸳劳,帶...
    沈念sama閱讀 36,181評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站也搓,受9級(jí)特大地震影響赏廓,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜还绘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評(píng)論 3 333
  • 文/蒙蒙 一楚昭、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拍顷,春花似錦抚太、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至踏揣,卻和暖如春庆亡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背捞稿。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工又谋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娱局。 一個(gè)月前我還...
    沈念sama閱讀 48,897評(píng)論 3 376
  • 正文 我出身青樓彰亥,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親衰齐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子任斋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評(píng)論 2 359

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