參考文章:

https://segmentfault.com/a/1190000002873657
http://blog.csdn.net/al4fun/article/details/53888990

一、NestedScrolling机制

吐槽:之前笔者在设计的时候,想在ViewPager的页面上实现仿微信的左滑删除。可是怎么都实现不了,由于当中跟ViewPager的滑动冲突了,当时才疏学浅(如今也是)。进了非常多坑,比方滑动的拦截、滑动事件在Down之后会跳过推断等。在没有系统学习过这方面知识的情况下以大败告终。
所以。谷歌人性化地推出了这个机制,滑动之前和爸爸商议一下,一切多么融洽。和之前盲人摸象的方式相比人性化多了。

1.滑动流程
子view获取到点击事件后,询问父亲是否须要配合滑动,然后每一次滑动之前都会询问父亲,并记录下父亲消耗的滑动距离,在上面完毕后才进行自己自身的滑动。

2.接口

NestedScrollingChild

//開始滑动
public boolean startNestedScroll(int axes);
//停止滑动
public void stopNestedScroll();
//在滑动前,进行滑动事件分配(询问)。consumed是父亲消耗的滑动距离,offsetInWindow
//是父亲在窗体中进行的对应的移动,子view须要依据这个进行自身调整(须要的话)
//差别于以下的,在这里能够进行父亲预备处理
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
//滑动后滑动事件的分配,子view询问父亲是否须要在滑动后消耗事件
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);

//惯性滚动相关
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

NestedScrollingParent

//当子view開始滑动时调用,能够在这里选择是否要与子view嵌套滑动。从而返回boolean
//当中target是发起滑动的对象,child是包括target的子view。nestedScrollAxes是方向标志位
//SCROLL_AXIS_HORIZONTAL 或 SCROLL_AXIS_VERTICAL
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
//老实说我认为这种方法在有了上面的onStartNestedScroll之后就有点鸡肋了
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
//结束滑动时调用
public void onStopNestedScroll(View target);

//在子滑动之前调用
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
//在子滑动之后调用    
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);

//当惯性嵌套滚动时被调用
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);

3.帮助类

NestedScrollingChildHelper
NestedScrollingParentHelper

故名思义。上面的帮助类帮助我们处理了上面父子接口的方法。它们帮助我们实现了逻辑上的方法。在一些情况下我们仅仅希望处理子接口或者父接口。为了对接能够在还有一个接口使用帮助类(比方以下的实例,改造SwipeRefreshLayout。我们更希望作为父亲处理子view事件而滑动自身。对于上层组件(父)不是非常关心,就能够使用NestedScrollingParentHelper来方便编程。

借用网上的一张图,能够看到两个接口之间的对应关系:

这里写图片描写叙述

图片来源:https://segmentfault.com/a/1190000002873657


二、实例演示:改造SwipeRefreshLayout

1.目的:
SwipeRefreshLayout就是一个实现了NestedScrolling机制的控件,能够方便的实现下拉刷新。如今我们想加上上拉刷新功能。能够反着做。给下方加一个可拉动的控件(小圆圈)。然后处理它的滑动事件。

为了能兼顾上层。我们再外面还加了常规的CoordinatorLayout和AppBarLayout作为測试。

成果:
这里写图片描写叙述

2.准备工作
改造之前当然要先把人家之前的成果准备好。
这里写图片描写叙述
首先当然是把我们的SwipeRefreshLayout移过来,能够换一个名字避免以后的冲突。然后布局中要用到两个控件。一个是CircleImageView。也就是显示的小圆圈,附带阴影功能;一个是MaterialProgressDrawable,用于在CircleImageView显示进程(颜色滑动)。在移动的时候SwipeRefreshLayout会报错,报错时把东西移动过来即可了。
然后阅读源代码。我们临时仅仅处理NestedScrolling机制,所以一般的移动流程比方onTouchEvent能够先放着,以后为了和其它不带NestedScrolling控件兼容的时候再改进。

3.開始改造工作
(1)參数測量
一个基本的问题就是,我们以下的圆圈(载入圈,以下简称圆圈)要放在哪?原生的直接放在中间然后上去一个圆圈身位的地方。所以以下圆圈水平位置一样。竖直的话就放在屏幕下方。

综合測试出这种距离比較好:

 WindowManager wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        int windowHeight = wm.getDefaultDisplay().getHeight();
               mOriginalOffsetBottom = windowHeight - mCircleDiameter/2;

所以对应的位置放置我们就这样:

mCircleViewBottom.layout((width / 2 - circleBottomWidth / 2), mCurrentTargetOffsetBottom - circleBottomHeight,
                (width / 2 + circleBottomWidth / 2), mCurrentTargetOffsetBottom);

(2)作为父控件配合滑动

  • a.是否配合滑动
 @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        //target 发起滑动的字view。能够不是当前view的直接子view
        //child 包括target的直接子view
        //返回true表示要与target配套滑动,为true则以下的accepted也会被调用
       //mReturningToStart是为了配合onTouchEvent的,这里我们不扩展
        return isEnabled() && !mReturningToStart && !mRefreshing && !mRefreshingBottom  && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//竖直方向
    }

这里我们仅仅添加了一个上拉刷新标志位

 @Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        Log.e(LOG_TAG,"onNestedScrollAccepted,axes="+axes);
        // Reset the counter of how much leftover scroll needs to be consumed.
        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
        // Dispatch up to the nested parent
        startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);//调用自己child接口的方法。实现向上传递
        mTotalUnconsumed = 0;
        //
        mTotalUnconsumedBottom = 0;
        mNestedScrollInProgress = true;

    }

这里也是,仅仅是添加了mTotalUnconsumedBottom ,这是我们上拉刷新的未消费路程。

  • b.滑动之前

我们能够看到源代码中的onNestedPreScroll有这么一段处理:

if (dy > 0 && mTotalUnconsumed > 0 ) {//向下拖dy小于0。所以这是为了处理拖circle到一半然后又缩回去的情况
            if (dy > mTotalUnconsumed) {//拖动的非常多,大于未消费的
                consumed[1] = dy - (int) mTotalUnconsumed;
                mTotalUnconsumed = 0;
            } else {//拖动一点,我们所实用给自己
                mTotalUnconsumed -= dy;
                consumed[1] = dy;
            }
            moveSpinner(mTotalUnconsumed);//move 到这个位置
        }

在滑动前先推断。我们未消费滑动路程是否还有,有则推断方向。假设是滑动的反方向,也就是我们再下拉刷新一半的时候又往回拉,这时做出处理。选择消费当前滑动路程。所以我们能够写出下方的拖动预处理:

if(dy<0 && mTotalUnconsumedBottom > 0 )
        {
             if(-dy>mTotalUnconsumedBottom)//假设拖动的非常多。就先给圆圈,然后还给子控件
            {
                consumed[1]= -(int) mTotalUnconsumedBottom;
                mTotalUnconsumedBottom = 0;
                mBottomIsScrolling = false;
            }else{//否则。全给父控件
                mTotalUnconsumedBottom +=dy;
                consumed[1]=dy;//          
                  }
            moveBottomSpinner(mTotalUnconsumedBottom);
        }

这里有个坑要提醒下。一開始笔者自作聪明。认为consumed參数应该传绝对值,导致后来往回滑的时候子控件跑得飞快(能够想想为什么),所以这里消费了负的路程就传回负的路程,能够看看源代码中NestedScrollingChildHelper的实现。

  • c.正式滑动

这个反而比較easy。仅仅须要加上推断当前子控件还能不能往上滑。

if(dy > 0 && !canChildScrollDown())
        {
            mTotalUnconsumedBottom +=dy;
            moveBottomSpinner(mTotalUnconsumedBottom);
            mBottomIsScrolling = true;
        }

滑动处理:

 private void moveBottomSpinner(float overscrollTop) {
        mProgressBottom.showArrow(true);
        float originalDragPercent = overscrollTop / mTotalDragDistance;
        float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
        float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;//这个不理解
        float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;//这样不就是负的吗//就是负的//能够是正的
        float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
                : mSpinnerOffsetEnd;///mSpinnerOffsetEnd 默认是拉到最底的可能位置 。和mTotalDragDistance一開始初始化是同样的
        float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
                / slingshotDist);
        float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
                (tensionSlingshotPercent / 4), 2)) * 2f;//tensionSlingshotPercent = x ,x/4-(x/4)^2,再*2,最多0.5
        float extraMove = (slingshotDist) * tensionPercent * 2;//这个是模拟后来的拖动,最多slingshotDist

        int targetY = mOriginalOffsetBottom - (int) ((slingshotDist * dragPercent) + extraMove);

大概解释一下,我们的滑动时分段的,在mTotalDragDistance滑动之前是线性的。在这之后会做一个加速的处理。最多延伸一个mSpinnerOffsetEnd

在实际的view位置改变中,我们使用的是

 setTargetOffsetTopAndBottomForBottom(targetY-mCurrentTargetOffsetBottom, true /* requires update */);

这种方法,里面是採用

ViewCompat.offsetTopAndBottom

来改变view的位置的。

  • d.结束滑动

假设手指离开的时候,拖动距离不为零,那么我们要做推断。做出对应的处理

if(mTotalUnconsumedBottom > 0 )
        {
            finishSpinnerBottom(mTotalUnconsumedBottom);
            mTotalUnconsumedBottom = 0;
            mBottomIsScrolling = false;
        }
  private void finishSpinnerBottom(float overscrollTop) {                
  if (overscrollTop > mTotalDragDistance) {
            setRefreshingBottom(true, true);
        } else {
            // cancel refresh
            mRefreshingBottom = false;
            mProgressBottom.setStartEndTrim(0f, 0f);
            AnimationListener listener = null;
            if (!mScale) {
                listener = new AnimationListener() {

                    @Override
                    public void onAnimationStart(Animation animation) {
                    }

                    @Override
                    public void onAnimationEnd(Animation animation) {
                        if (!mScale) {
                           // startScaleDownAnimation(null);
                            startScaleDownAnimationBottom(null);//倒着转
                        }
                    }

                    @Override
                    public void onAnimationRepeat(Animation animation) {
                    }

                };
            }
            animateOffsetToStartPositionBottom(mCurrentTargetOffsetBottom,listener);
            mProgressBottom.showArrow(false);
        }
    }

能够看到主要有两种处理,以mTotalDragDistance为界限。超过这个滑动距离我们就显示刷新,没有的话就动画回到原点。

(3)作为子控件配合滑动
我们知道。AppBarLayout和CoordinatorLayout会配合滑动。子view往上滑的时候会隐藏,假设不做处理,在下端圆圈滑动到一半的时候往回滑会把AppBar又拖出来,消费滑动事件。所以我们选择拦截,在下部圆圈滑动的时候优先处理滑动:

@Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
                //先拦截
        if(mBottomIsScrolling && mTotalUnconsumedBottom>0 &&dy<0)       {
            Log.e("fish","父:dispatchNestedPreScroll,mTotalUnconsumedBottom="+mTotalUnconsumedBottom+",dy=="+dy);
            if(-dy>=mTotalUnconsumedBottom)//向下拖动非常大
            {
                moveBottomSpinner(mTotalUnconsumedBottom);
            }else {
                moveBottomSpinner(-dy);
                mTotalUnconsumedBottom -= dy;
                dy = 0;
            }
        }
        return mNestedScrollingChildHelper.dispatchNestedPreScroll(
                dx, dy, consumed, offsetInWindow);
    }

到这里,我们的控件就基本完毕了,当然谷歌出版的控件,动画效果是不能少的。它的美观也体如今这里,由于基本是能模仿的,所以这里不再多讲。这个控件的改造主要麻烦在它的动画衔接以及滑动处理机制(加速等),剩下的都非常好理解,建议大家动手试一试。

项目地址:https://github.com/SGZoom/DailyWidget/tree/master/widgetpro

笔者仅仅是一名大学生,文章跟代码还有非常多不足之处,欢迎大家帮忙指出错误与不足,谢谢~


更新:2017-03-24
感谢评论区huowutong朋友的指正与改动,原来的程序存在bug,即上拉刷新的时候还能下拉刷新,在逻辑上疏漏了这一点。非常感激!

版权声明:本文为博主原创文章,未经博主同意不得转载。

举报

  • 本文已收录于下面专栏:

相关文章推荐

解决SwipeRefreshLayout与ScrollView滑动冲突

在页面为了兼容小屏幕设备我们须要嵌套一个ScrollView来让我们的布局能够滑动,此时恰好外层使用了SwipeRefreshLayout那滑动冲突就来了,以下给出解决的方法1.方法一:使用Nested...
  • a_zhon
  • a_zhon
  • 2016-09-28 21:03
  • 4700

使用SwipeRefreshLayout和RecyclerView实现仿“简书”下拉刷新和上拉载入很多其它

一、概述 我们公司眼下开发的全部Android APP都是遵循iOS风格设计的。这并非一个好现象。我决定将Android 5.x控件引入近期开发的项目中,使用RecyclerView代替以往使用的L...

NestedScrolling机制(一)——概述

现在。NestedScrolling机制(能够称为嵌套滚动或嵌套滑动)在各种app中的应用已经十分广泛了,下图是“饿了么”中的一个样例:当向上滚动列表时。列表的父view(整个白色部分)会跟着一起向上...
  • al4fun
  • al4fun
  • 2016-12-26 20:28
  • 1607

让Android Support V4中的SwipeRefreshLayout支持上拉载入很多其它

Android SDK中并没有默认加入下拉刷新组件。可是在Support V4中官方给出了一个SwipeRefreshLayout组件。它支持下拉刷新功能,但却不支持滑动究竟部时的上拉载入很多其它的功能。...

android 高仿IOS水滴版上下拉刷新的Listview

之前有分享一些刷新的Demo,近期找到一个刷新的样例,分享给大家。同一时候感谢原作者的分享!        如今给大家分享一个高仿IOS的Listview刷新效果,支持上下拉刷新。有些相似自己定义的XLis...

SwipeRefreshLayout + RecyclerView 实现 上拉刷新 和 下拉刷新

SwipeRefreshLayout 是谷歌公司推出的用于下拉刷新的控件。SwipeRefreshLayout已经被放到了sdk中。在Version 19.1之后SwipeRefreshLayou...

Android开发之 SwipeRefreshLayout

SwipeRefreshLayout概述 用户通过手势或者点击某个button实现内容视图的刷新,布局里增加SwipeRefreshLayout嵌套一个子视图如ListView、RecyclerView等...

SwipeRefreshLayout完美加入及完好上拉载入功能

项目地址:https://git.oschina.net/whos/SwipeRefreshAndLoadLayout/wikis/home 关于Google推出的下拉刷新控件SwipeRe...

使用Android SwipeRefreshLayout了解Android的嵌套滑动机制

使用Android SwipeRefreshLayout了解Android的嵌套滑动机制 NestedScrollingChild,NestedScrollingParent。NestedScroll...
  • hehe26
  • hehe26
  • 2016-12-06 17:16
  • 3062

通俗易懂的小样例来演示怎样使用NestedScroll

写在前面近期遇到了一个问题,在SwipeRefreshLayout中。有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题。并且跟嵌套滑动似乎非常有关联.
  • 微博
    微信
    QQ
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多仅仅同意输入30个字)

posted on 2017-08-19 11:55  lxjshuju  阅读(185)  评论(0编辑  收藏  举报