首先来看SwipeRefreshLayout(以下简称SR)的继承关系
NestedScrollingParent:嵌套滑动父接口
NestedScrollingChild :嵌套滑动子接口
Android 就是通过这两个接口, 来实现 子View 与父View 之间的嵌套滑动
- NestedScrollingChild:源码
public interface NestedScrollingChild {
/**
* Enable or disable nested scrolling for this view
* 为这个视图启用或禁用嵌套滚动
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* Returns true if nested scrolling is enabled for this view.
* 若启动嵌套滑动,则返回True
*/
public boolean isNestedScrollingEnabled();
/**
* Begin a nestable scroll operation along the given axes.
* 在给定的轴上开始一个新的滚动操作。
* ViewCompat.SCROLL_AXIS_HORIZONTAL 横向
* ViewCompat.SCROLL_AXIS_VERTICAL 纵向
*/
public boolean startNestedScroll(int axes);
/**
* Stop a nested scroll in progress.
* 停止嵌套的滚动
*/
public void stopNestedScroll();
/**
* Returns true if this view has a nested scrolling parent.
* 如果该视图有一个嵌套滚动的父视图,则返回true。
*/
public boolean hasNestedScrollingParent();
/**
* Dispatch one step of a nested scroll in progress.
*
* 在处理滑动之后 调用
* @param dxConsumed x轴上 被消费的距离
* @param dyConsumed y轴上 被消费的距离
* @param dxUnconsumed x轴上 未被消费的距离
* @param dyUnconsumed y轴上 未被消费的距离
* @param offsetInWindow view 的移动距离
* 如果事件被发送,则返回true,如果该事件不能被发送,则为false
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* Dispatch one step of a nested scroll in progress before this view consumes any portion of it.
*
*一般在滑动之前调用, 在ontouch 中计算出滑动距离, 然后调用该方法, 就给支持的嵌套的父View 处理滑动事件
* @param dx x 轴上滑动的距离, 相对于上一次事件, 不是相对于 down事件的 那个距离
* @param dy y 轴上滑动的距离
* @param consumed 一个数组, 可以传 一个空的 数组, 表示 x 方向 或 y 方向的事件 是否有被消费
* @param offsetInWindow 支持嵌套滑动到额父View 消费 滑动事件后 导致 本 View 的移动距离
* @return 支持的嵌套的父View 是否处理了 滑动事件
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* Dispatch a fling to a nested scrolling parent.
* @param velocityX x 轴上的滑动速度
* @param velocityY y 轴上的滑动速度
* @param consumed 是否被消费
* @return
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* Dispatch a fling to a nested scrolling parent before it is processed by this view.
*
*@param velocityX x 轴上的滑动速度
* @param velocityY y 轴上的滑动速度
* @return
* @param velocityX Horizontal fling velocity in pixels per second
* @param velocityY Vertical fling velocity in pixels per second
* @return true if a nested scrolling parent consumed the fling
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
- NestedScrollingParent源码:
public interface NestedScrollingParent {
/**
* React to a descendant view initiating a nestable scroll operation, claiming thenested scroll operation if appropriate.
* 对嵌套滚动的子View进行响应
*
*
* @param child ViewParent包含触发嵌套滚动的view的对象
* @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta rget)是相同的
* @param nestedScrollAxes 方向 ViewCompat.SCROLL_AXIS_HORIZONTAL
* ViewCompat.SCROLL_AXIS_VERTICAL
* @return true 如果ViewParent接受嵌套滚动操作,则返回true
*/
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
/**
* React to the successful claiming of a nested scroll operation.
* 对成功的使用嵌套滚动操作作出反应
* @param child ViewParent包含触发嵌套滚动的view的对象
* @param target触发嵌套滚动的view(在这里如果不涉及多层嵌套的话,child和ta rget)是相同的
* @param nestedScrollAxes 滑动的方向 ViewCompat#SCROLL_AXIS_HORIZONTAL},
* ViewCompat#SCROLL_AXIS_VERTICAL
*/
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
/**
* React to a nested scroll operation ending.
* 对一个嵌套滚动操作的结果进行响应
* @param target 启动滚动的View
*/
public void onStopNestedScroll(View target);
/**
* React to a nested scroll in progress.
* 对正在进行的嵌套滚动进行响应
* @param target 控制滚动的子View
* @param dxConsumed x轴消费的距离
* @param dyConsumed y轴消费的距离
* @param dxUnconsumed x轴未消费的距离
* @param dyUnconsumed y轴未消费的距离
*/
public void onNestedScroll(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.
*
* @param target 控制滚动的子View
* @param dx x轴消费总距离
* @param dy y轴消费总距离
* @param consumed Output. 父布局分别在x,y轴消费的总距离:consumed[0],
consumed[1]
*/
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
/**
* Request a fling from a nested scroll.
* 嵌套滑动的速度
* @param target 控制滚动的子View
* @param velocityX velocityX x 轴上的滑动速度
* @param velocityY y 轴上的滑动速度
* @param consumed 子view是否消费
* @return true if this parent consumed or otherwise reacted to the fling
*/
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
/**
* React to a nested fling before the target view consumes it.
*
* @param target 控制滚动的子View
* @param velocityX x 轴上的滑动速度
* @param velocityY y 轴上的滑动速度
* @return 如果父布局在这之前消费了该事件则返回True
*/
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
/**
* Return the current axes of nested scrolling for this NestedScrollingParent.返回一个当前滑动轴,以下3种情况
* @return Flags indicating the current axes of nested scrolling
* @see ViewCompat#SCROLL_AXIS_HORIZONTAL
* @see ViewCompat#SCROLL_AXIS_VERTICAL
* @see ViewCompat#SCROLL_AXIS_NONE
*/
public int getNestedScrollAxes();
}
这两个接口的作用在上面的注释中有详细的解释,下面就是最关键的SR源码的分析;因为SR继承的是ViewGroup,我们平常都会自定义View,而自定义View通常都少不了:onMeasure(测量),onDraw(绘画);而自定义ViewGroup会涉及到对子View的排版问题,所以在自定义ViewGroup中多了一个onLayout()方法需要我们处理,这些基本的问题解决后,若自定义控件涉及到触摸事件,也会需要我们对触摸事件的分发机制有一定的了解;然后就让我们根据SR源码来一步一步分析下拉刷新控件是怎样实现的!(对源码的分析都是以代码的注释的形式来进行的)
- SR构造:
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//ViewConfiguration定义UI中用于超时、大小和距离的标准常量和获取他们的值的方法
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//获取动画时间
mMediumAnimationDuration = getResources().getInteger(
android.R.integer.config_mediumAnimTime);
//若没有任何绘图,则设置此方法(没有重写onDrow方法)
setWillNotDraw(false);
//设置减速插值器
mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);
// 描述一个显示的一般信息的结构,例如它的大小、密度和字体大小。
final DisplayMetrics metrics = getResources().getDisplayMetrics();
mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);
//创建头部刷新控件
createProgressView();
//告诉ViewGroup是否按照该方法定义的顺序绘制它的孩子
ViewCompat.setChildrenDrawingOrderEnabled(this, true);
// the absolute offset has to take into account that the circle starts at an offset
mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);
mTotalDragDistance = mSpinnerOffsetEnd;
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;
//头部刷新控件起始位置
moveToStart(1.0f);
final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
setEnabled(a.getBoolean(0, true));
a.recycle();
}
在构造方法中主要做了一下几件事情:
- 对一些常量(列如动画时间,圆的直径,圆的偏移量等)的设置
- 将一个头部刷新控件加入进来
- 创建mNestedScrollingParentHelper,mNestedScrollingChildHelper等对象,为这个视图启用嵌套滚动
onMeasure 方法:三件事
- 找出目标View
- 测量子控件的大小
- 得到下拉刷新View的Index
@Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//mTarget:手势拖动的目标View
if (mTarget == null) {
//将不是头部刷新的View赋给mTarget
ensureTarget();
}
if (mTarget == null) {
return;
}
//根据测量规格测出目标View的大小
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.
for (int index = 0; index < getChildCount(); index++) {
if (getChildAt(index) == mCircleView) {
mCircleViewIndex = index;
break;
}
}
}
在得到各个子控件的大小后,就是对各个控件的排版问题,也就是 onLayout()方法
- 确定目标View的位置:child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
- 确定刷新控件的位置:mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,
(width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);
@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);
}
因为下拉刷新控件是在一开始的时候是不显示的,所以就要考虑各个子控件的绘制顺序,将下拉刷新控件放在最后绘制,getChildDrawingOrder用于返回当前迭代子视图的索引.就是说获取当前正在绘制的视图索引. 如果需要改变ViewGroup子视图绘制的顺序,则需要重载这个方法.(我试了一下,不重写好像也没问题)
@Override
protected int getChildDrawingOrder(int childCount, int 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;
}
}
然后就是触摸事件分发机制;onInterceptTouchEvent():onInterceptTouchEvent是在ViewGroup里面定义的,该方法决定了事件到底交给谁处理 。
- 当return true时,表示ViewGroup自己来处理onTouchEvent事件,子View接收不到onTouchEvent事件
- 当return false时,表示ViewGroup不拦截事件,直接交给子View处理
onTouchEvent:
- onTouchEvent只有当onInterceptTouchEvent返回true的时候才执行。它根据下拉的距离,动态的修改headerView的位置,通过调用setTargetOffsetTopAndBottom调用invalidate()方法进行重绘。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//一大堆根据当前状态判断是否拦截触摸事件的逻辑
//就是根据是否是最后一个条目或者是第一个条目进行事件拦截
....
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
...
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);
startDragging(y);
if (mIsBeingDragged) {
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
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);
}
mActivePointerId = INVALID_POINTER;
return false;
}
}
onTouchEvent方法中最重要的便是moveSpinner(overscrollTop)和finishSpinner(overscrollTop)方法的调用
- 获取拖拽百分比和高度差并修正
- 开启动画
- 动态修正下拉刷新控件的位置
设置监听
moveSpinner
@SuppressLint("NewApi")
private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);
//原始拖动距离百分比
float originalDragPercent = overscrollTop / mTotalDragDistance;
//原谅我的数学太差,我不知道我为什么用下面的公式计算下拉偏移量,
float dragPercent = Math.min(1f, Math.abs(originalDragPercent));
float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;
float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;
float slingshotDist = mUsingCustomStart ? mSpinnerOffsetEnd - mOriginalOffsetTop
: mSpinnerOffsetEnd;
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;
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();
}
}
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
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) {
}
};
}
//这个方法是进行下拉刷新的回复在ANIMATE_TO_START_DURATION=200毫秒内
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
mProgress.showArrow(false);
}
}
总结
到此我们的分析基本结束了,让我们一起来看看做了多少事情才写出一个下拉刷新控件
- addView() 加入下拉刷新控件
- 测量各个子view的大小
- onLayout()对子view的位置进行确定以及确定各子view的绘制顺序
- 触摸事件的分发机制
- 嵌套滑动实现
- 设置回调接口
下拉刷新控件的机制我们了解的差不多了,下面就是我们定制自己的下拉刷新,上拉加载控件了——仿UC头条下拉刷新布局
ps:上面也说了楼主数学太差,那个圆的角度变化和下拉距离偏移量的关系式对楼主来说太难了,所以就搞了个假的!不多说了,来看下效果图:
这只是加深对自定义ViewGroup的理解而做的一个小Demo,下面是代码地址
Github>>>,大家随便看看就行,推荐一个很酷炫的下拉刷新第三方,楼主就是看了这位大神写的下拉刷新控件才想看看原理是怎样的!
酷炫的下拉刷新上拉加载控件》》》
- 若有错误,敬请指正!!!