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 简化了布局文件如下



    
    
        
    

    
    

    
    
        

            
            
        
    


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
    }
}

现在的界面效果:

Android 嵌套滑动的研究篇一_第1张图片
布局

底部的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)
    }

到现在,已经完成了基本的需求了,但是嵌套滑动还是不行,不连贯,没有那种一口气 硬扯到底 的 感觉,需要放开,再拉 ;
效果图,如下:

Android 嵌套滑动的研究篇一_第2张图片
实现效果

这个时候,就回到了,开头留下的问题了,如何实现一拉到底中间整个过程没有间断;我们需要使用 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()
                }

最终效果如下:

Android 嵌套滑动的研究篇一_第3张图片
最终效果

水平滑动(加深印象)

效果如下:

Android 嵌套滑动的研究篇一_第4张图片
水平滑动

你可能感兴趣的:(Android 嵌套滑动的研究篇一)