支持嵌套滚动的下拉刷新实现
- 相信大家都使用过PullToRefreshListView, 这个作者没有更新了5年的上古神器,至今如果使用ListView 的话仍然可以愉快的使用,但是自从RecyclerView出现之后,ListView 在我们代码里面出现的频率越来越低,甚至一个项目里面的所有布局都使用了RecyclerView实现,此时原来的PullToRefreshListView将失去它的作用,现在大多数项目会使用Google support 里的
SwipeRefreshLayout
来进行下拉刷新的操作,使用过它的人多数都应该知道,其实我们还是需要做一些重写处理才能很好的进行使用;那么话说回来,如果我们想使用PullToRefreshListView 这个框架去接入RecyclerView 的上拉和下拉呢?其实上也是非常好扩展的,网上已经有很多的例子。但是,网上的大多数Demo只是简单的按照PullToRefreshListView原本的框架去扩展实现了RecyclerView 的下拉刷新,很少有考虑了RecyclerView嵌套滚动这一个特性的,以至于我们在使用了嵌套滚动布局的时候,就会出现下拉刷新和嵌套滚动冲突的情况,我觉得这样不太合理,所以根据嵌套滚动的实现逻辑,重新去继承修改了PullToRefreshListView 的源码,重新封装了一个PullToRefreshAttacher,接下来我讲讲实现逻辑。 - NestedScroll: android NestedScroll 的支持是从5.0开始的,只要您的SDK版本在5.0以上,翻看ScrollView 的源代码就能看到它已经支持了嵌套滚动。好在android 官方给我们提供了各种兼容库,这个ScrollView 嵌套滚动的低版本兼容的代码NestedScrollView就在V4包里。接下来我们来讲讲兼容包里的嵌套滚动的实现思路:
1. v4包里有两个接口:NestedScrollingChild
,它用来定义目标视图何时开始嵌套滚动和需要嵌套滚动的偏移量, 以及执行嵌套滚动的NestedScrollingParent
,它接受来自NestedScrollingChild
给定的需要嵌套滚动的参数去对需要滚动的目标执行坐标偏移,达到和目标视图一同滚动的效果。 如果您要从头实现一个自定义的可以嵌套滚动的视图,那么在您需要在目标视图扩展NestedScrollingChild
以及目标视图的父布局扩展NestedScrollingParent
,但好在v4包已经有帮我们实现过这两个接口的类,我们可以在布局里直接添加,如果您的一个滚动布局里需要支持嵌套滚动那么您只需要在最外层加上v4包里的CoordinatorLayout
布局即可。
2.NestedScrollingChildHelper
和NestedScrollingParentHelper
: 看名字就能看出,这两个类的作用是帮助您去实现嵌套滚动的,接下来的实现里我们会看到它的身影
3.NestedScrollingChild
和NestedScrollingParent
:接下来我们来看看它到底需要我们去扩展实现那些方法,代码如下:
public interface NestedScrollingChild {
public void setNestedScrollingEnabled(boolean enabled);
public boolean isNestedScrollingEnabled();
public boolean startNestedScroll(int axes);
public void stopNestedScroll();
public boolean hasNestedScrollingParent();
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
public interface NestedScrollingParent {
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
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);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
public int getNestedScrollAxes();
代码分析:
NestedScrollingChild
的所有扩展方法都可以通过NestedScrollingChildHelper
的方法去实现,大家可以去看下NestedScrollingChildHelper的代码,这里讲解几个方法,以下方法默认一一对应使用NestedScrollingChildHelper
里的方法, 而NestedScrollingParentHelper
主要承担一些状态记录的,相对简单,我们这里把NestedScrollingChild
和NestedScrollingParent
的方法一一对应起来分析:public void setNestedScrollingEnabled(boolean enabled);
顾名思义,要实现嵌套滚动,这一步是必须的,对应isNestedScrollingEnabled()
public boolean startNestedScroll(int axes);
和public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
来看下NestedScrollingChildHelper
里如何实现这个方法:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
我们可以看到,如果找到了NestedScrollingParent
那么将回调 parent 里的 onStartNestedScroll
和 onNestedScrollAccepted
,parent在这里可以做一些滚动开始之前的初始化处理,然后返回true;若没找到parent则返回false, 这个一般在手指按下的事件里调用,表示开始支持嵌套滚动啦,如果这里返回的是false,那么接下来的方法将不会起作用,因为没有NestedScrollingParent
支持。
3. public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
和
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
: 重点来了,这个方法实现稍稍复杂,就不贴代码了, 它围绕NestedScrollingChild
和 NestedScrollParent
展开,先来看下dispatchNestedPreScroll
官方的注释:
Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
*
*Nested pre-scroll events are to nested scroll events what touch intercept is to touch.
*dispatchNestedPreScroll
offers an opportunity for the parent view in a nested
* scrolling operation to consume some or all of the scroll operation before the child view
* consumes it.
*
* @param dx Horizontal scroll distance in pixels
* @param dy Vertical scroll distance in pixels
* @param consumed Output. If not null, consumed[0] will contain the consumed component of dx
* and consumed[1] the consumed dy.
* @param offsetInWindow Optional. If not null, on return this will contain the offset
* in local view coordinates of this view from before this operation
* to after it completes. View implementations may use this to adjust
* expected input coordinate tracking.
* @return true if the parent consumed some or all of the scroll delta
* @see #dispatchNestedScroll(int, int, int, int, int[])
接下来我们解释它的作用:当目标视图,比如一个RecyclerView 或者 NestedScrollView 要进行滚动之前,目标视图会先调用此方法,然后通过NestedScrollingChildHelper
调用到NestedScrollingParent
的 onNestedPreScroll
方法,此时父布局会根剧传入的偏移量还有其子view的LayoutParams 进行相应的处理,然后把消耗的偏移量放入 consumed
数组,目标视图将会根据consumed
去修正偏移量,然后再去滚动自己。
4. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
和
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
:同样先来看下
int dxUnconsumed, int dyUnconsumed)NestedScrollingChild
里的描述:
/**
* Dispatch one step of a nested scroll in progress.
*
*Implementations of views that support nested scrolling should call this to report
* info about a scroll in progress to the current nested scrolling parent. If a nested scroll
* is not currently in progress or nested scrolling is not
* {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.
* Compatible View implementations should also call
* {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before
* consuming a component of the scroll event themselves.
*
* @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step
* @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step
* @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view
* @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view
* @param offsetInWindow Optional. If not null, on return this will contain the offset
* in local view coordinates of this view from before this operation
* to after it completes. View implementations may use this to adjust
* expected input coordinate tracking.
* @return true if the event was dispatched, false if it could not be dispatched.
* @see #dispatchNestedPreScroll(int, int, int[], int[])
*/
解释:dispatchNestedScroll
将会在目标视图(如RecyclerView)滚动完之后调用,此时目标视图将会把已经消耗和未消耗的偏移量传入方法,此时将会在NestedScrollingParent
里产生onNestedScroll
回调,parent根据传入的已消耗和未消耗的偏移量进行嵌套滚动,将最终偏移的结果传入offsetInWindow
,目标视图通过offsetInWindow
进行偏移量修正。
5. public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)
和 public boolean onNestedPreFling(View target, float velocityX, float velocityY)
: fling操作发生在,手指移动后抬起的事件上,此时视图将继续滚动,velocityX
和 velocityY
表示此时继续滚动的速度,一般会配合 VelocityTracker
使用,传入一个速度,接下来的操作和dispatchNestedPreScroll类似。
6. public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
和
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)public boolean onNestedPreFling(View target, float velocityX, float velocityY)
:参照第5
2. 下拉刷新实现
* 有了上面的分析,我们知道,如果是支持嵌套滚动的滚动视图,那么就会实现NestedScrollingChild
方法,用来通知NestedScrollingParent
如何滚动,那么我们沿着这个思路,我们想要知道何时开始下拉刷新或者上拉加载,只需要扩展NestedScrollingParent
接口,从public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
监听里,我们就能知道他的消耗情况,如果dxUnconsumed 或者 dyUnconsumed 不为0 ,说明还没有消耗完,此时我们再扩展
int dxUnconsumed, int dyUnconsumed)NestedScrollingChild
接口, 通过NestedScrollingChildHelper
去执行public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
方法去调用真正的
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)NestedScrollingParent
进行preScroll操作,之后减去通过真正parent消耗的offsetInWindow
偏移量,此时如果uncounsumed
还不为0.则说明,此时嵌套滚动完成,这部分偏移量已经不会继续使用,那么我们就可以通过unconsumed 去进行下拉刷新或者上拉加载(Unconsumed < 0 , 为手指向下移动)
* 如果此时下拉刷新或者上拉加载界面已经展示,此时我们手指回滚,需要收起这个界面,那么我们在NestedScrollingParent
接口的public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
里实现,根据上面分析我们知道,此方法调用时,目标视图还没开始滚动,所以如果此时下拉或者上拉已经执行,那么我们根据其dx 或者 dy偏移量把上拉 / 下拉 出现的视图进行偏移,之后进行偏移量修正,通过NestedScrollingChild
调用public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
方法对真正的NestedScrollingParent
进行嵌套滚动执行
* 当NestedScrollingParent
的public void onStopNestedScroll(View target)
方法回调执行时,说明此时嵌套滚动停止,此时手指是抬起状态,那么我们调用下拉刷新框架的setState
方法处理刷新或者收起操作。
* 由于在stop时已经处理过下拉/上拉的刷新或者收起操作,我们不处理fling操作,直接实现NestedScrollingParent
的fling回调通过NestedScrollingChild
的fling操作给真正的NestedScrollingParent
进行嵌套滚动
3. 文字描述不够有力量,上核心代码,所有代码请参考:PullToRefreshAttacher:
/*******************************************************************************
* Copyright 2011, 2012 Chris Banes.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package ptra.hacc.cc.ptr;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.IntDef;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import ptra.hacc.cc.ptr.internal.FlipLoadingLayout;
import ptra.hacc.cc.ptr.internal.LoadingLayout;
import ptra.hacc.cc.ptr.internal.RotateLoadingLayout;
import ptra.hacc.cc.ptr.internal.Utils;
import ptra.hacc.cc.ptr.internal.ViewCompat;
public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T>{
protected static final int EXTRA_START = 0x1;
protected static final int EXTRA_BEFORE_PULL = 0x2;
protected static final int EXTRA_AFTER_PULL = 0x3;
// @IntDef({EXTRA_START, EXTRA_BEFORE_PULL, EXTRA_AFTER_PULL})
// @Retention(RetentionPolicy.SOURCE)
// protected @interface EXTRA_STATE{}
// ===========================================================
// Constants
// ===========================================================
static final boolean DEBUG = true;
static final boolean USE_HW_LAYERS = false;
static final String LOG_TAG = "PullToRefresh";
protected static final float FRICTION = 2.0f;
public static final int SMOOTH_SCROLL_DURATION_MS = 200;
public static final int SMOOTH_SCROLL_LONG_DURATION_MS = 325;
static final int DEMO_SCROLL_INTERVAL = 225;
static final String STATE_STATE = "ptr_state";
static final String STATE_MODE = "ptr_mode";
static final String STATE_CURRENT_MODE = "ptr_current_mode";
static final String STATE_SCROLLING_REFRESHING_ENABLED = "ptr_disable_scrolling";
static final String STATE_SHOW_REFRESHING_VIEW = "ptr_show_refreshing_view";
static final String STATE_SUPER = "ptr_super";
// ===========================================================
// Fields
// ===========================================================
private int mTouchSlop;
private float mLastMotionX, mLastMotionY;
private float mInitialMotionX, mInitialMotionY;
private boolean mIsBeingDragged = false;
private State mState = State.RESET;
private PullWay mPullWay = PullWay.getDefault();
private Mode mMode = Mode.getDefault();
Mode mCurrentMode;
T mRefreshableView;
private FrameLayout mRefreshableViewWrapper;
private boolean mShowViewWhileRefreshing = true;
private boolean mScrollingWhileRefreshingEnabled = false;
private boolean mFilterTouchEvents = true;
private boolean mOverScrollEnabled = true;
private boolean mLayoutVisibilityChangesEnabled = true;
private Interpolator mScrollAnimationInterpolator;
private AnimationStyle mLoadingAnimationStyle = AnimationStyle.getDefault();
private LoadingLayout mHeaderLayout;
private LoadingLayout mFooterLayout;
OnRefreshListener<T> mOnRefreshListener;
OnRefreshListener2<T> mOnRefreshListener2;
private OnPullEventListener<T> mOnPullEventListener;
private SmoothScrollRunnable mCurrentSmoothScrollRunnable;
// /**
// * editor by Hale Yang
// * mScrollingChildHelper
// */
// private NestedScrollingChildHelper mScrollingChildHelper;
// ===========================================================
// Constructors
// ===========================================================
public PullToRefreshBase(Context context) {
super(context);
init(context, null);
}
public PullToRefreshBase(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public PullToRefreshBase(Context context, Mode mode) {
super(context);
mMode = mode;
init(context, null);
}
public PullToRefreshBase(Context context, Mode mode, AnimationStyle animStyle) {
super(context);
mMode = mode;
mLoadingAnimationStyle = animStyle;
init(context, null);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (DEBUG) {
Log.d(LOG_TAG, "addView: " + child.getClass().getSimpleName());
}
final T refreshableView = getRefreshableView();
if (refreshableView instanceof ViewGroup) {
((ViewGroup) refreshableView).addView(child, index, params);
} else {
throw new UnsupportedOperationException("Refreshable View is not a ViewGroup so can't addView");
}
}
@Override
pu