前言
相信SwipeRefreshLayout对大家都不陌生,SwipeRefreshLayout是谷歌官方推出的下拉刷新控件,因为简洁,美观,使用起来简单(相信都有被第三方坑过的小码农,个人是比较偏向使用谷歌原生的东西,现在谷歌推出的东西,至少bug少,且也不失美观),深受各位开发人员的喜爱(掘金app也在使用)。
OK,下面进入正题。
SwipeRefreshLayout一般内部都是嵌套着RecyclerView,ListView,ScrollView 等一些可滚动的view,所以SwipeRefreshLayout无疑是继承了ViewGroup,OK,先让我们看看它的构造方法
public SwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
//....此部分代码省略,初始化了一些参数及设置(包括获得滑动多少像素才能滑动view的阈值,动画时长,背景宽高等)
createProgressView();
mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);
mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);
}
我们可以先看NestedScrollingParentHelper
,NestedScrollingChildHelper
这两个类,是Android在support.v4包中为大家提供的两个嵌套滚动帮助类,这里不深究,要是想了解的童鞋们,可以看下这篇洋神写的Android NestedScrolling机制完全解析 带你玩转嵌套滑动,OK,那我们继续往下看,createProgressView()
,从名字上看大概就知道是创建了那个转啊转刷新的View,那我们点进去,看下创建了什么。
private void createProgressView() {
mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2);
mProgress = new MaterialProgressDrawable(getContext(), this);
mProgress.setBackgroundColor(CIRCLE_BG_LIGHT);
mCircleView.setImageDrawable(mProgress);
mCircleView.setVisibility(View.GONE);
addView(mCircleView);
}
咦,我们这里可以看到创建了两个对象,CircleImageView
和MaterialProgressDrawable
,那这两个到底是什么呢,其实CircleImageView
就是刷新的自定义view,它继承了ImageView,里面利用OvalShape绘制了一个圆形,然后设置成了CircleImageView的背景,那指示器的去哪了呢,看上面的代码,mCircleView.setImageDrawable(mProgress)
,它将MaterialProgressDrawable以Drawable的形式设置给了CircleImageView,那MaterialProgressDrawable又是何方神圣,其实它就是这个进度条的样式,继承于Drawable,也是绘制出来的,那我们看下它的draw()
到底做了什么
public void draw(Canvas c) {
final Rect bounds = getBounds();
final int saveCount = c.save();
c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY());
mRing.draw(c, bounds);
c.restoreToCount(saveCount);
}
这里保存了画布的状态,让画布rotate,而且还把画布传给了mRing,mRing是MaterialProgressDrawable的内部类Ring,进度圈其实就是由Ring绘制的,代码如下
public void draw(Canvas c, Rect bounds) {
final RectF arcBounds = mTempBounds;
arcBounds.set(bounds);
arcBounds.inset(mStrokeInset, mStrokeInset);
final float startAngle = (mStartTrim + mRotation) * 360;
final float endAngle = (mEndTrim + mRotation) * 360;
float sweepAngle = endAngle - startAngle;
mPaint.setColor(mCurrentColor);
c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint);
drawTriangle(c, startAngle, sweepAngle, bounds);
if (mAlpha < 255) {
mCirclePaint.setColor(mBackgroundColor);
mCirclePaint.setAlpha(255 - mAlpha);
c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2,
mCirclePaint);
}
}
它绘制了箭头和进度圈,还有中间部分的背景圆,OK,如何创建ProgressView大概就是这样了,接下来看它是如何监听手势的,根据事件分发的机制,触摸事件应该是先传递到ViewGroup,根据onInterceptTouchEvent的返回值决定是否拦截事件的,那么就onInterceptTouchEvent出发:
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (action) {
case MotionEvent.ACTION_DOWN:
setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true);
mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
mIsBeingDragged = false;
final float initialDownY = getMotionEventY(ev, mActivePointerId);
if (initialDownY == -1) {
return false;
}
mInitialDownY = initialDownY;
break;
case MotionEvent.ACTION_MOVE:
final float y = getMotionEventY(ev, mActivePointerId);
final float yDiff = y - mInitialDownY;
if (yDiff > mTouchSlop && !mIsBeingDragged) {
mInitialMotionY = mInitialDownY + mTouchSlop;
mIsBeingDragged = true;
mProgress.setAlpha(STARTING_PROGRESS_ALPHA);
}
break;
return mIsBeingDragged;
}
这里我省略了一部分代码,这里其实做了不少处理,但是为了简洁,看的舒服,我把大部分return false的都给剔除掉了,我们主要看他如何出现下拉的,所以只看它拦截部分和关键部分,这里ACTION_DOWN
时记录了initialDownY
,也就是落指的Y坐标,接着看ACTION_MOVE
,这里我也省了一部分代码,主要看mIsBeingDragged
返回true的条件,当手指滑动的距离大于要滑动View的阈值(也就是要想使view滚动的条件,必须滑动了多少距离的意思),那么就会return ture,拦截后时间就是自己处理,那接下来就是看onTouchEvent
public boolean onTouchEvent(MotionEvent ev) {
switch (action) {
case MotionEvent.ACTION_MOVE: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
if (mIsBeingDragged) {
if (overscrollTop > 0) {
moveSpinner(overscrollTop);
} else {
return false;
}
}
break;
}
case MotionEvent.ACTION_UP: {
pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
final float y = MotionEventCompat.getY(ev, pointerIndex);
final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;
mIsBeingDragged = false;
finishSpinner(overscrollTop);
mActivePointerId = INVALID_POINTER;
return false;
}
}
return true;
}
同样,看关键部分,ACTION_MOVE
先计算出手指移动距离,然后大于0的话,就调用moveSpinner(overscrollTop)
,而ACTION_UP
一开始代码逻辑一样,然后调用了finishSpinner(overscrollTop)
,看来这两句就应该是主要的关键部分,让我们先来看下moveSpinner
方法
private void moveSpinner(float overscrollTop) {
mProgress.showArrow(true);
//...省略了计算 移动距离的逻辑代码
int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);
// where 1.0f is a full circle
if (mCircleView.getVisibility() != View.VISIBLE) {
mCircleView.setVisibility(View.VISIBLE);
}
//...省略了进入动画的逻辑代码
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 */);
}
这里主要根据你的移动距离,然后让其进度圈view显示,其中了还有比较多的细节(如控制移动速度,超过刷新距离后移动变慢)。倒数第二行执行了setProgressRotation,传入的是经过一堆计算后的rotation,传入该方法后,mProgress也就是MaterialProgressDrawable就根据它来绘制进度圈。
最后一行执行setTargetOffsetTopAndBottom,里面的代码比较简单,就是设置进度圈的位置
接着我们来看下ACTION_UP
里的finishSpinner(overscrollTop)
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) {
}
};
}
animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);
mProgress.showArrow(false);
}
}
代码比较简单,我就全贴了,
如果移动的距离没有大于设定值(如果有印象的童鞋应该会记得如果滑动一点点,那进度圈就会收回去),执行startScaleDownAnimation
,也就是消失的动画,
如果移动的距离大于设定值,就执行setRefreshing(true,true),该方法会调用animateOffsetToCorrectPosition
,代码如下
private void setRefreshing(boolean refreshing, final boolean notify) {
if (mRefreshing != refreshing) {
mNotify = notify;
ensureTarget();
mRefreshing = refreshing;
if (mRefreshing) {
animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);
} else {
startScaleDownAnimation(mRefreshListener);
}
}
}
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);
}
animateOffsetToCorrectPosition()
的方法也是全贴的了,对mCircleView(CircleImageView)
的一些属性重置,然后调用mCircleView.startAnimation()
,如果我们有传入过listener的话,就会执行onRefresh方法,刷新完再setRefreshing(false)
,就会再执行startScaleDownAnimation()
收回去了。
到这里,相信大家也会比较清楚SwipeRefreshLayout
的流程了, 希望看官老爷们喜欢,也希望我们大家在使用的同时,更能了解源码,知道如果出现问题该如何解决,如有写的不好的地方,还请见谅、指点,
更多精彩文章请关注微信公众号"Android经验分享":这里将长期为您分享Android高手经验、中外开源项目、源码解析、框架设计和Android好文推荐!QQ交流群:Android经验分享一区 386067289