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. 嵌套滑動場合

如上面的例子,當內層的view,如:list,滑動到頂部時,即 firstChild.getTop = 0 的,我們需要將list的事件,轉而給外層(怎么給呢?),讓外層,去消耗,這點稍有些麻煩(鴻洋的博客中有解決);在Android的事件分發中,如果找到了target了,就一直會把后續的事件源源不斷的給target;
而此時,一般我們需要抬起手指,然后重新下拉,這個時候,外層就收到了事件了;

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

2. 準備開始吧

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

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"/>
            <!-- 更多內容 -->
        </LinearLayout>
    </ScrollView>

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

3. 過程實現

一步一步來實現;

3.1 先實現界面布局

創建StickyNavVerticalLayout繼承自LinearyLayout,并添加相應的一些代碼,注釋在代碼里:

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)

    // --- 內部 View 相關成員變量
    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
    }
}

現在的界面效果:

布局

底部的scrollView可以滑動,但是不能滑動到底部;

3.2 添加手勢處理邏輯

實現 onInterceptTouchEvent, onTouchEvent, 注意,在這里,我們這2個方法,都返回true,表示事件由自己處理,不傳給子view(scrollView 收不到事件)

   // --- 事件操作相關的成員變量
    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
        }
    }

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

整體攔截事件效果

3.3 攔截事件的處理

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

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

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

// 先添加成員變量,記錄 top 的可見狀態
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不能再下拉 && 繼續下拉)
                    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)
    }

到現在,已經完成了基本的需求了,但是嵌套滑動還是不行,不連貫,沒有那種一口氣 硬扯到底 的 感覺,需要放開,再拉 ;
效果圖,如下:

實現效果

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

3.4 重新 dispatchTouchEvent 嵌套的滑動實現

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

private var isInControl = false     // 是否已dispatch

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        when (ev.action) {
            MotionEvent.ACTION_DOWN -> lastY = ev.y.toInt()
            MotionEvent.ACTION_MOVE -> {    // 進行判斷,是否重發事件
                val dy = ev.y - lastY
                // 頭不可見,繼續下拉,重發事件
                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事件轉換成 DOWN事件,重新進行分發;

=== > onTouchEvent  方法中修改

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

scroller的優化,在onInterceptTouchEvent() 與 onTouchEvent中分別 加入:

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

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

最終效果如下:

最終效果

水平滑動(加深印象)

效果如下:

水平滑動
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容