Android 嵌套滑動(dòng)的研究篇一

0. 參考資料

來(lái)自鴻洋的博客的例子失暂,非常感謝:
http://blog.csdn.net/lmj623565791/article/details/43649913

整體代碼:
https://github.com/zhaoyubetter/KotlinAndroidDemo/blob/master/widget/src/main/java/test/com/widget/nested/StickyNavVerticalLayout2.kt

1. 嵌套滑動(dòng)場(chǎng)合

如上面的例子,當(dāng)內(nèi)層的view友绝,如:list靖榕,滑動(dòng)到頂部時(shí)语御,即 firstChild.getTop = 0 的梯醒,我們需要將list的事件儿子,轉(zhuǎn)而給外層(怎么給呢?)漓藕,讓外層陶珠,去消耗,這點(diǎn)稍有些麻煩(鴻洋的博客中有解決)撵术;在Android的事件分發(fā)中背率,如果找到了target了,就一直會(huì)把后續(xù)的事件源源不斷的給target嫩与;
而此時(shí),一般我們需要抬起手指交排,然后重新下拉划滋,這個(gè)時(shí)候,外層就收到了事件了埃篓;

那可以實(shí)現(xiàn)嵌套滑動(dòng)嗎处坪?答案是可以的,我們可以用 nestedScrolling 的api來(lái)做架专,這塊同窘,我還暫未了解。所有這里部脚,采用原始的事件分發(fā)方案來(lái)解決這個(gè)問(wèn)題想邦;

2. 準(zhǔn)備開(kāi)始吧

這里我簡(jiǎn)化了一下代碼,僅供參考委刘;

2.1 簡(jiǎn)化了布局文件如下

<test.com.widget.nested.StickyNavVerticalLayout2 
       xmlns:android="http://schemas.android.com/apk/res/android"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent"
                   android:orientation="vertical">

    <!-- 頭部 -->
    <RelativeLayout
        android:id="@+id/id_stickynavlayout_topview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#4400ff00">
        <TextView
            android:layout_width="match_parent"
            android:layout_height="256dp"
            android:gravity="center"
            android:text="嵌套滑動(dòng)"
            android:textSize="30sp"
            android:textStyle="bold"/>
    </RelativeLayout>

    <!-- 假的懸浮頭 -->
    <TextView
        android:id="@+id/id_stickynavlayout_indicator"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#ffffffff"
        android:gravity="center"
        android:text="懸浮頭"/>

    <!-- 嵌套的 scrollView  -->
    <ScrollView
        android:id="@+id/id_stickynavlayout_scrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:divider="?android:attr/listDivider"
            android:showDividers="middle"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="100dp"
                android:text="text1"/>
            <!-- 更多內(nèi)容 -->
        </LinearLayout>
    </ScrollView>

</test.com.widget.nested.StickyNavVerticalLayout2>

3. 過(guò)程實(shí)現(xiàn)

一步一步來(lái)實(shí)現(xiàn)丧没;

3.1 先實(shí)現(xiàn)界面布局

創(chuàng)建StickyNavVerticalLayout繼承自LinearyLayout,并添加相應(yīng)的一些代碼锡移,注釋在代碼里:

class StickyNavVerticalLayout2(context: Context, attrs: AttributeSet?, defAttrStyle: Int) : LinearLayout(context, attrs, defAttrStyle) {
    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    // --- 內(nèi)部 View 相關(guān)成員變量
    private var top: View? = null
    private var nav: View? = null
    private var scrollView: View? = null
    private var topHeight: Int? = 0     // top的高度

    init {
    }

    override fun onFinishInflate() {
        super.onFinishInflate()
        top = findViewById(R.id.id_stickynavlayout_topview)
        nav = findViewById(R.id.id_stickynavlayout_indicator)
        scrollView = findViewById(R.id.id_stickynavlayout_scrollview)
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        scrollView?.layoutParams?.height = measuredHeight.minus(nav?.measuredHeight ?: 0)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        topHeight = top?.measuredHeight?: 0
    }
}

現(xiàn)在的界面效果:

布局

底部的scrollView可以滑動(dòng)呕童,但是不能滑動(dòng)到底部;

3.2 添加手勢(shì)處理邏輯

實(shí)現(xiàn) onInterceptTouchEvent, onTouchEvent淆珊, 注意夺饲,在這里,我們這2個(gè)方法,都返回true往声,表示事件由自己處理擂找,不傳給子view(scrollView 收不到事件)

   // --- 事件操作相關(guān)的成員變量
    private var lastY: Int = 0
    private var isDrag = false      // 是否拖拽

    private var scroller: Scroller = Scroller(getContext())
    private var velocityTracker: VelocityTracker? = null
    private var touchSlop: Int = 0
    private var maxFlingVelocity: Int = 0
    private var minFlingVelocity: Int = 0


    init {
        val config = ViewConfiguration.get(context)
        touchSlop = config.scaledTouchSlop
        maxFlingVelocity = config.scaledMaximumFlingVelocity
        minFlingVelocity = config.scaledMinimumFlingVelocity
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.let {
            val y = it.y
            initVelocityTracker()
            velocityTracker?.let { it.addMovement(event) }     // 添加運(yùn)動(dòng)軌跡

            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    lastY = y.toInt()
                }
                MotionEvent.ACTION_MOVE -> {
                    val dy = y - lastY
                    if (!isDrag && Math.abs(dy) > touchSlop) {
                        isDrag = true
                    }
                    if (isDrag) {
                        scrollBy(0, -dy.toInt())    // 反向取反
                    }
                    lastY = y.toInt()
                }

                MotionEvent.ACTION_UP -> {          // 抬起,運(yùn)動(dòng)軌跡判斷烁挟,是否fling
                    isDrag = false
                    velocityTracker?.let {
                        it.computeCurrentVelocity(1000, maxFlingVelocity?.toFloat())
                        if (Math.abs(it.yVelocity) > minFlingVelocity) {
                            fling(-it.yVelocity.toInt())
                        }
                    }
                    releaseVelocity()
                }

                MotionEvent.ACTION_CANCEL -> {
                    isDrag = false
                    releaseVelocity()
                }
            }
        }
        return true    
        //return super.onTouchEvent(event)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        return true
        //return super.onInterceptTouchEvent(ev)
    }

    override fun computeScroll() {
        if (scroller.computeScrollOffset()) {
            scrollTo(0, scroller.currY)
            invalidate()
        }
    }

  /**
     * 邊界處理
     */
    override fun scrollTo(x: Int, y: Int) {
        var tmpY = y
        if (y < 0) tmpY = 0
        if (y > topHeight) tmpY = topHeight
        super.scrollTo(x, tmpY)
    }

    private inline fun fling(velocityY: Int) {
        scroller.let {
            it.fling(0, scrollY, 0, velocityY, 0, 0, 0, topHeight)     // 滑翔
            invalidate()
        }
    }

    private inline fun initVelocityTracker() {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain()
        }
    }

    private inline fun releaseVelocity() {
        velocityTracker?.let {
            it.recycle()
            velocityTracker = null
        }
    }

大部分說(shuō)明 在鴻洋的 博客里寫(xiě)的很詳細(xì)了婴洼,這里就不再敘述了;
主要目的是撼嗓,實(shí)現(xiàn) StickyNavVerticalLayout2 的整體的滑動(dòng)柬采,滑翔等;
可以看到 scrollview不能單獨(dú)滑動(dòng)
實(shí)現(xiàn)效果為:

整體攔截事件效果

3.3 攔截事件的處理

去掉 onTouchEvent 的 return true且警;
重寫(xiě)onInterceptTouchEvent方法粉捻,讓其在特定的情況下攔截事件,需要攔截分為2種情況:

  1. topView 可見(jiàn)時(shí)攔截斑芜;
  2. topview不可見(jiàn)肩刃,并且 內(nèi)部的 scrollView 在頂部,并且還在下拉的狀態(tài)下杏头,進(jìn)行攔截盈包;

我們這里使用ViewCompat來(lái)進(jìn)行View是否還可以繼續(xù)滾動(dòng)的判斷;我們來(lái)看代碼:

// 先添加成員變量醇王,記錄 top 的可見(jiàn)狀態(tài)
private var topHide = false

override fun scrollTo(x: Int, y: Int) {
        var tmpY = y
        if (y < 0) tmpY = 0
        if (y > topHeight) tmpY = topHeight
        super.scrollTo(x, tmpY)
        topHide = scrollY == topHeight  // 更新 topHide
 }

override fun onTouchEvent(event: MotionEvent?): Boolean {
   .....省略代碼.....
   // return true
   return super.onTouchEvent(event)
}

    /**
     * 攔截判斷
     */
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        val y = ev.y
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> lastY = y.toInt()
            MotionEvent.ACTION_MOVE -> {        // 重點(diǎn)
                val dy = y - lastY
                if (Math.abs(dy) > touchSlop) {
                    // topView 可見(jiàn) || (topView不可見(jiàn) && scrollView不能再下拉 && 繼續(xù)下拉)
                    if (!topHide || (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0)) {
                        lastY = y.toInt()
                        isDrag = true
                        initVelocityTracker()
                        velocityTracker?.let {
                            it.addMovement(ev)
                        }
                        return true
                    }
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                isDrag = false
                releaseVelocity()
            }
        }

        return super.onInterceptTouchEvent(ev)
    }

到現(xiàn)在呢燥,已經(jīng)完成了基本的需求了,但是嵌套滑動(dòng)還是不行寓娩,不連貫叛氨,沒(méi)有那種一口氣 硬扯到底 的 感覺(jué),需要放開(kāi)棘伴,再拉 寞埠;
效果圖,如下:

實(shí)現(xiàn)效果

這個(gè)時(shí)候焊夸,就回到了仁连,開(kāi)頭留下的問(wèn)題了,如何實(shí)現(xiàn)一拉到底中間整個(gè)過(guò)程沒(méi)有間斷淳地;我們需要使用 dispatchTouchEvent 這個(gè)方法了怖糊;

3.4 重新 dispatchTouchEvent 嵌套的滑動(dòng)實(shí)現(xiàn)

上面的問(wèn)題,在于颇象,當(dāng)事件被 子 view 接收后伍伤,后續(xù)的事件,都會(huì)跑到子view遣钳;但事件的傳遞扰魂,都是從父到子的過(guò)程,事件的傳遞會(huì)經(jīng)過(guò)父的dispatch,通過(guò)這個(gè)方法劝评,將事件在父與子View中根據(jù)條件來(lái)傳遞姐直,從而實(shí)現(xiàn)嵌套滑動(dòng);

private var isInControl = false     // 是否已dispatch

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
            MotionEvent.ACTION_MOVE -> {    // 進(jìn)行判斷蒋畜,是否重發(fā)事件
                val dy = ev.y - lastY
                // 頭不可見(jiàn)声畏,繼續(xù)下拉,重發(fā)事件
                if (topHide && !ViewCompat.canScrollVertically(scrollView, -1) && dy > 0 && !isInControl) {
                    isInControl = true
                    ev.action = MotionEvent.ACTION_CANCEL
                    val ev2 = MotionEvent.obtain(ev)
                    dispatchTouchEvent(ev)
                    ev2.action = MotionEvent.ACTION_DOWN
                    return dispatchTouchEvent(ev2)
                }
            }
        }

        return super.dispatchTouchEvent(ev)
    }

我們需要在 onTouchEvent方法加入以下代碼片段姻成,即:在邊界時(shí)插龄,將MOVE事件轉(zhuǎn)換成 DOWN事件,重新進(jìn)行分發(fā)科展;

=== > onTouchEvent  方法中修改

MotionEvent.ACTION_MOVE -> {
     val dy = y - lastY
     if (!isDrag && Math.abs(dy) > touchSlop) {
             isDrag = true
     }
     if (isDrag) {
             scrollBy(0, -dy.toInt())    // 反向取反
     }
     // 如果滑到頂了均牢,將事件轉(zhuǎn)換成點(diǎn)擊事情,發(fā)送
     if (scrollY == topHeight) {
              event.action = MotionEvent.ACTION_DOWN
              dispatchTouchEvent(event)
              isInControl = false
      }
     lastY = y.toInt()
 }

scroller的優(yōu)化才睹,在onInterceptTouchEvent() 與 onTouchEvent中分別 加入:

 override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        val y = ev.y
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> lastY = y.toInt()
            MotionEvent.ACTION_MOVE -> {        // 重點(diǎn)
                // 慣性未結(jié)束徘跪,攔截事件
                if(!scroller.isFinished) {
                    return true
                }
.......
.......

  // onTouchEvent
  override fun onTouchEvent(event: MotionEvent?): Boolean {
           // .... 省略代碼
            when (event.action) {
                MotionEvent.ACTION_DOWN -> {
                    lastY = y.toInt()
                    if(!scroller.isFinished) {       // 未結(jié)束時(shí),結(jié)束scroller
                        scroller.abortAnimation()
                        return true
                    }
                }
        
               // ACTION_CANCEL時(shí)時(shí)琅攘,取消scroller
                MotionEvent.ACTION_CANCEL -> {  
                    isDrag = false
                    if(!scroller.isFinished) {
                        scroller.abortAnimation()
                    }
                    releaseVelocity()
                }

最終效果如下:

最終效果

水平滑動(dòng)(加深印象)

效果如下:

水平滑動(dòng)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末垮庐,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坞琴,更是在濱河造成了極大的恐慌突硝,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件置济,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡锋八,警方通過(guò)查閱死者的電腦和手機(jī)浙于,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)挟纱,“玉大人羞酗,你說(shuō)我怎么就攤上這事∥煞” “怎么了檀轨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)欺嗤。 經(jīng)常有香客問(wèn)我参萄,道長(zhǎng),這世上最難降的妖魔是什么煎饼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任讹挎,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘筒溃。我一直安慰自己马篮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布怜奖。 她就那樣靜靜地躺著浑测,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歪玲。 梳的紋絲不亂的頭發(fā)上迁央,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音读慎,去河邊找鬼漱贱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛夭委,可吹牛的內(nèi)容都是我干的幅狮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼株灸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼崇摄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起慌烧,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤逐抑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后屹蚊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體厕氨,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年汹粤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了命斧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嘱兼,死狀恐怖国葬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情芹壕,我是刑警寧澤汇四,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站踢涌,受9級(jí)特大地震影響通孽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜斯嚎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一利虫、第九天 我趴在偏房一處隱蔽的房頂上張望挨厚。 院中可真熱鬧,春花似錦糠惫、人聲如沸疫剃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)巢价。三九已至,卻和暖如春固阁,著一層夾襖步出監(jiān)牢的瞬間壤躲,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工备燃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留碉克,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓并齐,卻偏偏與公主長(zhǎng)得像漏麦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子况褪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

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