Android 嵌套滑动的研究篇二 - 仿jd商品页

参考资料:

  1. http://blog.csdn.net/dingding_android/article/details/52948379
  2. http://www.jianshu.com/p/1bc9e712ee71
  3. https://segmentfault.com/a/1190000002873657

实例完整代码,请参考:
https://github.com/momodae/KotlinWidget/blob/master/widget/src/main/java/cz/widget/layout/SlidingUpLayout.kt

嵌套滑动的解释

在传统的android事件分发过程中,如果子view消费了事件,后续的事件都会交给该子view;但在某些场合,上如一篇,在 listview 滑动下拉到头时,如果继续下拉,希望,父能够拉下来;这就涉及到了嵌套滑动了;

简而言之,子view与父view一起来协作处理滑动的过程,整个流程在开启在子view;

用我一好哥们的解释:android传统的事件分发解决方案处理,像中式教育(孩子指子view如果你想解决这个问题,那你自己解决就好了),而新的嵌套滑动机制,更像西式教育(父与孩子一起解决问题);

NestedScrolling嵌套滑动相关类与接口

  1. NestedScrollingChild
  2. NestedScrollingParent
  3. NestedScrollingChildHelper
  4. NestedScrollingParentHelper

如果想实现NestedScrolling,子view需要实现NestedScrollingChild接口,父View实现NestedScrollingParent接口, Helper为其帮助类;

接口方法非常的多,我们来看一下:

NestedScrollingChild 接口方法:

4个主要的方法,分别标上了注释,其他方法需要在实战中理解

    public void setNestedScrollingEnabled(boolean enabled) {
    }
    public boolean isNestedScrollingEnabled() {
        return false;
    }
    /**
    * 由子View开启NestedScrolling机制,通知父容器,我要和你配合处理    
      Touch事件;
    * 这个函数内部会去寻找实现了NestedScrolling机制的父容器,
    * 如果找到了就返回true,如果没有则返回false。
    **/
    public boolean startNestedScroll(int axes) {
        return false;
    }
  
  /**
   * 结束整个流程
   */
    public void stopNestedScroll() {
    }
    public boolean hasNestedScrollingParent() {
        return false;
    }

  /**
  * 在子元素滑动之后调用,向父view汇报滚动情况,包括子view消费的部分和子view没有消费的部分。
    如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回true,否则为false。
  */
   public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return false;
   } 

 /**
 * 一般在onTouch的MOVE事件中调用,用来通知父容器,我要和你配合处理Touch事件
 *  如果父view接受了它的滚动参数,进行了部分消费,则这个函数返回
 true,否则为false
 * @param dx 水平滑动距离
 * @param dy 垂直滑动距离
 * @param consumed  consumed数组中存放着父容器消费掉的距离,
 consumed[0]是x轴上的距离,consumed[1]是y轴上的距离
 * @param offsetInWindow  offsetInWindow偏移量
 */
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return false;
    }
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return false;
    }
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return false;
    }

NestedScrollingParent 接口方法:

    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return false;
    }
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
    }
    public void onStopNestedScroll(View target) {
    }
    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    }
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
    }
    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
        return false;
    }
    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
        return false;
    }
    public int getNestedScrollAxes() {
        return 0;
    }

整个流程对应关系:
一般是子view发起调用,父view接受回调。

子View 父View
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

NestedScrollingHelper 相关源码请参考:

http://www.jianshu.com/p/7548398c41ff
分析的很详细;

实战例子 - 上拉显示商品详情页

前面的介绍中,有2个实例,都非常好;可以试试;这里参考一好哥们实现的代码,将其思路一步一步展示出来;

类似于jd的商品详情页;

布局文件

通过自定义布局,嵌入2个或者3个子View(布局的上部分 topLayout 与下部分bottomLayout 都用 NestScrollingView包裹起来,并设置了id值);

topLayout 用于展示默认状态下的商品基本信息;
bottomLayout 当上拉完成时,用于展示商品详情

布局文件示例如下:



    
    

        

            

            ......

        
    

    
    

        

        
    

    
    

        

        

            

            

        
    


新建SlidingUpLayout类,用来对应布局文件

实现 NestedScrollingParent 接口,重写布局、测量方法,并设置一些减速标记信息;
主要方法在 onNestedPreScroll

class SlidingUpLayout(context: Context, attrs: AttributeSet?, defAttrStyle: Int) :
        ViewGroup(context, attrs, defAttrStyle), NestedScrollingParent {

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    companion object {
        val TWO_CHILD_COUNT = 2
        val THREE_CHILD_COUNT = 3
        val TAG = "SlidingUpLayout"
        val DEBUG = true
    }


    private val viewConfig: ViewConfiguration = ViewConfiguration.get(context)
    private var velocityTracker: VelocityTracker? = null
    private val scroller = ScrollerCompat.create(context)
    private val maxFlingVelocity = viewConfig.scaledMaximumFlingVelocity
    private val minFlingVelocity = viewConfig.scaledMinimumFlingVelocity

    private var scrollDuration = 1000
    private val resistance = 1.8f      // 阻力系数


    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (childCount != TWO_CHILD_COUNT && THREE_CHILD_COUNT != childCount) {
            throw IllegalArgumentException("Error child count! must two or three child count")
        }

        measureChildren(widthMeasureSpec, heightMeasureSpec)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        var layoutTop = paddingTop
        (0..childCount - 1).map { getChildAt(it) }.forEach {
            it.layout(l, layoutTop, r, layoutTop + it.measuredHeight)
            layoutTop += it.measuredHeight
        }
    }

    // -------------------- 嵌套滑动 -----------------------
    override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
        return 0 != (ViewCompat.SCROLL_AXIS_VERTICAL and nestedScrollAxes)   // 开启
    }

    /**
     * target 会发生变化,要么是topLayout,要么是bottomLayout
     */
    /**
     * target 会发生变化,要么是topLayout,要么是bottomLayout
     */
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
        val topLayout = getChildAt(0)
        val bottomLayout = getChildAt(childCount - 1)

        if (null != topLayout.findViewById(target.id)) {
            if (dy > 0 && !ViewCompat.canScrollVertically(target, dy)) {    // (上拉)
                consumed[1] = (dy / resistance).toInt()
                scrollBy(dx, consumed[1])
            } else if (dy < 0 && scrollY > 0) {         // 下拉 && SlidingUpLayout整体 往上偏移时) 消费dy
                consumed[1] = dy
                scrollBy(dx, (consumed[1] / resistance).toInt())
            }
        } else if (null != bottomLayout.findViewById(target.id)) {
            // (下拉) or (bottomLayout没有完整显示时),拦截
            // 完整显示时:bottomLayout.top = scrollY
            if ((dy < 0 && !ViewCompat.canScrollVertically(target, dy)) ||
                    bottomLayout.top > scrollY) {
                consumed[1] = dy
                var dy = (dy / resistance).toInt()
                if (scrollY + dy > bottomLayout.top) {  // 不能越界
                    dy = bottomLayout.top - scrollY
                }
                scrollBy(dx, dy)
            }
        }
    }

看一下效果:

Android 嵌套滑动的研究篇二 - 仿jd商品页_第1张图片
能够滑动的效果

展示与关闭详情页逻辑 (onStopNestedScroll)

在手指抬起那一刻,我们判断一下,滑动的距离,进行详情页的关闭与打开;
onStopNestedScroll 回调方法中,进行距离判断,因 onStopNestedScroll 方法nestedScrolling开始时,便会调用,所以在这里加入成员变量记录一下,避免关闭or打开方法的无效执行;

private var isNestedPreScroll = false //此标记标记nested内为拖动事件

// onNestedPreScroll 发生了设置为true
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
     ...
    isNestedPreScroll = true
}
override fun onStopNestedScroll(target: View) {
        if (DEBUG) Log.e(TAG, "onStopNestedScroll")
        if (isNestedPreScroll) {
            val topLayout = getChildAt(0)
            val bottomLayout = getChildAt(childCount - 1)
            if (null != topLayout.findViewById(target.id)) {
                if (scrollY > minScrollDistance) {
                    setLayoutShadow(Gravity.END)        // 显示bottom
                } else {
                    setLayoutShadow(Gravity.START)
                }
            } else if (null != bottomLayout.findViewById(target.id)) {
                if (scrollY + minScrollDistance < bottomLayout.top) {
                    setLayoutShadow(Gravity.START)
                } else {
                    setLayoutShadow(Gravity.END)       // 显示bottom
                }
            }
        }
        isNestedPreScroll = false
    }

   /**
     * 打开或关闭当前布局体
     * 展开上边
     * @see Gravity.TOP Gravity.START
     * 展开底部
     * @see Gravity.BOTTOM  Gravity.END
     */
    fun setLayoutShadow(gravity: Int) {
        if (gravity == Gravity.BOTTOM || gravity == Gravity.END) {
            val bottomLayout = getChildAt(childCount - 1)
            scroller.startScroll(scrollX, scrollY, 0, bottomLayout.top - scrollY, scrollDuration)
        } else if (gravity == Gravity.TOP || gravity == Gravity.START) {
            scroller.startScroll(scrollX, scrollY, 0, -scrollY, scrollDuration)
        }
        invalidate()
    }

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

效果如下:

Android 嵌套滑动的研究篇二 - 仿jd商品页_第2张图片
自动滑动的处理

处理Fling

监听瞬间加速度,重写系统的 dispatchTouchEvent,在这里进行加速度的判断;新增成员变量 isNestedFling 用来记录是否 fling,避免 onStopNestedScroll 再次执行

private var isNestedFling = false//此标记标记nested本次流程内为惯性滑动事件

// 修改一下,加入变量判断
override fun onStopNestedScroll(target: View) {
        if (isNestedPreScroll && !isNestedFling) {
         ....
        }
        isNestedPreScroll = false
        isNestedFling = false
    }

override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        initVelocityTrackerIfNotExists()
        velocityTracker?.addMovement(ev)

        when (ev.action) {
            MotionEvent.ACTION_UP -> {
                velocityTracker?.let {
                    it.computeCurrentVelocity(1000, maxFlingVelocity.toFloat())
                    if (Math.abs(it.yVelocity) > minFlingVelocity) {
                        val topLayout = getChildAt(0)
                        val bottomLayout = getChildAt(childCount - 1)
                        val scrollView = getCurrentScrollView(ev.y)

                        if (DEBUG) {
                            Log.e(TAG, "fling===>  scrollY:$scrollY yVelocity: ${it.yVelocity} scrollView: $scrollView")
                        }

                        if (scrollY > 0 && topLayout == scrollView) {                           // 向上滑动
                            if (it.yVelocity > 0) {
                                setLayoutShadow(Gravity.START)
                            } else {
                                setLayoutShadow(Gravity.END)
                            }
                        } else if (bottomLayout == scrollView && (scrollY < bottomLayout.top)) { // 向下滑动
                            if (it.yVelocity > 0) {
                                setLayoutShadow(Gravity.START)
                            } else {
                                setLayoutShadow(Gravity.END)
                            }
                        }
                        isNestedFling = true
                    }
                }
                releaseVelocity()
            }

            MotionEvent.ACTION_CANCEL -> releaseVelocity()
        }

        return super.dispatchTouchEvent(ev)
    }

效果如下:

Android 嵌套滑动的研究篇二 - 仿jd商品页_第3张图片
实现 fling效果

细节整理

scroller 未结束的一些整理

 override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean{
        if(!scroller.isFinished){
            scroller.abortAnimation()
            invalidate()
        }
        return 0!=(ViewCompat.SCROLL_AXIS_VERTICAL and nestedScrollAxes)
    }

你可能感兴趣的:(Android 嵌套滑动的研究篇二 - 仿jd商品页)