SwipeRefreshLayout
谷歌的下拉刷新,新的界面效果
Install / Use
/learn @hanks-zyh/SwipeRefreshLayoutREADME
简介
SwipeRefreshLayout 是一个下拉刷新控件,几乎可以包裹一个任何可以滚动的内容(ListView GridView ScrollView RecyclerView),可以自动识别垂直滚动手势。使用起来非常方便。
| | |
|:-:|:-:|
||
|
1.将需要下拉刷新的空间包裹起来
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</android.support.v4.widget.SwipeRefreshLayout>
2.设置刷新动画的触发回调
//设置下拉出现小圆圈是否是缩放出现,出现的位置,最大的下拉位置
mySwipeRefreshLayout.setProgressViewOffset(true, 50, 200);
//设置下拉圆圈的大小,两个值 LARGE, DEFAULT
mySwipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);
// 设置下拉圆圈上的颜色,蓝色、绿色、橙色、红色
mySwipeRefreshLayout.setColorSchemeResources(
android.R.color.holo_blue_bright,
android.R.color.holo_green_light,
android.R.color.holo_orange_light,
android.R.color.holo_red_light);
// 通过 setEnabled(false) 禁用下拉刷新
mySwipeRefreshLayout.setEnabled(false);
// 设定下拉圆圈的背景
mSwipeLayout.setProgressBackgroundColor(R.color.red);
/*
* 设置手势下拉刷新的监听
*/
mySwipeRefreshLayout.setOnRefreshListener(
new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// 刷新动画开始后回调到此方法
}
}
);
通过 setRefreshing(false) 和 setRefreshing(true) 来手动调用刷新的动画。
onRefresh的回调只有在手势下拉的情况下才会触发,通过setRefreshing只能调用刷新的动画是否显示。 SwipeRefreshLayout 也可放在 CoordinatorLayout 内共同处理滑动冲突,有兴趣可以尝试。
SwipeRefreshLayout 源码分析
本文基于 v4 版本
23.2.0
extends ViewGroup implements NestedScrollingParent NestedScrollingChild
java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.support.v4.widget.SwipeRefreshLayout
SwipeRefreshLayout 的分析分为两个部分:自定义 ViewGroup 的部分,处理和子视图的嵌套滚动部分。
SwipeRefreshLayout extends ViewGroup
其实就是一个自定义的 ViewGroup ,结合我们自己平时自定义 ViewGroup 的步骤:
- 初始化变量
- onMeasure
- onLayout
- 处理交互 (
dispatchTouchEventonInterceptTouchEventonTouchEvent)
接下来就按照上面的步骤进行分析。
1.初始化变量
SwipeRefreshLayout 内部有 2 个 View,一个圆圈(mCircleView),一个内部可滚动的 View(mTarget)。除了 View,还包含一个 OnRefreshListener 接口,当刷新动画被触发时回调。

/**
* Constructor that is called when inflating SwipeRefreshLayout from XML.
*
* @param context
* @param attrs
*/
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 系统默认的最小滚动距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 系统默认的动画时长
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
setWillNotDraw(false);
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
// 获取 xml 中定义的属性
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
// 刷新的圆圈的大小,单位转换成 sp
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density);
mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density);
// 创建刷新动画的圆圈
createProgressView();
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
// the absolute offset has to take into account that the circle starts at an offset
mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
// 刷新动画的临界距离值
mTotalDragDistance = mSpinnerFinalOffset;
// 通过 NestedScrolling 机制来处理嵌套滚动
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
// 创建刷新动画的圆圈
private void createProgressView() {
mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
mProgress = new MaterialProgressDrawable(getContext(), this);
mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
mCircleView.setImageDrawable(mProgress);
mCircleView.setVisibility(View.GONE);
addView(mCircleView);
}
初始化的时候创建一个出来一个 View (下拉刷新的圆圈)。可以看出使用背景圆圈是 v4 包里提供的 CircleImageView 控件,中间的是 MaterialProgressDrawable 进度条。
另一个 View 是在 xml 中包含的可滚动视图。
2.onMeasure
onMeasure 确定子视图的大小。
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
// 确定内部要滚动的View,如 RecycleView
ensureTarget();
}
if (mTarget == null) {
return;
}
// 测量子 View (mTarget)
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
// 测量刷新的圆圈 mCircleView
mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY));
if (!mUsingCustomStart && !mOriginalOffsetCalculated) {
mOriginalOffsetCalculated = true;
mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight();
}
// 计算 mCircleView 在 ViewGroup 中的索引
mCircleViewIndex = -1;
// Get the index of the circleview.
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
这个步骤确定了 mCircleView 和 SwipeRefreshLayout 的子视图的大小。
3.onLayout
onLayout 主要负责确定各个子视图的位置。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// 获取 SwipeRefreshLayout 的宽高
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
// 考虑到给控件设置 padding,去除 padding 的距离
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
// 设置 mTarget 的位置
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
// 根据 mCurrentTargetOffsetTop 变量的值来设置 mCircleView 的位置
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}

在 onLayout 中放置了 mCircleView 的位置,注意 顶部位置是 mCurrentTargetOffsetTop ,mCurrentTargetOffsetTop 初始距离是-mCircleView.getMeasuredHeight(),所以是在 SwipeRefreshLayout 外。
经过以上几个步骤,SwipeRefreshLayout 创建了子视图,确定他们的大小、位置,现在所有视图可以显示在界面了。
处理与子视图的滚动交互
下拉刷新控件的主要功能是当子视图下拉到最顶部时,继续下拉可以出现刷新动画。而子视图可以滚动时需要将所有滚动事件都交给子视图。借助 Android 提供的 NestedScrolling 机制,使得 SwipeRefreshLayout 很轻松的解决了与子视图的滚动冲突问题。
SwipeRefreshLayout 通过实现 NestedScrollingParent 和 NestedScrollingChild 接口来处理滚动冲突。SwipeRefreshLayout 作为 Parent 嵌套一个可以滚动的子视图,那么就需要了解一下 NestedScrollingParent 接口
/**
当你希望自己的自定义布局支持嵌套子视图并且处理滚动操作,就可以实现该接口。
实现这个接口后可以创建一个 NestedScrollingParentHelper 字段,使用它来帮助你处理大部分的方法。
处理嵌套的滚动时应该使用 `ViewCompat`,`ViewGroupCompat`或`ViewParentCompat` 中的方法来处理,这是一些兼容库,
他们保证 Android 5.0之前的兼容性垫片的静态方法,这样可以兼容 Android 5.0 之前的版本。
*/
public interface NestedScrollingParent {
/**
* 当子视图调用 startNestedScroll(View, int) 后调用该方法。返回 true 表示响应子视图的滚动。
* 实现这个方法来声明支持嵌套滚动,如果返回 true,那么这个视图将要配合子视图嵌套滚动。当嵌套滚动结束时会调用到 onStopNestedScroll(View)。
*
* @param child 可滚动的子视图
* @param target NestedScrollingParent 的直接可滚动的视图,一般情况就是 child
* @param nestedScrollAxes 包含 ViewCompat#SCROLL_AXIS_HORIZONTAL, ViewCompat#SCROLL_AXIS_VERTICAL 或者两个值都有。
* @return 返回 true 表示响应子视图的滚动。
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* 如果 onStartNestedScroll 返回 true ,然后走该方法,这个方法里可以做一些初始化。
*/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* 子视图开始滚动前会调用这个方法。这时候父布局(也就是当前的 NestedScrollingParent 的实现类)可以通过这个方法来配合子视图同时处理滚动事件。
*
* @param target 滚动的子视图
* @param dx 绝对值为手指在x方向滚动的距离,dx<0 表示手指在屏幕向右滚动
* @param dy 绝对值为手指在y方向滚动的距离,dy<0 表示手指在屏幕向下滚动
* @param consumed 一个数组,值用来表示父布局消耗了多少距离,未消耗前为[0,0], 如果父布局想处理滚动事件,就可以在这个方法的实现中为consumed[0],consumed[1]赋值。
* 分别表示x和y方向消耗的距离。如父布局想在竖直方向(y)完全拦截子视图,那么让 consumed[1] = dy,就把手指产生的触摸事件给拦截了,子视图便响应不到触摸事件了 。
*/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* 这个方法表示子视图正在滚动,并且把滚动距离回调用到该方法,前提是 onStartNestedScroll 返回了 true。
* <p>Both the consumed and unconsumed portions of the scroll distance are reported to the
* ViewParent. An implementation may choose to use the consumed portion to match or chase scroll
* position of multiple child elements, for example. The unconsumed portion may be used to
* allow continuous dragging of multiple scrolling or draggable elements, such as scrolling
* a list within a vertical drawer whe
