Android 嵌套滑動的研究篇一

0. 參考資料

來自鴻洋的博客的例子,非常感謝:
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)內(nèi)層的view,如:list,滑動到頂部時,即 firstChild.getTop = 0 的,我們需要將list的事件,轉(zhuǎn)而給外層(怎么給呢?),讓外層,去消耗,這點稍有些麻煩(鴻洋的博客中有解決);在Android的事件分發(fā)中,如果找到了target了,就一直會把后續(xù)的事件源源不斷的給target;
而此時,一般我們需要抬起手指,然后重新下拉,這個時候,外層就收到了事件了;

那可以實現(xiàn)嵌套滑動嗎?答案是可以的,我們可以用 nestedScrolling 的api來做,這塊,我還暫未了解。所有這里,采用原始的事件分發(fā)方案來解決這個問題;

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

這里我簡化了一下代碼,僅供參考;

2.1 簡化了布局文件如下

<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="嵌套滑動"
            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. 過程實現(xiàn)

一步一步來實現(xiàn);

3.1 先實現(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可以滑動,但是不能滑動到底部;

3.2 添加手勢處理邏輯

實現(xiàn) onInterceptTouchEvent, onTouchEvent, 注意,在這里,我們這2個方法,都返回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) }     // 添加運動軌跡

            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 -> {          // 抬起,運動軌跡判斷,是否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
        }
    }

大部分說明 在鴻洋的 博客里寫的很詳細(xì)了,這里就不再敘述了;
主要目的是,實現(xiàn) StickyNavVerticalLayout2 的整體的滑動,滑翔等;
可以看到 scrollview不能單獨滑動
實現(xiàn)效果為:

整體攔截事件效果

3.3 攔截事件的處理

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

  1. topView 可見時攔截;
  2. topview不可見,并且 內(nèi)部的 scrollView 在頂部,并且還在下拉的狀態(tài)下,進行攔截;

我們這里使用ViewCompat來進行View是否還可以繼續(xù)滾動的判斷;我們來看代碼:

// 先添加成員變量,記錄 top 的可見狀態(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 -> {        // 重點
                val dy = y - lastY
                if (Math.abs(dy) > touchSlop) {
                    // topView 可見 || (topView不可見 && 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)完成了基本的需求了,但是嵌套滑動還是不行,不連貫,沒有那種一口氣 硬扯到底 的 感覺,需要放開,再拉 ;
效果圖,如下:

實現(xiàn)效果

這個時候,就回到了,開頭留下的問題了,如何實現(xiàn)一拉到底中間整個過程沒有間斷;我們需要使用 dispatchTouchEvent 這個方法了;

3.4 重新 dispatchTouchEvent 嵌套的滑動實現(xiàn)

上面的問題,在于,當(dāng)事件被 子 view 接收后,后續(xù)的事件,都會跑到子view;但事件的傳遞,都是從父到子的過程,事件的傳遞會經(jīng)過父的dispatch,通過這個方法,將事件在父與子View中根據(jù)條件來傳遞,從而實現(xiàn)嵌套滑動;

private var isInControl = false     // 是否已dispatch

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
            MotionEvent.ACTION_MOVE -> {    // 進行判斷,是否重發(fā)事件
                val dy = ev.y - lastY
                // 頭不可見,繼續(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方法加入以下代碼片段,即:在邊界時,將MOVE事件轉(zhuǎn)換成 DOWN事件,重新進行分發(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)換成點擊事情,發(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 -> {        // 重點
                // 慣性未結(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é)束時,結(jié)束scroller
                        scroller.abortAnimation()
                        return true
                    }
                }
        
               // ACTION_CANCEL時時,取消scroller
                MotionEvent.ACTION_CANCEL -> {  
                    isDrag = false
                    if(!scroller.isFinished) {
                        scroller.abortAnimation()
                    }
                    releaseVelocity()
                }

最終效果如下:

最終效果

水平滑動(加深印象)

效果如下:

水平滑動
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容