转自:
hanks-zyh/SwipeRefreshLayout
SwipeRefreshLayout 是什么,这个不用多说,本篇主要是看源码,学习.
通过setRefreshing(true)和setRefreshing(true)来手动调用触发和关闭刷新
注意:
onRefresh的回调,只有在手势下拉时,爱会触发,通过setRefreshing(true/false)只能控制刷新的动画是否显示(圈圈是否转转转).
SwipeRefreshLayout源码分析:
源码基于: appcompat-v7:27.1.1
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
- 处理交互 (dispatchTouchEvent onInterceptTouchEvent onTouchEvent)
接下来就按照上面的步骤进行分析。
1.初始化变量:
SwipeRefreshLayout 内部有 2 个 View,一个圆圈(mCircleView),一个内部可滚动的View(mTarget)。除了 View,还包含一个 OnRefreshListener 接口,当刷新动画被触发时回调。
preview.png
从 构造方法开始看:
/**
* 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();
// 系统默认的动画时长 400ms
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
//开启 完整 绘制 模式
setWillNotDraw(false);
//圈圈 show hide 的动画插值器
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
//圈圈 直径 与 像素密度 换算
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
// 创建刷新动画的圆圈
createProgressView();
//
setChildrenDrawingOrderEnabled(true);
// the absolute offset has to take into account that the circle starts at an offset
// Default offset in dips from the top of the view to where the progress spinner should stop
//绝对偏移量必须考虑到圆是从偏移量开始的
//圈圈 滑动的最大距离
mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density;
// 刷新动画的临界距离值
mTotalDragDistance = mSpinnerFinalOffset;
// 通过 NestedScrolling 机制来处理嵌套滚动
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
//nestal scroll enable 是 NestedScrollingChild 接口里的方法
setNestedScrollingEnabled(true);
//关于 圈圈 位置相关 onLayout时使用mCurrentTargetOffsetTop 的值
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
moveToStart(1.0f);
// 获取 xml 中定义的属性
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
创建刷新动画的圆圈:
private void createProgressView() {
mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT);
mProgress = new CircularProgressDrawable(getContext());
mProgress.setStyle(CircularProgressDrawable.DEFAULT);
mCircleView.setImageDrawable(mProgress);
mCircleView.setVisibility(View.GONE);
addView(mCircleView);
}
初始化的时候创建一个出来一个 View (下拉刷新的圆圈)。可以看出使用背景圆圈是 v4 包里提供的 CircleImageView 控件,中间的是 MaterialProgressDrawable 进度条。 另一个 View 是在 xml 中包含的可滚动视图。
2.onMeasure:
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
//初始化 mTarget 即内容部分
ensureTarget();
}
if (mTarget == null) {
return;
}
//测量mTarget
mTarget.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
//测量 圈圈 mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));
mCircleViewIndex = -1;
// Get the index of the circleview.计算 圈圈 在ViewGroup中的索引,给mCircleViewIndex赋值
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
这一步骤,完成了所有View 子View的测量过程 , 同时 初始化了mCircleViewIndex,为后面通过该值拿到圈圈 做准备
3.onLayout
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//onMeasure 完成,获得测量得到的 宽和高
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
//去除SwipeRefreshLayout的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();
//layout mTarget的位置
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
//layout圈圈 的位置,其中mCurrentTargetOffsetTop 决定了 top(和bottom)的位置
/*
构造方法里的初值是如下:即在屏幕上方(不可见),直径的距离
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
*/
mCircleView.layout(
(width / 2 - circleWidth / 2),
mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2),
mCurrentTargetOffsetTop + circleHeight);
}
circle_position.png
在onLayout中放置了mCircleView(圈圈)的位置,注意,其top(顶部)位置是mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;,而-mCirDiameter是圆的直径取负数,是在屏幕上方不可见的
到此,经过onMeasure和onLayout,Swipelayout及其子View已经定他们的大小、位置,都在屏幕上正确展示了.
3. 处理与子视图的滚动交互
这一部分,才是重头戏,是重中之重!!!
下拉刷新控件的主要功能是档子视图下拉到最顶部是,继续下拉可以出现刷新动画.而子视图本身如果可以滚动的话,所有滚动事件又要都交给子视图. 借助Android提供的nestredScrolling机制,是的SwipeRefreshlayout很轻松的解决了与子视图的滚动冲突问题,SwipeRefreshLayout通过实现NestedScrollingParent和NestedScrollingChild接口来处理滚动冲突.
SwipeRefreshLayout作为Parent嵌套一个可以滚动的子视图,那么就需要了解一下nestedScrollingParent接口
我们先贴出注释文档和翻译:
/**
* This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses
* that wish to support scrolling operations delegated by a nested child view.
当你的ViewGroup希望支持由嵌套子视图委托的滚动操作,你就要实现这个接口
*
* <p>Classes implementing this interface should create a final instance of a
* {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods
* to the <code>NestedScrollingParentHelper</code> methods of the same signature.</p>
实现这个接口的类,要创建一个final的NestedScrollingParentHelper变量,并把View or ViewGroup的方法都委托给Helper中的相同参数的方法
*
* <p>Views invoking nested scrolling functionality should always do so from the relevant
* {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility
* shim static methods. This ensures interoperability with nested scrolling views on Android
* 5.0 Lollipop and newer.</p>
调用嵌套滚动功能的视图应始终从相关的{@link ViewCompat},{@link ViewGroupCompat}或{@link ViewParentCompat}兼容静态方法执行此操作。
这确保了与Android 5.0 Lollipop和更新的嵌套滚动视图的互操作性。
*/
下面是源码:
public interface NestedScrollingParent {
/**
* 当子视图调用 ViewCompat.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 表示响应子视图的滚动。
*/
boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**
* 这一方法,在onStartNestedScroll() 返回true之后,被调用
* 作用是: 让这个支持嵌套滑动的ViewGroup和它的父类来进行初始化相关的配置
* 实现这个方法时,要调用其super方法,不可省略
*
* @param child 这个ViewGroup的直接子View,包括target
* @param target 启动嵌套滑动的那个子View
* @param axes 标志位,有如下两种情况:
* {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
* {@link ViewCompat#SCROLL_AXIS_VERTICAL} or
* both
* @see #onStartNestedScroll(View, View, int)
* @see #onStopNestedScroll(View)
*/
void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);
/**
* 当嵌套滑动结束时,调用此方法
*在嵌套滑动结束后,进行一下清理工作
* 例如: 当一个嵌套滑动被ACTION_UP 或者 ACTION_CANCEL给结束了,就调用
*此方法
*实现这个方法的时候,必须要调用其super方法,如果存在super对象的话
*
* @param target 启动嵌套滑动那个View
*/
void onStopNestedScroll(@NonNull View target);
/**
* React to a nested scroll in progress.
* 嵌套滑动进行中,调用此方法
*
* <p>This method will be called when the ViewParent's current nested scrolling child view
* dispatches a nested scroll event.
*当这个ViewParent的childView正在滑动时,调用此方法
*To receive calls to this method the ViewParent must have
* previously returned <code>true</code> for a call to
* {@link #onStartNestedScroll(View, View, int)}.</p>
*当然,这个方法被调用的前提是: onStartnestScroll(View, View, int) 里返回了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 where the drawer begins dragging once the edge of inner
* scrolling content is reached.</p>
* 例如: "未消耗的距离"可以用来允许连续拖动多个滚动或可拖动元素,比如 在一个垂直抽屉中有个滚动列表,
* 在内部滚动内容达到边缘到达时,抽屉开始拖动。(译者注: 就行BottomNestScrollDialog那种)
*
* @param target The descendent view controlling the nested scroll
* 这个嵌套滑动的发起者 即 内部的可滚动的子View
* @param dxConsumed Horizontal scroll distance in pixels already consumed by target
* 消耗的 x方向滚动
* @param dyConsumed Vertical scroll distance in pixels already consumed by target
* 未消耗的 x方向滚动
* @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target
* 消耗的 y方向滚动
* @param dyUnconsumed Vertical scroll distance in pixels not consumed by target
* 未消耗的y方向滚动
*/
void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
/**
* React to a nested scroll in progress before the target view consumes a portion of the scroll.
* 当嵌套滑动进行之中,target消耗部分滑动之前,此方法被调用(译者注: 即滑动的那个子View,消耗滑动距离之前)
*
* <p>When working with nested scrolling often the parent view may want an opportunity
* to consume the scroll before the nested scrolling child does. An example of this is a
* drawer that contains a scrollable list. The user will want to be able to scroll the list
* fully into view before the list itself begins scrolling.</p>
* 在嵌套滑动中: paterntView 可能想要有机会在childView(target)消耗滑动之前,自己先消耗滑动,比如:
* 这儿有个抽屉,抽屉含有一个可滚动list,用户可能希望在滑动是,先把抽屉完全全滑开之后,这个list再开始滚动,
* 这个时候,就是这个方法派上用场的时候
*
* <p><code>onNestedPreScroll</code> is called when a nested scrolling child invokes
* {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should
* report how any pixels of the scroll reported by dx, dy were consumed in the
* <code>consumed</code> array. Index 0 corresponds to dx and index 1 corresponds to dy.
* This parameter will never be null. Initial values for consumed[0] and consumed[1]
* will always be 0.</p>
*
* 当嵌套滑动的target(即childView) 调用disptchNestedPreScroll(int, int, int[], int[])方法是,这个方法被调用
* 实现这个方法时,应该上报:自己消耗了多少像素的滑动距离,通过把小号的值放在int[] consumed里面.
* index 0 : x方向消耗的滑动距离
* index 1 : y方向小号的滑动距离
* 这个数组从 不会为null 初始值是[0,0]
*
* @param target View that initiated the nested scroll
* 触发嵌套滑动的子View
* @param dx Horizontal scroll distance in pixels
* x方向滑动距离
* @param dy Vertical scroll distance in pixels
* y方向滑动距离
* @param consumed Output. The horizontal and vertical scroll distance consumed by this parent
* 我们要返回的数据,里面包含了,这个viewParent 消耗掉的 x和y方向的滑动距离
*/
void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);
/**
* Request a fling from a nested scroll.
* 在嵌套滑动中 请求消耗fling(手指抛滑)的动作
*
* <p>This method signifies that a nested scrolling child has detected suitable conditions
* for a fling. Generally this means that a touch scroll has ended with a
* {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
* the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
* along a scrollable axis.</p>
* 这个方法表示: 嵌套滑动的childView(译者注:target) 检测到了一个fling动作,通常这意味着:
* 在滑动方向上达到或超过了最小的FlingVelocity(判断是否fling的速度阈值)
*
* <p>If a nested scrolling child view would normally fling but it is at the edge of
* its own content, it can use this method to delegate the fling to its nested scrolling
* parent instead. The parent may optionally consume the fling or observe a child fling.</p>
*如果一个嵌套滑动的childView(target) 想要正常的进行fling滑动,但是它(target)已经达到了自己的内容边缘
*那么可以用此方法,把fling滑动委托给parentView,parentView就会有选择的消耗fling操作,或者就静静看着childView的fling滑动
*
* @param target View that initiated the nested scroll
* @param velocityX Horizontal velocity in pixels per second
* x方向滑动速度 像素/s
* @param velocityY Vertical velocity in pixels per second
y方向滑动速度 像素/s
* @param consumed true if the child consumed the fling, false otherwise
* true -> target 消耗了fling滑动 false -> 没消耗
* @return true if this parent consumed or otherwise reacted to the fling
* 返回true: parent将要消耗fling
* false: 不消耗
*/
boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);
/**
* React to a nested fling before the target view consumes it.
* 在target 对消耗掉fling滑动之前,回调到此方法
*
* <p>This method siginfies that a nested scrolling child has detected a fling with the given
* velocity along each axis. Generally this means that a touch scroll has ended with a
* {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds
* the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity}
* along a scrollable axis.</p>
* 这个方法表示:
* 嵌套滑动的childView(target)检测到了一个fling滑动(速度为: velocityX,velocityY),
*
* <p>If a nested scrolling parent is consuming motion as part of a
* {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for
* it to also consume the pre-fling to complete that same motion. By returning
* <code>true</code> from this method, the parent indicates that the child should not
* fling its own internal content as well.</p>
* 如果如果前面 onNestedPreScroll 中父布局消耗了事件,那么这个方法也会回调
* 返回ture: paentView表明:childView(target) 不应该fling它自己(即target自己)的内部内容
*
* @param target View that initiated the nested scroll
* @param velocityX Horizontal velocity in pixels per second
* @param velocityY Vertical velocity in pixels per second
* @return true if this parent consumed the fling ahead of the target view
* return true 表明parent在其child(target)之前,消耗了fling动作
*/
boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);
/**
* Return the current axes of nested scrolling for this NestedScrollingParent.
* 返回当前 嵌套滚动,沿哪个坐标轴进行
*
* <p>A NestedScrollingParent returning something other than {@link ViewCompat#SCROLL_AXIS_NONE}
* is currently acting as a nested scrolling parent for one or more descendant views in
* the hierarchy.</p>
* 么看懂,好像没什么用
*
* @return Flags indicating the current axes of nested scrolling
* @see ViewCompat#SCROLL_AXIS_HORIZONTAL
* @see ViewCompat#SCROLL_AXIS_VERTICAL
* @see ViewCompat#SCROLL_AXIS_NONE
*/
@ScrollAxis
int getNestedScrollAxes();
}
上面是对NestScrollingParent的翻译,下面看一下SwipeRefreshlayout是如何实现这些方法的:
// NestedScrollingParent
// 子 View (target)开始滚动前回调此方法,返回 true 表示接 Parent 收嵌套滚动,然后调用 onNestedScrollAccepted
// 具体可以看 NestedScrollingChildHelper 的源码
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
// 子 View 回调,判断是否开始嵌套滚动 ,
return isEnabled() && !mReturningToStart && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
// Reset the counter of how much leftover scroll needs to be consumed.
mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);
// ...省略代码
}
对onStartNestedScroll(View child, View target, int nestedScrollAxes) 代码做解释说明:
return isEnabled() && !mReturningToStart && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
什么时候会返回true?
- 条件1: view是enable的
- 条件2: target不能正在返回初始位置
什么时候会返回初始位置: 刷新被取消或者刷新已经开始 - 条件3: 没有正在刷新
- 条件4: 是竖直方向的嵌套滚动
只有上面4项同时满足时,swipeRefresh才响应嵌套滑动,即才能下拉刷新
SwipeRefreshLayout 只接受竖直方向(Y轴)的滚动,并且在刷新动画进行中不接受滚动。
接下来看真正滚动的代码:onNestedPreScroll和onNestedScroll
(PS: 按照正常人的思维逻辑,建议先关注onNestedScroll里的部分,因为此处是处理下拉,显示圈圈的过程)
// 子View(target) 在滚动的时候会触发, 看父类消耗了多少距离
// * @param dx x 轴滚动的距离
// * @param dy y 轴滚动的距离
// * @param consumed 代表 父 View 消费的滚动距离
// * index0: x方向
// * index1: y方向
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// dy > 0 表示手指在屏幕向上移动
// mTotalUnconsumed 表示子视图Y轴未消费的距离
// 发生在 : 已经下拉了部分,此时不抬手,手指下向上滑动,想让圈圈回去,会触发此处代码
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
// SwipeRefreshLayout 就吧子视图位消费的距离全部消费
了。
//这里mTotalUnConsumed是已经向下滑动的距离,当向上滑动的dy大于已经向下移动的距离,
//多出的部分(即: 二者之差),也要消费掉,因此,在consumed[1] 里返回了这个差
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy; // 消费的 y 轴的距离
consumed[1] = dy;
}
// 出现动画圆圈,并向上移动
moveSpinner(mTotalUnconsumed);
}
// ... 省略代码
}
// onStartNestedScroll 返回 true 才会调用此方法。
// 此方法表示子View将滚动事件分发到父 View(SwipeRefreshLayout)
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
// ... 省略代码
// 手指在屏幕上向下滚动,并且子视图不可以滚动(即子视图内容不能再向下移动)
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed);
}
}
SwipeRefreshLayout通过NestedScrollingParent接口完成了处理子视图嵌套滑动(同方向滑动冲突) 的问题,中间省略了一些处理: SwipeRefreshlayout作为child在其他支持嵌套滑动的父容器中的代码,这里不做深究
但是,下拉刷新需要根据手指在屏幕的状态(DOWN/MOVE/UP)来进行刷新动画,所以我们还要处理触摸事件:
首先是onInterceptTuchEvent,返回true,表示拦截事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//去报 target 不为 null
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
//手指按下时 判断mReturningToStart,即,判断是否圈圈在返回top,(即是否下拉一半,取消,或者是否完整下拉已经触发refresh)
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
//以下几个状态,立刻返回false 不拦截
//1.eanble=false 2.圈圈在返回顶部 3.target仍然能上滑(手指上-下) 4. 正在刷新 5. 正在嵌套滑动(正在嵌套滑动是为什么不拦截: 因为拦截了,target就不能滑动了)
if (!isEnabled() || mReturningToStart || canChildScrollUp()
|| mRefreshing || mNestedScrollInProgress) {
// Fail fast if we're not in a state where a swipe is possible
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
/*
由于pointer的index值在不同的MotionEvent对象中会发生变化,但是id值却不会变化。所以,当我们要记录一个触摸点的事件流时,就只需要保存其id,然后使用findPointerIndex(int)来获得其index值,然后再获得其他信息。
*/
//将第一根手指,视为activie的手指 这里处理了多指下拉的情况
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
// 记录手指按下的位置,用于判断 滑动距离
mInitialDownY = ev.getY(pointerIndex);
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return false;
}
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
return false;
}
final float y = ev.getY(pointerIndex);
startDragging(y);
break;
/*
插入startDragging(y);便于理解
判断当拖动距离大于最小距离时设置 mIsBeingDragged = true;
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
// 正在拖动状态,更新圆圈的 progressbar 的 alpha 值
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
*/
case MotionEvent.ACTION_POINTER_UP:
//多指下拉,此时抬起一根手指
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
//可以看到,除了直接返回false的情况,
//其余都是返回的mIsBeingDraged 这个标志位只有在MOVE时是true,即只在startDragging(float y)里是true 也即只有在拖动时才是true,即只有在拖动时才拦截
return mIsBeingDragged;
}
可以看到源码也就是进行简单处理,DOWN 的时候记录一下位置,MOVE 时判断移动的距离,返回值 mIsBeingDragged 为 true 时, 即 onInterceptTouchEvent 返回true,SwipeRefreshLayout 拦截触摸事件,不分发给 mTarget,然后把 MotionEvent 传给 onTouchEvent 方法。其中有一个判断子View的是否还可以滚动的方法 canChildScrollUp。
ps: 该方法是判断target 能否继续向上滑动(字面意思),即能否接受手指从上向下
/**
* @return Whether it is possible for the child view of this layout to
* scroll up. Override this if the child view is a custom view.
*/
public boolean canChildScrollUp() {
if (mChildScrollUpCallback != null) {
return mChildScrollUpCallback.canChildScrollUp(this, mTarget);
}
if (mTarget instanceof ListView) {
return ListViewCompat.canScrollList((ListView) mTarget, -1);
}
return mTarget.canScrollVertically(-1);
}
插入ViewCompat.canScrollList( ) 方法:
public static boolean canScrollList(@NonNull ListView listView, int direction) {
if (Build.VERSION.SDK_INT >= 19) {
// Call the framework version directly
return listView.canScrollList(direction);
} else {
// provide backport on earlier versions
final int childCount = listView.getChildCount();
if (childCount == 0) {
return false;
}
final int firstPosition = listView.getFirstVisiblePosition();
if (direction > 0) {
final int lastBottom = listView.getChildAt(childCount - 1).getBottom();
final int lastPosition = firstPosition + childCount;
return lastPosition < listView.getCount()
|| (lastBottom > listView.getHeight() - listView.getListPaddingBottom());
} else {
final int firstTop = listView.getChildAt(0).getTop();
return firstPosition > 0 || firstTop < listView.getListPaddingTop();
}
}
}
/**
* Check if this view can be scrolled vertically in a certain direction.
负数: 顶部是否可以往下滚动
正数: 底部是否可以往上滚动
*
* @param direction Negative to check scrolling up, positive to check scrolling down.
* @return true if this view can be scrolled in the specified direction, false otherwise.
*/
public boolean canScrollVertically(int direction) {
final int offset = computeVerticalScrollOffset();
final int range = computeVerticalScrollRange() - computeVerticalScrollExtent();
if (range == 0) return false;
if (direction < 0) {
return offset > 0;
} else {
return offset < range - 1;
}
}
当SwipeRefreshLayout 拦截了触摸事件之后( mIsBeingDragged 为 true ),将 MotionEvent 交给 onTouchEvent 处理。
@Override
public boolean onTouchEvent(MotionEvent ev) {
// ... 省略代码
switch (action) {
case MotionEvent.ACTION_DOWN:
// 获取第一个按下的手指
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
// 处理多指触控
pointerIndex = ev.findPointerIndex(mActivePointerId);
// ... 省略代码
final float y = ev.getY(pointerIndex);
startDragging(y);
//计算拖动开始 的位置: mInitialMotionY,变更mIsBeingDragged(是否正在拖动的状态)标志位
/*
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
*/
//移动 圈圈
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}//action move end
//处理多点 触控,变更mActivePointerId
case MotionEvent.ACTION_POINTER_DOWN: {
pointerIndex = ev.getActionIndex();
if (pointerIndex < 0) {
Log.e(LOG_TAG,
"Got ACTION_POINTER_DOWN event but have an invalid action index.");
return false;
}
mActivePointerId = ev.getPointerId(pointerIndex);
break;
}
//多点触控 手指 数量 变更,如果mActivePointer抬起,更新mActivePointerId,同时调整位置
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
//抬起手指
case MotionEvent.ACTION_UP: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id.");
return false;
}
if (mIsBeingDragged) {
final float y = ev.getY(pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
/*
finshSpinner(overScrollTop)
通过滑动距离,决定是刷新 or 取消刷新
*/
}
mActivePointerId = INVALID_POINTER;
return false;
}//action Up end
case MotionEvent.ACTION_CANCEL:
return false;
return true;
}
在手指滚动过程中通过判断 mIsBeingDragged 来移动刷新的圆圈(对应的是 moveSpinner ),手指松开将圆圈移动到正确位置(初始位置或者刷新动画的位置,对应的是 finishSpinner 方法)。
// 手指下拉过程中触发的圆圈的变化过程,透明度变化,渐渐出现箭头,大小的变化
private void moveSpinner(float overscrollTop) {
// 设置为有箭头的 progress
mProgress.setArrowEnabled(true);
// 进度转化成百分比
float originalDragPercent = overscrollTop / mTotalDragDistance;
// 避免百分比超过 100%
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
// 调整拖动百分比,造成视差效果
float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
//
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
// 这里mUsingCustomStart 为 true 代表用户自定义了起始出现的坐标
float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset - mOriginalOffsetTop
: mSpinnerFinalOffset;
// 弹性系数
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
/ slingshotDist);
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
// 因为有弹性系数,不同的手指滚动距离不同于view的移动距离
int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
// where 1.0f is a full circle
if (mCircleView.getVisibility() != View.VISIBLE) {
mCircleView.setVisibility(View.VISIBLE);
}
// 设置的是否有缩放
if (!mScale) {
ViewCompat.setScaleX(mCircleView, 1f);
ViewCompat.setScaleY(mCircleView, 1f);
}
// 设置缩放进度
if (mScale) {
setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));
}
// 移动距离未达到最大距离
if (overscrollTop < mTotalDragDistance) {
if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA
&& !isAnimationRunning(mAlphaStartAnimation)) {
// Animate the alpha
startProgressAlphaStartAnimation();
}
} else {
if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {
// Animate the alpha
startProgressAlphaMaxAnimation();
}
}
// 出现的进度,裁剪 mProgress
float strokeStart = adjustedPercent * .8f;
mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));
mProgress.setArrowScale(Math.min(1f, adjustedPercent));
// 旋转
float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;
mProgress.setProgressRotation(rotation);
// 调整 圈圈 位置
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, true /* requires update */);
}
刷新圆圈的移动过程也是有好几种状态,看上面的注释基本上就比较清楚了。
下面看finishSpinner(float overscrollTop)的代码:
通过滑动距离,决定是刷新 or 取消刷新
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {//触发刷新
//移动距离超过了刷新的临界值,触发刷新动画
setRefreshing(true, true /* notify */);
/*
//变更圈圈位置,开始刷新
private void setRefreshing(boolean refreshing, final boolean notify) {
//如果之前刷新状态,与传入刷新状态不一致,则触发下面逻辑
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
startScaleDownAnimation(mRefreshListener);
}
}
}
*/
} else {//取消刷新
// cancel refresh
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
Animation.AnimationListener listener = null;
if (!mScale) {
listener = new Animation.AnimationListener() {
...
@Override
public void onAnimationEnd(Animation animation) {
if (!mScale) {
startScaleDownAnimation(null);
}
}
...
};
}
//恢复圈圈到初始位置
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
//设置没有箭头
mProgress.setArrowEnabled(false);
/*
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
if (mScale) {
// Scale the item back down
startScaleDownReturnToStartAnimation(from, listener);
} else {
mFrom = from;
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToStartPosition);
}
}
*/
}//取消刷新 end
}
setRefreshing(...) 说明
可以看到调用 setRefresh(true,true) 方法触发刷新动画并进行回调,但是这个方法是 private 的。前面提到我们自己调用 setRefresh(true) 只能产生动画,而不能回调刷新函数,那么我们就可以用反射调用 2 个参数的 setRefresh 函数。 或者手动调 setRefreshing(true)+ OnRefreshListener.onRefresh 方法。
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {//如果之前刷新状态,与传入刷新状态不一致,则触发下面逻辑
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {//开始刷新
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {//取消刷新
//缩小圈圈
startScaleDownAnimation(mRefreshListener);
}
}
}
一个参数的setOnRefreshing(boolean)
/**
* Notify the widget that refresh state has changed. Do not call this when
* refresh is triggered by a swipe gesture.
* 通知空间:刷新状态已经更改
* 不要再手势触发刷新时,调用此方法
*
* @param refreshing Whether or not the view should show refresh progress.
* 是否要显示 刷新的progress
*/
public void setRefreshing(boolean refreshing) {
if (refreshing && mRefreshing != refreshing) {
// scale and show
mRefreshing = refreshing;
int endTarget = 0;
if (!mUsingCustomStart) {
endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
} else {
endTarget = mSpinnerOffsetEnd;
}
setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
mNotify = false;
startScaleUpAnimation(mRefreshListener);
} else {
setRefreshing(refreshing, false /* notify */);
}
}
关注上面的mNotify = false,这个标志位,决定了在mRefreshListener(动画回调)中,是否通知SwipeRefreshListenr
if (mNotify) {
if (mListener != null) {
mListener.onRefresh();
}
}
这里是false,因此单纯的调用setRefresh(boolean refreshing),只能产生动画,而不能回调刷新函数,这一点,要切记切记
animateOffsetToCorrectPosition
开启一个动画,然后在动画结束后回调 onRefresh 方法。,其中的AnimatoionListener 就是mRefreshingListern,在动画结束时(onAnimationEnd) 调用了mListener.onRefresh
private void animateOffsetToCorrectPosition(int from, AnimationListener listener) {
mFrom = from;
mAnimateToCorrectPosition.reset();
mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);
mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToCorrectPosition);
}
mRefreshListener
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
//...
@Override
public void onAnimationEnd(Animation animation) {
if (mRefreshing) {
// Make sure the progress view is fully visible
mProgress.setAlpha(MAX_ALPHA);
mProgress.start();
if (mNotify) {
if (mListener != null) {
mListener.onRefresh();
}
}
mCurrentTargetOffsetTop = mCircleView.getTop();
} else {
reset();
}
}
};
总结:
分析 SwipeRefreshLayout 的流程就是按照平时我们自定义 ViewGroup 的流程,但是其中也有好多需要我们借鉴的地方,如何使用 NestedScrolling相关机制 ,多点触控的处理,onMeasure 中减去了 padding,如何判断子 View 是否可滚动,如何确定 ViewGroup 中某一个 View 的索引等。 此外,一个好的下拉刷新框架不仅仅要兼容各种滚动的子控件,还要考虑自己要兼容 NestedScrollingChild 的情况,比如放到 CooCoordinatorLayout 的情况,目前大多数开源的下拉刷新好像都没有达到这个要求,一般都是只考虑了内部嵌套滚动子视图的情况,没有考虑自己作为滚动子视图的情况。













网友评论