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 当上拉完成时,用于展示商品详情

布局文件示例如下:

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

    <!-- topLayout -->
    <android.support.v4.widget.NestedScrollView
        android:id="@+id/sliding_top"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical">

            <TextView
                android:id="@+id/text1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:paddingBottom="48dp"
                android:paddingTop="48dp"
                android:text="Text1"
                android:textSize="48sp"/>

            ......

        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>

    <!-- 中间提示部分 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:text="上拉查看商品详情"/>

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="#ffffff"/>
    </LinearLayout>

    <!-- bottomLayout -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <View
            android:id="@+id/titleBar"
            android:layout_width="match_parent"
            android:layout_height="?actionBarSize"
            android:background="?colorAccent"/>

        <android.support.v4.widget.NestedScrollView
            android:id="@+id/sliding_bottom"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@color/colorAccent"
            android:orientation="vertical">

            <WebView
                android:id="@+id/webview"
                android:layout_width="match_parent"
                android:layout_height="match_parent">

            </WebView>

        </android.support.v4.widget.NestedScrollView>
    </LinearLayout>

</test.com.widget.nested.view.SlidingUpLayout>

新建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)
            }
        }
    }

看一下效果:

能够滑动的效果

展示与关闭详情页逻辑 (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()
        }
    }

效果如下:

自动滑动的处理

处理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)
    }

效果如下:

实现 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)
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容