您的位置:首页 > 移动开发 > Android开发

SwipeRefreshLayout源码分析+自定义UC头条下拉刷新Demo

2017-07-27 17:37 239 查看

首先来看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 d
114fd
yConsumed,
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>>>,大家随便看看就行,推荐一个很酷炫的下拉刷新第三方,楼主就是看了这位大神写的下拉刷新控件才想看看原理是怎样的!

酷炫的下拉刷新上拉加载控件》》》

若有错误,敬请指正!!!

拼搏在技术道路上的一只小白And成长之路

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息