參考文章:
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来方便编程。
借用网上的一张图,能够看到两个接口之间的对应关系:
二、实例演示:改造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
- 2016-09-28 21:03
- 4700
-
使用SwipeRefreshLayout和RecyclerView实现仿“简书”下拉刷新和上拉载入很多其它
一、概述 我们公司眼下开发的全部Android APP都是遵循iOS风格设计的。这并非一个好现象。我决定将Android 5.x控件引入近期开发的项目中,使用RecyclerView代替以往使用的L...- LeoLeoHan
- 2016-03-26 23:23
- 31821
-
NestedScrolling机制(一)——概述
现在。NestedScrolling机制(能够称为嵌套滚动或嵌套滑动)在各种app中的应用已经十分广泛了,下图是“饿了么”中的一个样例:当向上滚动列表时。列表的父view(整个白色部分)会跟着一起向上...- al4fun
- 2016-12-26 20:28
- 1607
-
让Android Support V4中的SwipeRefreshLayout支持上拉载入很多其它
Android SDK中并没有默认加入下拉刷新组件。可是在Support V4中官方给出了一个SwipeRefreshLayout组件。它支持下拉刷新功能,但却不支持滑动究竟部时的上拉载入很多其它的功能。...- bboyfeiyu
- 2014-10-10 10:23
- 29608
-
android 高仿IOS水滴版上下拉刷新的Listview
之前有分享一些刷新的Demo,近期找到一个刷新的样例,分享给大家。同一时候感谢原作者的分享! 如今给大家分享一个高仿IOS的Listview刷新效果,支持上下拉刷新。有些相似自己定义的XLis...- seven2729
- 2015-09-18 11:41
- 2752
-
SwipeRefreshLayout + RecyclerView 实现 上拉刷新 和 下拉刷新
SwipeRefreshLayout 是谷歌公司推出的用于下拉刷新的控件。SwipeRefreshLayout已经被放到了sdk中。在Version 19.1之后SwipeRefreshLayou...- dalancon
- 2015-05-28 17:06
- 116874
-
Android开发之 SwipeRefreshLayout
SwipeRefreshLayout概述 用户通过手势或者点击某个button实现内容视图的刷新,布局里增加SwipeRefreshLayout嵌套一个子视图如ListView、RecyclerView等...- AnalyzeSystem
- 2016-05-03 15:50
- 4939
-
SwipeRefreshLayout完美加入及完好上拉载入功能
项目地址:https://git.oschina.net/whos/SwipeRefreshAndLoadLayout/wikis/home 关于Google推出的下拉刷新控件SwipeRe...- ljx19900116
- 2014-12-01 16:28
- 75446
-
使用Android SwipeRefreshLayout了解Android的嵌套滑动机制
使用Android SwipeRefreshLayout了解Android的嵌套滑动机制 NestedScrollingChild,NestedScrollingParent。NestedScroll...- hehe26
- 2016-12-06 17:16
- 3062
-
通俗易懂的小样例来演示怎样使用NestedScroll
写在前面近期遇到了一个问题,在SwipeRefreshLayout中。有时候下拉,圆球不会下来,等松开手指的时候,球会突然闪一下,不明所以。想到这个应该是滑动相关的问题。并且跟嵌套滑动似乎非常有关联.- dingding_android
- 2016-10-27 18:19
- 1399
11条评论