Read the fuck source code!!!
一直说分析这个ViewGroup,但是都被各种事打断了。最近几天好像没有别的事情打扰,所以还是有一点进度的。加了注释版本的SwipeRefreshLayout记在这里,希望对大家有帮助。插一句:如果情况好的话,我应该还是会另外写一篇文章,详细介绍这个ViewGroup。
Talk is cheap,show you the code:
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 android.support.v4.widget;
import android.content.Context;
import android.content.res.TypedArray;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.NestedScrollingChild;
import android.support.v4.view.NestedScrollingChildHelper;
import android.support.v4.view.NestedScrollingParent;
import android.support.v4.view.NestedScrollingParentHelper;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Transformation;
import android.widget.AbsListView;
import android.widget.ListView;
/**
* The SwipeRefreshLayout should be used whenever the user can refresh the
* contents of a view via a vertical swipe gesture. SwipeRefreshLayout应该被使用,每当用户可以通过垂直滑动手势来刷新视图的内容时。
* The activity that instantiates this view should add an OnRefreshListener to be notified
* whenever the swipe to refresh gesture is completed. 实例化了这个View的Activity应该添加一个OnRefreshListener来被通知这个刷新手势完成了。
* The SwipeRefreshLayout will notify the listener each and every time the gesture is completed again;
* SwipeRefreshLayout将会在每次手势再次完成时通知监听者;
* the listener is responsible for correctly determining when to actually initiate a refresh of its content.
* 监听者负责正确地确定何时真正启动其内容的刷新。
* If the listener determines there should not be a refresh, it must call setRefreshing(false) to cancel any visual indication of a refresh.
* 如果侦听器确定不应该进行刷新,则必须调用setRefreshing(false)来取消刷新的任何可视指示。
* If an activity wishes to show just the progress animation, it should call setRefreshing(true).
* 如果Activity想要仅仅显示进度动画,它应该调用setRefreshing(true)。
* To disable the gesture and progress animation, call setEnabled(false) on the view.
* 为了除能这个手势和进度动画,在这个View上调用setEnable(false)方法。
* <p>
* This layout should be made the parent of the view that will be refreshed as a
* result of the gesture and can only support one direct child.
* 这个布局应该成为由于这个手势将会被刷新的View的父对象,并且仅仅支持一个直接的子对象。
* This view will also be made the target of the gesture and will be forced to match both the
* width and the height supplied in this layout.
* 这个View将也成为手势的目标,并且将会被强制匹配这个布局提供的宽和高。
* The SwipeRefreshLayout does not provide accessibility events; instead, a menu item must be provided to allow
* refresh of the content wherever this gesture is used.
* SwipeRefreshLayout不提供可访问性事件; 相反,必须提供菜单项以允许在使用此手势的位置刷新内容。
* </p>
*/
public class SwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,
NestedScrollingChild {
// Maps to ProgressBar.Large style
public static final int LARGE = CircularProgressDrawable.LARGE;
// Maps to ProgressBar default style
public static final int DEFAULT = CircularProgressDrawable.DEFAULT;
@VisibleForTesting
static final int CIRCLE_DIAMETER = 40;
@VisibleForTesting
static final int CIRCLE_DIAMETER_LARGE = 56;
private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName();
private static final int MAX_ALPHA = 255;
private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA);
private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;
private static final int INVALID_POINTER = -1;
private static final float DRAG_RATE = .5f;
// Max amount of circle that can be filled by progress during swipe gesture,
// where 1.0 is a full circle
private static final float MAX_PROGRESS_ANGLE = .8f;
private static final int SCALE_DOWN_DURATION = 150;
private static final int ALPHA_ANIMATION_DURATION = 300;
private static final int ANIMATE_TO_TRIGGER_DURATION = 200;
private static final int ANIMATE_TO_START_DURATION = 200;
// Default background for the progress spinner
private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA;
// Default offset in dips from the top of the view to where the progress spinner should stop
private static final int DEFAULT_CIRCLE_TARGET = 64;
private View mTarget; // the target of the gesture
OnRefreshListener mListener;
boolean mRefreshing = false;
private int mTouchSlop;
private float mTotalDragDistance = -1;
// If nested scrolling is enabled, the total amount that needed to be
// consumed by this as the nested scrolling parent is used in place of the
// overscroll determined by MOVE events in the onTouch handler
// 如果启用嵌套滚动,则使用嵌套滚动父级所需的总数量来代替onTouch处理程序中MOVE事件所确定的超滚动量
private float mTotalUnconsumed;
private final NestedScrollingParentHelper mNestedScrollingParentHelper;
private final NestedScrollingChildHelper mNestedScrollingChildHelper;
private final int[] mParentScrollConsumed = new int[2];
private final int[] mParentOffsetInWindow = new int[2];
private boolean mNestedScrollInProgress;
private int mMediumAnimationDuration;
/**这个值初始化时,将会和mOriginalOffsetTop一样为负的mCricleView的直径*/
int mCurrentTargetOffsetTop;
private float mInitialMotionY;
//最开始按下的位置,在onInterceptTouchEvent()方法中进行了记录。这个值的作用是记录下手指拽动的偏移。
private float mInitialDownY;
private boolean mIsBeingDragged;//是否开始拽动
private int mActivePointerId = INVALID_POINTER;
// Whether this item is scaled up rather than clipped
boolean mScale;
// Target is returning to its start offset because it was cancelled or a
// refresh was triggered.
// 目标正在回复到它的初始位置,因为它被取消或者一个新的刷新被触发了
// 这个变量为true,表示这个意思:Target正在返回到其起始偏移量,因为它已被取消或触发了刷新。
private boolean mReturningToStart;
private final DecelerateInterpolator mDecelerateInterpolator;
private static final int[] LAYOUT_ATTRS = new int[] {
android.R.attr.enabled
};
CircleImageView mCircleView;
private int mCircleViewIndex = -1;
protected int mFrom;
float mStartingScale;
/**这个值会被初始化为负的mCricleView的直径*/
protected int mOriginalOffsetTop;
//设置滑动指示器滑动结束时的位移mSpinnerOffsetEnd为64dp,同样mTotalDragDistance也设置为64dp
int mSpinnerOffsetEnd;
CircularProgressDrawable mProgress;
private Animation mScaleAnimation;
private Animation mScaleDownAnimation;
private Animation mAlphaStartAnimation;
private Animation mAlphaMaxAnimation;
private Animation mScaleDownToStartAnimation;
boolean mNotify;
private int mCircleDiameter;
// Whether the client has set a custom starting position;
//表明客户设置了一个自定义的开始位置
boolean mUsingCustomStart;
private OnChildScrollUpCallback mChildScrollUpCallback;
//进度条动画控制Listener
private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@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 {
//没有就重置让mCricleView回归原来的位置
reset();
}
}
};
/**这个方法的功能就是重置动画(包括停止:mCircleView、mProgress的动画效果;让mCircleView变得不可见,并回归原来的位置:如果开启了缩放动画,将会让mCircleView的x,y缩放到最小,即不可见;)*/
void reset() {
mCircleView.clearAnimation();
mProgress.stop();
mCircleView.setVisibility(View.GONE);
setColorViewAlpha(MAX_ALPHA);
// Return the circle to its start position
if (mScale) {
//发生了放缩
//这样设置将会让mCircleView缩放大小为0,即mCircleView将会不可见
setAnimationProgress(0 /* animation complete and view is hidden */);
} else {
//如果没有发生缩放,将会将mCricleView提到前面,同时发生mOriginalOffsetTop - mCurrentTargetOffsetTop大小的位移
//位移之后,将mCricle当前的top值赋值给mCurrentTargetOffsetTop
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop);
}
//将mCricle当前的top值赋值给mCurrentTargetOffsetTop
mCurrentTargetOffsetTop = mCircleView.getTop();
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (!enabled) {
//不使能就复位
reset();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
reset();
}
//设置透明度
private void setColorViewAlpha(int targetAlpha) {
mCircleView.getBackground().setAlpha(targetAlpha);
mProgress.setAlpha(targetAlpha);
}
/**
* The refresh indicator starting and resting position is always positioned
* near the top of the refreshing content. This position is a consistent
* location, but can be adjusted in either direction based on whether or not
* there is a toolbar or actionbar present.
* 刷新指示符的开始和结束位置总是位于刷新内容的顶部附近。此位置是一个固定的位置,但可以根据是否存在toolbar和actionbar进行调整。调用这个方法会让刷新指示器重置
* <p>
* <strong>Note:</strong> Calling this will reset the position of the refresh indicator to
* <code>start</code>.
* </p>
*
* @param scale Set to true if there is no view at a higher z-order than where the progress
* spinner is set to appear. Setting it to true will cause indicator to be scaled
* up rather than clipped.
* @param start The offset in pixels from the top of this view at which the
* progress spinner should appear.
* @param end The offset in pixels from the top of this view at which the
* progress spinner should come to rest after a successful swipe
* gesture.
*/
public void setProgressViewOffset(boolean scale, int start, int end) {
mScale = scale;
mOriginalOffsetTop = start;
mSpinnerOffsetEnd = end;
mUsingCustomStart = true;
reset();
mRefreshing = false;
}
/**
* @return The offset in pixels from the top of this view at which the progress spinner should
* appear.
*/
public int getProgressViewStartOffset() {
return mOriginalOffsetTop;
}
/**
* @return The offset in pixels from the top of this view at which the progress spinner should
* come to rest after a successful swipe gesture.
*/
public int getProgressViewEndOffset() {
return mSpinnerOffsetEnd;
}
/**
* The refresh indicator resting position is always positioned near the top
* of the refreshing content. This position is a consistent location, but
* can be adjusted in either direction based on whether or not there is a
* toolbar or actionbar present.
*
* @param scale Set to true if there is no view at a higher z-order than where the progress
* spinner is set to appear. Setting it to true will cause indicator to be scaled
* up rather than clipped.
* @param end The offset in pixels from the top of this view at which the
* progress spinner should come to rest after a successful swipe
* gesture.
*/
public void setProgressViewEndTarget(boolean scale, int end) {
mSpinnerOffsetEnd = end;
mScale = scale;
mCircleView.invalidate();
}
/**
* One of DEFAULT, or LARGE.
* 设置刷新View的大小,二选一:默认和大
*/
public void setSize(int size) {
if (size != CircularProgressDrawable.LARGE && size != CircularProgressDrawable.DEFAULT) {
return;
}
final DisplayMetrics metrics = getResources().getDisplayMetrics();
if (size == CircularProgressDrawable.LARGE) {
mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density);
} else {
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
}
// force the bounds of the progress circle inside the circle view to
// update by setting it to null before updating its size and then
// re-setting it
mCircleView.setImageDrawable(null);
mProgress.setStyle(size);
mCircleView.setImageDrawable(mProgress);
}
/**
* Simple constructor to use when creating a SwipeRefreshLayout from code.
*
* @param context
*/
public SwipeRefreshLayout(Context context) {
this(context, null);
}
/**
* Constructor that is called when inflating SwipeRefreshLayout from XML.
* 构造方法:初始化了缩放触角mTouchSlop(ScaledTouchSlop这个数值是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件),
* mMediumAnimationDuration中等动画持续时间,mCricleView的直径mCircleDiameter,调用createProgressView创建mCricleView
*
* @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);
//ViewGroup默认情况下,出于性能考虑,会被设置成WILL_NOT_DROW,这样,ondraw就不会被执行了。
setWillNotDraw(false);
//DecelerateInterpolator参数:动画应该放松的程度。 将系数设置为1.0f会产生颠倒的y = x ^ 2抛物线。 增加1.0f以上的因子会夸大缓解效应(即开始更快,结束更慢)
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
createProgressView();
//设置当前Viewgroup添加childView的顺序由getChildDrawingOrder方法决定。
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
// the absolute offset has to take into account that the circle starts at an offset
// 设置滑动指示器滑动结束时的位移mSpinnerOffsetEnd为64dp,同样mTotalDragDistance也设置为64dp
mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
mTotalDragDistance = mSpinnerOffsetEnd;
//创建parentHelper
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
//创建childHelper
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
//设置初始偏移顶部mOriginalOffsetTop和当前偏移顶部mCurrentTargetOffsetTop为负的mCricleView的直径大小
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
moveToStart(1.0f);
//根据xml中的设置,控制是否使能
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
/**获取childView的绘制顺序*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
//如果mCircleViewIndex小于0,则直接返回i
if (mCircleViewIndex < 0) {
return i;
} else if (i == childCount - 1) {
// Draw the selected child last
return mCircleViewIndex;
} else if (i >= mCircleViewIndex) {
// Move the children after the selected child earlier one
return i + 1;
} else {
// Keep the children before the selected child the same
return i;
}
}
//创建mCricleView,创建之后,自动添加到ViewGroup
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);
}
/**
* Set the listener to be notified when a refresh is triggered via the swipe
* gesture.
*/
public void setOnRefreshListener(OnRefreshListener listener) {
mListener = listener;
}
/**
* 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.
*/
public void setRefreshing(boolean refreshing) {
if (refreshing && mRefreshing != refreshing) {
//判断是不是需要刷新,并且当前状态为非刷新状态时,开始刷新
// scale and show
// 设置为刷新状态
mRefreshing = refreshing;
int endTarget = 0;
if (!mUsingCustomStart) {
//如果用户没有设置个性化的起始位置,endTarget设置为mSpinnerOffsetEnd + mOriginalOffsetTop
//在构造函数中,mOriginalOffsetTop被设置为负的mCricleView的直径
//在构造函数中,滑动指示器滑动结束时的位移mSpinnerOffsetEnd被设置为64dp,同样mTotalDragDistance也设置为64dp
endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;
} else {
endTarget = mSpinnerOffsetEnd;
}
/**
* 这将会让mCricleView提到前台
* 同时将mCricleView进行offset大小的竖直位移
* 将mCricle当前的top值赋值给mCurrentTargetOffsetTop
*/
setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);
mNotify = false;
//开始放大动画,因为当前mRefreshing为true,但mNotifu为false,所以在动画结束时,在mRefreshListener动画结束回调中,不会调用mListener的onRefresh方法
startScaleUpAnimation(mRefreshListener);
} else {
//设置刷新状态为false,同时不通知
setRefreshing(refreshing, false /* notify */);
}
}
/**放大动画,如果sdk版本大于等于11,设置透明度为最大 */
private void startScaleUpAnimation(AnimationListener listener) {
mCircleView.setVisibility(View.VISIBLE);
if (android.os.Build.VERSION.SDK_INT >= 11) {
// Pre API 11, alpha is used in place of scale up to show the
// progress circle appearing.
// Don't adjust the alpha during appearance otherwise.
mProgress.setAlpha(MAX_ALPHA);
}
mScaleAnimation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
//在这里设置mCricleView的缩放比例,即产生动画效果
setAnimationProgress(interpolatedTime);
}
};
mScaleAnimation.setDuration(mMediumAnimationDuration);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mScaleAnimation);
}
/**
* Pre API 11, this does an alpha animation.
* 在API11之前,这会产生一个alpha动画
* 设置mCricleView的比例,如果在API11之前,这将会产生一个alpha动画
* @param progress
*/
void setAnimationProgress(float progress) {
mCircleView.setScaleX(progress);
mCircleView.setScaleY(progress);
}
/**设置刷新
* @param refreshing 刷新状态
* @param notify 是否通知(如果为true,则会回调通知OnRefreshListener.onRefresh方法)
* 只有mRefreshing!=refreshing时才会执行内部代码
* 这个方法将会设置mNotify为nnotify,mRefreshing为refreshing
* 如果refreshing为true,则会调用animateOffsetToCorrectPosition,这个方法会将mCricleView移动到mCurrentTargetOffsetTop位置
* 反之,将会开始缩小动画
*/
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
startScaleDownAnimation(mRefreshListener);
}
}
}
/**缩小动画 */
void startScaleDownAnimation(Animation.AnimationListener listener) {
mScaleDownAnimation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
/**
* Pre API 11, this does an alpha animation.
* 在API11之前,这会产生一个alpha动画
* 设置mCricleView的比例,如果在API11之前,这将会产生一个alpha动画
* @param progress
*/
setAnimationProgress(1 - interpolatedTime);
}
};
mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);
mCircleView.setAnimationListener(listener);
mCircleView.clearAnimation();
mCircleView.startAnimation(mScaleDownAnimation);
}
private void startProgressAlphaStartAnimation() {
//STARTING_PROGRESS_ALPHA = .3f * 255(MAX_ALPHA)
mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA);
}
private void startProgressAlphaMaxAnimation() {
mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA);
}
/**根据开始和结束的alpha产生一个alpha动画,并让mCricleView开始动画*/
private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) {
Animation alpha = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
mProgress.setAlpha(
(int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime)));
}
};
//ALPHA_ANIMATION_DURATION为300ms
alpha.setDuration(ALPHA_ANIMATION_DURATION);
// Clear out the previous animation listeners.
mCircleView.setAnimationListener(null);
mCircleView.clearAnimation();
mCircleView.startAnimation(alpha);
return alpha;
}
/**
* @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)}
*/
@Deprecated
public void setProgressBackgroundColor(int colorRes) {
setProgressBackgroundColorSchemeResource(colorRes);
}
/**
* Set the background color of the progress spinner disc.
*
* @param colorRes Resource id of the color.
*/
public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) {
setProgressBackgroundColorSchemeColor(ContextCompat.getColor(getContext(), colorRes));
}
/**
* Set the background color of the progress spinner disc.
*
* @param color
*/
public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {
mCircleView.setBackgroundColor(color);
}
/**
* @deprecated Use {@link #setColorSchemeResources(int...)}
*/
@Deprecated
public void setColorScheme(@ColorRes int... colors) {
setColorSchemeResources(colors);
}
/**
* Set the color resources used in the progress animation from color resources.
* The first color will also be the color of the bar that grows in response
* to a user swipe gesture.
*
* @param colorResIds
*/
public void setColorSchemeResources(@ColorRes int... colorResIds) {
final Context context = getContext();
int[] colorRes = new int[colorResIds.length];
for (int i = 0; i < colorResIds.length; i++) {
colorRes[i] = ContextCompat.getColor(context, colorResIds[i]);
}
setColorSchemeColors(colorRes);
}
/**
* Set the colors used in the progress animation. The first
* color will also be the color of the bar that grows in response to a user
* swipe gesture.
*
* @param colors
*/
public void setColorSchemeColors(@ColorInt int... colors) {
ensureTarget();
mProgress.setColorSchemeColors(colors);
}
/**
* @return Whether the SwipeRefreshWidget is actively showing refresh
* progress.
*/
public boolean isRefreshing() {
return mRefreshing;
}
/**确保target存在,即要让mTarget不为空 */
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (mTarget == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(mCircleView)) {
mTarget = child;
break;
}
}
}
}
/**
* Set the distance to trigger a sync in dips
*
* @param distance
*/
public void setDistanceToTriggerSync(int distance) {
mTotalDragDistance = distance;
}
/**在这个方法中对mTarget和mCricleView进行了Layout,首先layout mTarget,然后是mCricleView*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
final View child = mTarget;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop();
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
int circleWidth = mCircleView.getMeasuredWidth();
int circleHeight = mCircleView.getMeasuredHeight();
mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
}
/**计算宽高 在此处还查找了mCricleView的index,即对mCircleViewIndex进行赋值*/
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (mTarget == null) {
ensureTarget();
}
if (mTarget == null) {
return;
}
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.
// 获取mCircleViewIndex
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
/**
* Get the diameter of the progress circle that is displayed as part of the
* swipe to refresh layout.
* 获取mCricleView的直径
* @return Diameter in pixels of the progress circle view.
*/
public int getProgressCircleDiameter() {
return mCircleDiameter;
}
/**
* 判断child是否还可以上滑
* @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);
}
/**
* Set a callback to override {@link SwipeRefreshLayout#canChildScrollUp()} method. Non-null
* callback will return the value provided by the callback and ignore all internal logic.
* @param callback Callback that should be called when canChildScrollUp() is called.
*/
public void setOnChildScrollUpCallback(@Nullable OnChildScrollUpCallback callback) {
mChildScrollUpCallback = callback;
}
/**拦截触摸事件(在派发触摸事件前,该方法会被调用)*/
// 参考:https://blog.csdn.net/lvxiangan/article/details/9309927
/**
*Implement this method to intercept all touch screen motion events. This allows you to watch events as they are dispatched
* to your children, and take ownership of the current gesture at any point.
*实现这个方法去拦截所有的触摸屏幕的动作事件。这允许在事件派发到你的子View时观察他们,同时在任何时候掌控当前手势的所有权。
*Using this function takes some care, as it has a fairly complicated interaction with View.onTouchEvent(MotionEvent),
* and using it requires implementing that method as well as this one in the correct way. Events will be received
* in the following order:
*使用这个方法时需要注意,由于它与View.onTouchEvent(MotionEvent)有一个相当的复杂的交互,并且使用它需要以正确的方式执行该方法。
*事件将按以下顺序收到:
*You will receive the down event here.
*你将会在这收到按下事件。
* 1. The down event will be handled either by a child of this view group, or given to your own onTouchEvent() method
* to handle; this means you should implement onTouchEvent() to return true, so you will continue to see the rest
* of the gesture (instead of looking for a parent view to handle it). Also, by returning true from onTouchEvent(),
* you will not receive any following events in onInterceptTouchEvent() and all touch processing must happen in
* onTouchEvent() like normal.
* 1. 按下事件将会由这个ViewGroup的子View来处理,或提供一个你自己的onTouchEvent()方法去处理;这意味着你需要要实现onTouchEvent()
* 以返回true,这样你将会继续看到剩下的手势(而不是寻找一个父View去处理它)。当然,通过从onTouchEvent()返回true,你将
* 不会在onInterceptTouchEvent()中接收任何接下来的事件,并且正常情况下所有的触摸处理必须发生在onTouchEvent()中。
* 2. For as long as you return false from this function, each following event (up to and including the final up) will
* be delivered first here and then to the target's onTouchEvent().
* 2. 只要你从这个函数返回false,每个下面的事件(直到并包括最后一个)都将在这里首先被传递,然后被传递到目标的onTouchEvent()。(笔记:这意味着本类的onTouchEvent()不会别调用)
* 3. If you return true from here, you will not receive any following events: the target view will receive the same
* event but with the action ACTION_CANCEL, and all further events will be delivered to your onTouchEvent() method
* and no longer appear here.
* 3. 如果你从这里返回true,(接下来,在这个方法中)你将不会收到以下任何事件:目标视图将接收到相同的事件,但是Action是ACTION_CANCEL,并且所有其他事件
* 将传递到你的onTouchEvent()方法,并且不会再出现在此处。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//这一步的操作是确保给mTarget赋值(当然,如果没有其它子View,mTarget也将不会被赋值)
ensureTarget();
final int action = ev.getActionMasked();
int pointerIndex;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
//在未使能、正在回到开始部位和childView可以向上滑、正在刷新中不处理
//每个下面的事件(直到并包括最后一个)都将在这里首先被传递,然后被传递到目标的onTouchEvent()
//mReturningToStart为true,表示这个意思:Target正在返回到其起始偏移量,因为它已被取消或触发了刷新。
// 如果SwipeRefreshLayout被除能、Target正在返回到其起始偏移量、子View可以还可以上滑、
// 正在刷新、正在嵌套滑动,则不拦截触摸事件
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:
//mCricleView归位
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());
// 记录下手指id
mActivePointerId = ev.getPointerId(0);
//开始拽动标志位设置为false
mIsBeingDragged = false;
//参考:触摸事件详解:https://www.cnblogs.com/everhad/p/6075716.html
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);
/**开始拖动,这个方法判断如果偏差距离大于触角,并且mIsBeingDragged为false时,给mInitialMotionY赋值为mInitialDownY + mTouchSlop
* 同时,mIsBeingDragged赋值为true,mProgress的alpha设置为STARTING_PROGRESS_ALPHA
*/
startDragging(y);
//通过上面知道,如果startDragging()运行完成后,onInterceptTouchEvent()将会返回true,意味这个接下来所有其他事件将传递到本类的onTouchEvent()方法中,接下继续完成拖动效果
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mIsBeingDragged = false;
mActivePointerId = INVALID_POINTER;
break;
}
return mIsBeingDragged;
}
@Override
public void requestDisallowInterceptTouchEvent(boolean b) {
// if this is a List < L or another view that doesn't support nested
// scrolling, ignore this request so that the vertical scroll event
// isn't stolen
if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)
|| (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {
// Nope.
} else {
super.requestDisallowInterceptTouchEvent(b);
}
}
// NestedScrollingParent
/**
*@link(http://android.xsoftlab.net/reference/android/support/v4/view/NestedScrollingParent.html#onNestedScrollAccepted(android.view.View, android.view.View, int))
*React to a descendant view initiating a nestable scroll operation, claiming the nested scroll operation if appropriate.
*响应启动可嵌套滚动操作的后代视图,并在适当的情况下声明嵌套滚动操作。
*This method will be called in response to a descendant view invoking startNestedScroll(View, int). Each parent up the view hierarchy will be given an opportunity to respond and claim the nested scrolling operation by returning true.
*这个方法将在调用startNestedScroll(View,int)的后代视图中被调用。 视图层次结构中的每个父代都将有机会通过返回true来响应和声明嵌套的滚动操作。
*This method may be overridden by ViewParent implementations to indicate when the view is willing to support a nested scrolling operation
*that is about to begin. If it returns true, this ViewParent will become the target view's nested scrolling parent for the duration of
*the scroll operation in progress. When the nested scroll is finished this ViewParent will receive a call to onStopNestedScroll(View).
*ViewParent实现可能会重写此方法,以指示视图何时愿意支持即将开始的嵌套滚动操作。
*如果它返回true,则在正在进行滚动操作期间,此ViewParent将成为目标视图的嵌套滚动父级。当嵌套滚动完成后,此ViewParent将接收对onStopNestedScroll(View)的调用。
*/
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return isEnabled() && !mReturningToStart && !mRefreshing
&& (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
/**
*React to the successful claiming of a nested scroll operation.
*响应成功声明嵌套滚动操作。
*This method will be called after onStartNestedScroll returns true. It offers an opportunity for the view and its superclasses to perform initial configuration for the nested scroll. Implementations of this method should always call their superclass's implementation of this method if one is present.
*此方法将在onStartNestedScroll返回true后调用。 它为视图及其超类为嵌套滚动执行初始配置提供了机会。
*如果存在这种方法的实现,则应始终调用其超类的此方法的实现。
*Parameters
*child Direct child of this ViewParent containing target
*target View that initiated the nested scroll
*nestedScrollAxes Flags consisting of SCROLL_AXIS_HORIZONTAL, SCROLL_AXIS_VERTICAL or both
*/
@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);
// Dispatch up to the nested parent
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
mTotalUnconsumed = 0;
mNestedScrollInProgress = true;
}
/**React to a nested scroll in progress before the target view consumes a portion of the scroll.
* 在目标视图占用滚动的一部分之前,对正在进行的嵌套滚动进行处理。
*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.
*当经常使用嵌套滚动时,父视图可能希望有机会在嵌套滚动子代之前使用滚动。 一个例子是一个包含可滚动列表的抽屉。
*用户将希望能够在列表本身开始滚动之前将列表完全滚动到视图中。
*onNestedPreScroll is called when a nested scrolling child invokes dispatchNestedPreScroll(int, int, int[], int[]). The implementation should report how any pixels of the scroll reported by dx, dy were consumed in the consumed 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.
*onNestedPreScroll在嵌套滚动子级调用dispatchNestedPreScroll(int,int,int [],int [])时被调用。
*该实现应报告dx,dy所报告的滚动像素是如何在消耗的数组中消耗的。 索引0对应于dx,索引1对应于dy。 该参数永远不会为空。
*消耗[0]和消耗[1]的初始值将始终为0。
*Parameters
*target View that initiated the nested scroll
*dx Horizontal scroll distance in pixels
*dy Vertical scroll distance in pixels
*consumed Output. The horizontal and vertical scroll distance consumed by this parent
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
// If we are in the middle of consuming, a scroll, then we want to move the spinner back up
// before allowing the list to scroll
// 如果我们正处于消耗,滚动的过程中,那么我们希望在允许列表滚动之前移动微调器
if (dy > 0 && mTotalUnconsumed > 0) {
if (dy > mTotalUnconsumed) {
consumed[1] = dy - (int) mTotalUnconsumed;
mTotalUnconsumed = 0;
} else {
mTotalUnconsumed -= dy;
consumed[1] = dy;
}
moveSpinner(mTotalUnconsumed);
}
// If a client layout is using a custom start position for the circle
// view, they mean to hide it again before scrolling the child view
// If we get back to mTotalUnconsumed == 0 and there is more to go, hide
// the circle so it isn't exposed if its blocking content is moved
if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0
&& Math.abs(dy - consumed[1]) > 0) {
mCircleView.setVisibility(View.GONE);
}
// Now let our nested parent consume the leftovers
final int[] parentConsumed = mParentScrollConsumed;
if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
}
@Override
public int getNestedScrollAxes() {
return mNestedScrollingParentHelper.getNestedScrollAxes();
}
@Override
public void onStopNestedScroll(View target) {
mNestedScrollingParentHelper.onStopNestedScroll(target);
mNestedScrollInProgress = false;
// Finish the spinner for nested scrolling if we ever consumed any
// unconsumed nested scroll
if (mTotalUnconsumed > 0) {
finishSpinner(mTotalUnconsumed);
mTotalUnconsumed = 0;
}
// Dispatch up our nested parent
stopNestedScroll();
}
@Override
public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
// Dispatch up to the nested parent first
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
mParentOffsetInWindow);
// This is a bit of a hack. Nested scrolling works from the bottom up, and as we are
// sometimes between two nested scrolling views, we need a way to be able to know when any
// nested scrolling parent has stopped handling events. We do that by using the
// 'offset in window 'functionality to see if we have been moved from the event.
// This is a decent indication of whether we should take over the event stream or not.
final int dy = dyUnconsumed + mParentOffsetInWindow[1];
if (dy < 0 && !canChildScrollUp()) {
mTotalUnconsumed += Math.abs(dy);
moveSpinner(mTotalUnconsumed);
}
}
// NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return mNestedScrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return mNestedScrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
mNestedScrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return mNestedScrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return mNestedScrollingChildHelper.dispatchNestedPreScroll(
dx, dy, consumed, offsetInWindow);
}
@Override
public boolean onNestedPreFling(View target, float velocityX,
float velocityY) {
return dispatchNestedPreFling(velocityX, velocityY);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY,
boolean consumed) {
return dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
private boolean isAnimationRunning(Animation animation) {
return animation != null && animation.hasStarted() && !animation.hasEnded();
}
/**开始移动指示器mCricleView */
// 手指下拉过程中触发的圆圈的变化过程,透明度变化,渐渐出现箭头,大小的变化
private void moveSpinner(float overscrollTop) {
//使能arrow
mProgress.setArrowEnabled(true);
//mTotalDragDistance默认设置为64dp
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;
// 这里mUsingCustomStart 为 true 代表用户自定义了起始出现的坐标
float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
: mSpinnerOffsetEnd;
//弹性系数
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)
/ slingshotDist);
//公式:y = (x/4 - (x/4)^2)*2 (x[0,2],y[0,1/2])
float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(
(tensionSlingshotPercent / 4), 2)) * 2f;
float extraMove = (slingshotDist) * tensionPercent * 2;
//因为有弹性系数,所以,在手指移动相同的一段距离,mCricleView移动的距离不同
//(随着偏移距离越大,手指移动间隔不变的情况下,mCricleView移动距离越小。)。
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) {
mCircleView.setScaleX(1f);
mCircleView.setScaleY(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);
/**
* 这将会让mCricleView提到前台
* 同时将mCricleView进行offset大小的竖直位移
* 将mCricle当前的top值赋值给mCurrentTargetOffsetTop
*/
setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop);
}
/**
*该方法将会把mCricleView动画到初始位置
*根据入参overscrollTop与mTotalDragDistance大小比较,来确实是否触发刷新动画
*/
private void finishSpinner(float overscrollTop) {
if (overscrollTop > mTotalDragDistance) {
//设置为刷新状态,并回调刷新接口
setRefreshing(true, true /* notify */);
} else {
// cancel refresh
mRefreshing = false;
mProgress.setStartEndTrim(0f, 0f);
Animation.AnimationListener listener = null;
if (!mScale) {
listener = new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (!mScale) {
startScaleDownAnimation(null);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
};
}
/**将mCricleView从mCurrentTargetOffsetTop移动到初始位置 */
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
//不显示arrow
mProgress.setArrowEnabled(false);
}
}
/**触摸事件
* 能够运行到这里,说明在onInterceptTouchEvent()方法的ACTION_MOVE中,startDragging()方法将mIsBeingDragged置为true
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getActionMasked();
int pointerIndex = -1;
if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {
mReturningToStart = false;
}
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:
mActivePointerId = ev.getPointerId(0);
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_MOVE: {
pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex < 0) {
Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id.");
return false;
}
final float y = ev.getY(pointerIndex);
//开始拖动,这个方法判断如果偏差距离大于触角,并且mIsBeingDragged为false时,给mInitialMotionY赋值为mInitialDownY + mTouchSlop
//同时,mIsBeingDragged赋值为true,mProgress的alpha设置为STARTING_PROGRESS_ALPHA
//理论上来讲,该方法中的逻辑不应该被触发,因为能运行到这里,说明mIsBeingDragged不为false
startDragging(y);
//开始移动spinner
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;//DRAG_RATE = .5f
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
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;
}
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;//DRAG_RATE = .5f
mIsBeingDragged = false;
finishSpinner(overscrollTop);
}
mActivePointerId = INVALID_POINTER;
return false;
}
case MotionEvent.ACTION_CANCEL:
return false;
}
return true;
}
/**开始拖动,这个方法判断如果偏差距离大于触角,并且mIsBeingDragged为false时,给mInitialMotionY赋值为mInitialDownY + mTouchSlop
* 同时,mIsBeingDragged赋值为true,mProgress的alpha设置为STARTING_PROGRESS_ALPHA
*/
private void startDragging(float y) {
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
//防抖动?额,没搞懂
mInitialMotionY = mInitialDownY + mTouchSlop;
//在这里设置mIsBeingDragged为true
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
}
/**
* 将mCricleView归位到正确的位置的动画
*/
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);
}
/**将mCricleView动画到初始位置 */
private void animateOffsetToStartPosition(int from, AnimationListener listener) {
if (mScale) {
// Scale the item back down
// 缩放这个item返回到初始位置
startScaleDownReturnToStartAnimation(from, listener);
} else {
//将来源位置赋值给mFrom,这个值将会在mAnimateToStartPosition动画中使用,用于计算offset,从而计算出mCricleView应该出现的位置
mFrom = from;
//在这个动画的回调中,会将mCricleView移动到初始位置
mAnimateToStartPosition.reset();
mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);
mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mAnimateToStartPosition);
}
}
/**
* 通过动画让目标回到正确的位置,根据mFrom和mCricleView.top来就是你偏差,
* 依据插值时间,创建mProgress的动画,同时会将mCricleView进行竖直位移
*/
private final Animation mAnimateToCorrectPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
int targetTop = 0;
int endTarget = 0;
if (!mUsingCustomStart) {
endTarget = mSpinnerOffsetEnd - Math.abs(mOriginalOffsetTop);
} else {
endTarget = mSpinnerOffsetEnd;
}
targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));
int offset = targetTop - mCircleView.getTop();
setTargetOffsetTopAndBottom(offset);
mProgress.setArrowScale(1 - interpolatedTime);
}
};
/**移动到顶部*/
void moveToStart(float interpolatedTime) {
int targetTop = 0;
targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));
int offset = targetTop - mCircleView.getTop();
setTargetOffsetTopAndBottom(offset);
}
private final Animation mAnimateToStartPosition = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
//不断的将mCricleView移动到顶部
moveToStart(interpolatedTime);
}
};
private void startScaleDownReturnToStartAnimation(int from,
Animation.AnimationListener listener) {
mFrom = from;
mStartingScale = mCircleView.getScaleX();
mScaleDownToStartAnimation = new Animation() {
@Override
public void applyTransformation(float interpolatedTime, Transformation t) {
float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime));
setAnimationProgress(targetScale);
moveToStart(interpolatedTime);
}
};
mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION);
if (listener != null) {
mCircleView.setAnimationListener(listener);
}
mCircleView.clearAnimation();
mCircleView.startAnimation(mScaleDownToStartAnimation);
}
/**
* 这将会让mCricleView提到前台
* 同时将mCricleView进行offset大小的竖直位移
* 将mCricle当前的top值赋值给mCurrentTargetOffsetTop
*/
void setTargetOffsetTopAndBottom(int offset) {
mCircleView.bringToFront();
//对mCricleView进行竖直位移,offset大于零表示正向位移(向下),反之反向位移
ViewCompat.offsetTopAndBottom(mCircleView, offset);
mCurrentTargetOffsetTop = mCircleView.getTop();
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = ev.getActionIndex();
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mActivePointerId = ev.getPointerId(newPointerIndex);
}
}
/**
* Classes that wish to be notified when the swipe gesture correctly
* triggers a refresh should implement this interface.
*/
public interface OnRefreshListener {
/**
* Called when a swipe gesture triggers a refresh.
*/
void onRefresh();
}
/**
* Classes that wish to override {@link SwipeRefreshLayout#canChildScrollUp()} method
* behavior should implement this interface.
*/
public interface OnChildScrollUpCallback {
/**
* Callback that will be called when {@link SwipeRefreshLayout#canChildScrollUp()} method
* is called to allow the implementer to override its behavior.
*
* @param parent SwipeRefreshLayout that this callback is overriding.
* @param child The child view of SwipeRefreshLayout.
*
* @return Whether it is possible for the child view of parent layout to scroll up.
*/
boolean canChildScrollUp(SwipeRefreshLayout parent, @Nullable View child);
}
}
网友评论