SwipeRefreshLayout,用最少的代码定制最美的上下拉刷新样式
下拉刷新框架其实有很多,而且质量都比较高。但是在日常开发中,每一款产品都会有一套自己独特的一套刷新样式。相信有很多小伙伴在个性化定制中都或多或少的遇到过麻烦。今天我就给大家推荐一个在定制方面很出彩的一个刷新框架SwipeToLoadLayout,该框架自身完成了下拉刷新与上拉加载功能,同时将顶部视图与底部视图的UI定制功能通过接口很方便的提供给使用者自行定义。
相关代码已经上传到github上,欢迎star、fork
基本流程
先简单了解一下SwipeToLoadLayout的使用流程,以下拉刷新为例:
- 完成Header部分,实现SwipeRefreshTrigger与SwipeRefreshTrigger接口
- 完成activity或fragment的布局,在SwipeToLoadLayout节点下配置好Header与下拉目标组件(如RecyclerView等)
这里还是要稍微说一下,因为这个布局过程还是有一定的规则的
首先布局的id是固定的,这个我们在ids.xml中就能看出。框架提供三个View:Header、Target、Footer,分别对应三个位置的View
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="swipe_target" type="id" />
<item name="swipe_refresh_header" type="id" />
<item name="swipe_load_more_footer" type="id" />
</resources>
其次onFinishInflate()方法告诉我们,最多只能同时存在这三个View,不能有更多的子View了
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final int childNum = getChildCount();
if (childNum == 0) {
// no child return
return;
} else if (0 < childNum && childNum < 4) {
mHeaderView = findViewById(R.id.swipe_refresh_header);
mTargetView = findViewById(R.id.swipe_target);
mFooterView = findViewById(R.id.swipe_load_more_footer);
} else {
// more than three children: unsupported!
throw new IllegalStateException("Children num must equal or less than 3");
}
if (mTargetView == null) {
return;
}
if (mHeaderView != null && mHeaderView instanceof SwipeTrigger) {
mHeaderView.setVisibility(GONE);
}
if (mFooterView != null && mFooterView instanceof SwipeTrigger) {
mFooterView.setVisibility(GONE);
}
}
这样你就能得出下一步该怎么来实现了吧?没错肯定是这样的
<?xml version="1.0" encoding="utf-8"?>
<com.aspsine.swipetoloadlayout.SwipeToLoadLayout >
<View
android:id="@id/swipe_refresh_header" />
<android.support.v7.widget.RecyclerView
android:id="@id/swipe_target" />
<View
android:id="@id/swipe_load_more_footer" />
</com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
Header的部分尤为重要。我们需在Header上实现SwipeTrigger与SwipeRefreshTrigger接口,接口中的方法分别对应滑动刷新在各个状态下的回调。它们分别为
onPrepare:代表下拉刷新开始的状态
onMove:代表正在滑动过程中的状态
onRelease:代表手指松开后,下拉刷新进入松开刷新的状态
onComplete:代表下拉刷新完成的状态
onReset:代表下拉刷新重置恢复的状态
onRefresh:代表正在刷新中的状态
有了这几个接口,我们就可以完成Header部分的任何动画效果了。当然上拉加载更多的场景,只是把SwipeRefreshTrigger接口换成SwipeLoadMoreTrigger接口而已,其他跟下拉刷新情况完全相同
- 在activity或fragment中配置下拉监听事件,并在数据获取完成后主动触发刷新swipeToLoadLayout.setRefreshing(false);完成功能
更深入的部分我们放到源码分析里面再说
看起来好像很简单,那么我们就通过几个小Demo了解一下如何使用吧
仿新浪微博
之所以第一个范例选择新浪微博,是因为它是最传统刷新风格:根据箭头和文字的不同来表明当前不同的状态
如果你在早期研究过PullToRefresh,那么很容易在这个框架基础上实现相应的视图更新功能
先完成头部的定义。WeiboRefreshHeaderView作为头,其实际为一个LinearLayout
class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
头部布局很简单
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="60dip"
android:gravity="center"
android:orientation="horizontal">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/pb_weibo"
style="?android:attr/progressBarStyleSmallInverse"
android:layout_centerInParent="true"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/iv_weibo"
android:src="@mipmap/tableview_pull_refresh_arrow_down"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
<TextView
android:id="@+id/tv_weibo"
android:layout_marginStart="10dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下拉刷新"/>
</com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView>
activity的布局也很简单,把头跟身子一起加在SwipeToLoadLayout里
<?xml version="1.0" encoding="utf-8"?>
<com.aspsine.swipetoloadlayout.SwipeToLoadLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/swipe_weibo">
<include
layout="@layout/header_weibo"
android:id="@id/swipe_refresh_header" />
<TextView
android:id="@id/swipe_target"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="下拉刷新"/>
</com.aspsine.swipetoloadlayout.SwipeToLoadLayout>
下面就是完成头部动画效果了。新浪微博的这个效果就是视图被下拉到头部高度之后,将箭头位置旋转一下同时更换文字,刷新时展现progressbar即可
class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
var pb_weibo: ProgressBar? = null
var iv_weibo: ImageView? = null
var tv_weibo: TextView? = null
// 是否发生旋转
var rotated = false
private val rotate_up: Animation by lazy {
AnimationUtils.loadAnimation(context, R.anim.rotate_up)
}
private val rotate_down: Animation by lazy {
AnimationUtils.loadAnimation(context, R.anim.rotate_down)
}
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
override fun onFinishInflate() {
super.onFinishInflate()
pb_weibo = findViewById(R.id.pb_weibo)
iv_weibo = findViewById(R.id.iv_weibo)
tv_weibo = findViewById(R.id.tv_weibo)
}
override fun onReset() {
pb_weibo?.visibility = View.GONE
iv_weibo?.visibility = View.VISIBLE
tv_weibo?.text = "下拉刷新"
}
override fun onComplete() {
tv_weibo?.text = "刷新完成"
pb_weibo?.visibility = View.GONE
}
override fun onRelease() {
}
override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
if (p0 > SizeUtils.dp2px(60f)) {
if (!rotated) {
rotated = true
tv_weibo?.text = "释放更新"
iv_weibo?.clearAnimation()
iv_weibo?.startAnimation(rotate_up)
}
}
else {
if (rotated) {
rotated = false
tv_weibo?.text = "下拉刷新"
iv_weibo?.clearAnimation()
iv_weibo?.startAnimation(rotate_down)
}
}
}
override fun onPrepare() {
}
override fun onRefresh() {
tv_weibo?.text = "加载中"
iv_weibo?.clearAnimation()
iv_weibo?.visibility = View.GONE
pb_weibo?.visibility = View.VISIBLE
}
}
对照一下上文的刷新周期,应该很好理解
美团外卖
美团外卖是利用ImageView直接播放一段animation直到刷新完成停止。在下拉过程中,该ImageView随着位移的距离变化而发生相应的大小变化
美团外卖动画效果是由一系列的图片组成的,所以与新浪微博效果相比更为简单一些
一样要完成头部视图的定义
class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:gravity="center"
android:padding="10dip">
<ImageView
android:id="@+id/iv_mt"
android:layout_width="112dp"
android:layout_height="44dp"
android:background="@drawable/animation_list_refresh_mt"
android:transformPivotX="56dp"
android:transformPivotY="22dp"
android:scaleY="0.3"
android:scaleX="0.3"/>
</com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView>
剩下就是完成动画的播放与缩放的处理了
class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
var iv_mt: ImageView? = null
val animationDrawable: AnimationDrawable by lazy {
iv_mt?.background as AnimationDrawable
}
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
override fun onFinishInflate() {
super.onFinishInflate()
iv_mt = findViewById(R.id.iv_mt)
}
override fun onReset() {
}
override fun onComplete() {
animationDrawable.stop()
}
override fun onRelease() {
}
override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
val percent = if (p0 * 1.0f / SizeUtils.dp2px(44f) > 1) 1f else p0 * 1.0f / SizeUtils.dp2px(44f)
iv_mt?.scaleY = (0.3f + 0.7 * percent).toFloat()
iv_mt?.scaleX = (0.3f + 0.7 * percent).toFloat()
}
override fun onPrepare() {
if (!animationDrawable.isRunning) {
animationDrawable.start()
}
iv_mt?.scaleY = 0.3f
iv_mt?.scaleX = 0.3f
}
override fun onRefresh() {
if (!animationDrawable.isRunning) {
animationDrawable.start()
}
iv_mt?.scaleY = 1f
iv_mt?.scaleX = 1f
}
}
代码都很简单,很容易理解
饿了么
饿了么的效果是通过SVG来实现的
饿了么app对资源进行了混淆,所以我拿不到图片,只能随便从其他地方找一个了
一样是Header的编写,这里面有一点不同,我用android-pathview这个开源框架实现SVG播放进度控制功能
我需要将这个动画效果在下拉刷新的过程中实现
class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
<?xml version="1.0" encoding="utf-8"?>
<com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<com.eftimoff.androipathview.PathView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/pathView_ele"
android:layout_width="58dp"
android:layout_height="58dp"
app:pathColor="@android:color/black"
app:svg="@raw/issues"
app:pathWidth="2dp"/>
</com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView>
下面就是根据滑动偏移量来处理SVG播放的进度
class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
var pathView_ele: PathView? = null
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
override fun onFinishInflate() {
super.onFinishInflate()
pathView_ele = findViewById(R.id.pathView_ele)
}
override fun onReset() {
}
override fun onComplete() {
pathView_ele?.setPercentage(1f)
}
override fun onRelease() {
}
override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
val percent = 1 - (SizeUtils.dp2px(58f) - p0) * 1.0f / SizeUtils.dp2px(58f)
val value = if (percent >= 1) 1f else percent
pathView_ele?.setPercentage(value)
}
override fun onPrepare() {
pathView_ele?.setPercentage(0f)
}
override fun onRefresh() {
pathView_ele?.setPercentage(1f)
}
}
这里你会发出一个疑问,怎么效果与饿了么有的差距?饿了么是滑动到Header完成展开之后就不再继续下滑了,那咱们这个怎么实现呢?那我只能说不好意思,在现有条件下咱们实现不了,只能通过改源码完成
那我们就顺带来阅读源码,看看这个地方怎么改进吧?
源码分析
之前的onFinishInflate咱们就不说了,那个就是告诉我们只能有三个View,分别是Header、Target、Footer
然后是测量阶段,在测量阶段可以得到两个重要的变量mHeaderHeight与mFooterHeight,他们分别代表Header与Footer的高度。同时如果定义的mRefreshTriggerOffset(松开刷新的高度)比Header或Footer的高度小,则修正这个刷新位置
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// header
if (mHeaderView != null) {
final View headerView = mHeaderView;
measureChildWithMargins(headerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = ((MarginLayoutParams) headerView.getLayoutParams());
mHeaderHeight = headerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (mRefreshTriggerOffset < mHeaderHeight) {
mRefreshTriggerOffset = mHeaderHeight;
}
}
// target
if (mTargetView != null) {
final View targetView = mTargetView;
measureChildWithMargins(targetView, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
// footer
if (mFooterView != null) {
final View footerView = mFooterView;
measureChildWithMargins(footerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
MarginLayoutParams lp = ((MarginLayoutParams) footerView.getLayoutParams());
mFooterHeight = footerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
if (mLoadMoreTriggerOffset < mFooterHeight) {
mLoadMoreTriggerOffset = mFooterHeight;
}
}
}
在onLayout中对三个视图进行布局
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
layoutChildren();
mHasHeaderView = (mHeaderView != null);
mHasFooterView = (mFooterView != null);
}
这里有一个重要的方法layoutChildren,这个方法就是改变三个视图的位置的。当然这个位置要根据不同的类型来处理,默认情况下我们都是STYLE.CLASSIC类型。
private void layoutChildren() {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int paddingRight = getPaddingRight();
final int paddingBottom = getPaddingBottom();
if (mTargetView == null) {
return;
}
// layout header
if (mHeaderView != null) {
final View headerView = mHeaderView;
MarginLayoutParams lp = (MarginLayoutParams) headerView.getLayoutParams();
final int headerLeft = paddingLeft + lp.leftMargin;
final int headerTop;
switch (mStyle) {
case STYLE.CLASSIC:
// classic
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
break;
case STYLE.ABOVE:
// classic
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
break;
case STYLE.BLEW:
// blew
headerTop = paddingTop + lp.topMargin;
break;
case STYLE.SCALE:
// scale
headerTop = paddingTop + lp.topMargin - mHeaderHeight / 2 + mHeaderOffset / 2;
break;
case STYLE.BLEW2CLASSIC:
// blew2classic
if (mHeaderOffset > mHeaderHeight) {
headerTop = paddingTop + lp.topMargin;
}
else {
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
}
break;
default:
// classic
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
break;
}
final int headerRight = headerLeft + headerView.getMeasuredWidth();
final int headerBottom = headerTop + headerView.getMeasuredHeight();
headerView.layout(headerLeft, headerTop, headerRight, headerBottom);
}
// layout target
if (mTargetView != null) {
final View targetView = mTargetView;
MarginLayoutParams lp = (MarginLayoutParams) targetView.getLayoutParams();
final int targetLeft = paddingLeft + lp.leftMargin;
final int targetTop;
switch (mStyle) {
case STYLE.CLASSIC:
// classic
targetTop = paddingTop + lp.topMargin + mTargetOffset;
break;
case STYLE.ABOVE:
// above
targetTop = paddingTop + lp.topMargin;
break;
case STYLE.BLEW:
// classic
targetTop = paddingTop + lp.topMargin + mTargetOffset;
break;
case STYLE.SCALE:
// classic
targetTop = paddingTop + lp.topMargin + mTargetOffset;
break;
case STYLE.BLEW2CLASSIC:
// classic
targetTop = paddingTop + lp.topMargin + mTargetOffset;
break;
default:
// classic
targetTop = paddingTop + lp.topMargin + mTargetOffset;
break;
}
final int targetRight = targetLeft + targetView.getMeasuredWidth();
final int targetBottom = targetTop + targetView.getMeasuredHeight();
targetView.layout(targetLeft, targetTop, targetRight, targetBottom);
}
// layout footer
if (mFooterView != null) {
final View footerView = mFooterView;
MarginLayoutParams lp = (MarginLayoutParams) footerView.getLayoutParams();
final int footerLeft = paddingLeft + lp.leftMargin;
final int footerBottom;
switch (mStyle) {
case STYLE.CLASSIC:
// classic
footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
break;
case STYLE.ABOVE:
// classic
footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
break;
case STYLE.BLEW:
// blew
footerBottom = height - paddingBottom - lp.bottomMargin;
break;
case STYLE.SCALE:
// scale
footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight / 2 + mFooterOffset / 2;
break;
case STYLE.BLEW2CLASSIC:
// blew2classic
if (mFooterOffset > mFooterHeight) {
footerBottom = height - paddingBottom - lp.bottomMargin;
}
else {
footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
}
break;
default:
// classic
footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
break;
}
final int footerTop = footerBottom - footerView.getMeasuredHeight();
final int footerRight = footerLeft + footerView.getMeasuredWidth();
footerView.layout(footerLeft, footerTop, footerRight, footerBottom);
}
if (mStyle == STYLE.CLASSIC
|| mStyle == STYLE.ABOVE) {
if (mHeaderView != null) {
mHeaderView.bringToFront();
}
if (mFooterView != null) {
mFooterView.bringToFront();
}
} else if (mStyle == STYLE.BLEW || mStyle == STYLE.SCALE || mStyle == STYLE.BLEW2CLASSIC) {
if (mTargetView != null) {
mTargetView.bringToFront();
}
}
}
以下拉刷新为例,看这行代码。
paddingTop与lp.topMargin都是0,mHeaderHeight是Header的高度,mHeaderOffset就是手指滑动的距离(这个稍后会有说明)。在下拉过程中,mHeaderOffset的值会越来越大,所以headerTop的值是从-mHeaderHeight开始逐渐增大的,所以headerView会向下逐步移动
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset
而Target更为简单,你手指滑动多少它就跟着滑动多少
targetTop = paddingTop + lp.topMargin + mTargetOffset;
这样能够想象出饿了么滑动到mHeaderHeight高度之后如何处理的吧,请参考我自己定义的style--BLEW2CLASSIC
if (mHeaderOffset > mHeaderHeight) {
headerTop = paddingTop + lp.topMargin;
}
else {
headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
}
继续往下来到事件分发部分了
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
final int action = MotionEventCompat.getActionMasked(ev);
switch (action) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// swipeToRefresh -> finger up -> finger down if the status is still swipeToRefresh
// in onInterceptTouchEvent ACTION_DOWN event will stop the scroller
// if the event pass to the child view while ACTION_MOVE(condition is false)
// in onInterceptTouchEvent ACTION_MOVE the ACTION_UP or ACTION_CANCEL will not be
// passed to onInterceptTouchEvent and onTouchEvent. Instead It will be passed to
// child view's onTouchEvent. So we must deal this situation in dispatchTouchEvent
onActivePointerUp();
break;
}
return super.dispatchTouchEvent(ev);
}
获取事件之后,在手指释放的时候执行onActivePointerUp(),咱们来看看。分别判断了当前是处在下拉以刷新、上拉以加载更多、松开以刷新、松开以加载更多,然后滚动到响应的位置上去。注意在松开状态时,执行了onRelease()回调
private void onActivePointerUp() {
if (STATUS.isSwipingToRefresh(mStatus)) {
// simply return
scrollSwipingToRefreshToDefault();
} else if (STATUS.isSwipingToLoadMore(mStatus)) {
// simply return
scrollSwipingToLoadMoreToDefault();
} else if (STATUS.isReleaseToRefresh(mStatus)) {
// return to header height and perform refresh
mRefreshCallback.onRelease();
scrollReleaseToRefreshToRefreshing();
} else if (STATUS.isReleaseToLoadMore(mStatus)) {
// return to footer height and perform loadMore
mLoadMoreCallback.onRelease();
scrollReleaseToLoadMoreToLoadingMore();
}
}
随后就是事件拦截的判断。只要你向下滑动时Target确实不能再向下移动了或者向上滑动时Target确实不能再向上移动了,那么SwipeRefreshLayout就把事件拦截,执行onTouchEvent里面的位移操作了
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
final int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
// if it isn't an ing status or default status
if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) ||
STATUS.isReleaseToRefresh(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
// abort autoScrolling, not trigger the method #autoScrollFinished()
mAutoScroller.abortIfRunning();
if (mDebug) {
Log.i(TAG, "Another finger down, abort auto scrolling, let the new finger handle");
}
}
if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)
|| STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
return true;
}
// let children view handle the ACTION_DOWN;
// 1\. children consumed:
// if at least one of children onTouchEvent() ACTION_DOWN return true.
// ACTION_DOWN event will not return to SwipeToLoadLayout#onTouchEvent().
// but the others action can be handled by SwipeToLoadLayout#onInterceptTouchEvent()
// 2\. children not consumed:
// if children onTouchEvent() ACTION_DOWN return false.
// ACTION_DOWN event will return to SwipeToLoadLayout's onTouchEvent().
// SwipeToLoadLayout#onTouchEvent() ACTION_DOWN return true to consume the ACTION_DOWN event.
// anyway: handle action down in onInterceptTouchEvent() to init is an good option
break;
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_POINTER) {
return false;
}
float y = getMotionEventY(event, mActivePointerId);
float x = getMotionEventX(event, mActivePointerId);
final float yInitDiff = y - mInitDownY;
final float xInitDiff = x - mInitDownX;
mLastY = y;
mLastX = x;
boolean moved = Math.abs(yInitDiff) > Math.abs(xInitDiff)
&& Math.abs(yInitDiff) > mTouchSlop;
boolean triggerCondition =
// refresh trigger condition
(yInitDiff > 0 && moved && onCheckCanRefresh()) ||
//load more trigger condition
(yInitDiff < 0 && moved && onCheckCanLoadMore());
if (triggerCondition) {
// if the refresh's or load more's trigger condition is true,
// intercept the move action event and pass it to SwipeToLoadLayout#onTouchEvent()
return true;
}
break;
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(event);
mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_POINTER;
break;
}
return super.onInterceptTouchEvent(event);
}
下面就是位移过程。
如果当期处于初始STATUS_DEFAULT状态,则进入STATUS_SWIPING_TO_REFRESH,同时回调onPrepare()方法
如果在下拉刷新流程中向上滑动并且滑动偏移量小于0,为了不让Target部分移动到屏幕之外,则将体系流程恢复到初始STATUS_DEFAULT状态,同时使用fixCurrentStatusLayout()方法调整三个View的位置。上拉加载更多流程同理
在正常下拉刷新流程中,如果当期状态是STATUS_SWIPING_TO_REFRESH或者是STATUS_RELEASE_TO_REFRESH,即处于下拉以刷新、松开以刷新状态,如果下拉的距离超过mRefreshTriggerOffset,则进入松开以刷新状态,反之则进入下拉以刷新状态。上拉加载更多流程同理
这时候会触发位移发生fingerScroll()
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = MotionEventCompat.getActionMasked(event);
switch (action) {
case MotionEvent.ACTION_DOWN:
mActivePointerId = MotionEventCompat.getPointerId(event, 0);
return true;
case MotionEvent.ACTION_MOVE:
// take over the ACTION_MOVE event from SwipeToLoadLayout#onInterceptTouchEvent()
// if condition is true
final float y = getMotionEventY(event, mActivePointerId);
final float x = getMotionEventX(event, mActivePointerId);
final float yDiff = y - mLastY;
final float xDiff = x - mLastX;
mLastY = y;
mLastX = x;
if (Math.abs(xDiff) > Math.abs(yDiff) && Math.abs(xDiff) > mTouchSlop) {
return true;
}
if (STATUS.isStatusDefault(mStatus)) {
if (yDiff > 0 && onCheckCanRefresh()) {
mRefreshCallback.onPrepare();
setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
} else if (yDiff < 0 && onCheckCanLoadMore()) {
mLoadMoreCallback.onPrepare();
setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
}
} else if (STATUS.isRefreshStatus(mStatus)) {
if (mTargetOffset <= 0) {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
return true;
}
} else if (STATUS.isLoadMoreStatus(mStatus)) {
if (mTargetOffset >= 0) {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
return true;
}
}
if (STATUS.isRefreshStatus(mStatus)) {
if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)) {
if (mTargetOffset >= mRefreshTriggerOffset) {
setStatus(STATUS.STATUS_RELEASE_TO_REFRESH);
} else {
setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
}
fingerScroll(yDiff);
}
} else if (STATUS.isLoadMoreStatus(mStatus)) {
if (STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
if (-mTargetOffset >= mLoadMoreTriggerOffset) {
setStatus(STATUS.STATUS_RELEASE_TO_LOAD_MORE);
} else {
setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
}
fingerScroll(yDiff);
}
}
return true;
case MotionEvent.ACTION_POINTER_DOWN: {
final int pointerIndex = MotionEventCompat.getActionIndex(event);
final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex);
if (pointerId != INVALID_POINTER) {
mActivePointerId = pointerId;
}
mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
break;
}
case MotionEvent.ACTION_POINTER_UP: {
onSecondaryPointerUp(event);
mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mActivePointerId == INVALID_POINTER) {
return false;
}
mActivePointerId = INVALID_POINTER;
break;
default:
break;
}
return super.onTouchEvent(event);
}
位移无非就是对mTargetOffset进行赋值,同时调整三个View的位置。注意这里调用了onMove()回调
private void fingerScroll(final float yDiff) {
float ratio = mDragRatio;
float yScrolled = yDiff * ratio;
// make sure (targetOffset>0 -> targetOffset=0 -> default status)
// or (targetOffset<0 -> targetOffset=0 -> default status)
// forbidden fling (targetOffset>0 -> targetOffset=0 ->targetOffset<0 -> default status)
// or (targetOffset<0 -> targetOffset=0 ->targetOffset>0 -> default status)
// I am so smart :)
float tmpTargetOffset = yScrolled + mTargetOffset;
if ((tmpTargetOffset > 0 && mTargetOffset < 0)
|| (tmpTargetOffset < 0 && mTargetOffset > 0)) {
yScrolled = -mTargetOffset;
}
if (mRefreshFinalDragOffset >= mRefreshTriggerOffset && tmpTargetOffset > mRefreshFinalDragOffset) {
yScrolled = mRefreshFinalDragOffset - mTargetOffset;
} else if (mLoadMoreFinalDragOffset >= mLoadMoreTriggerOffset && -tmpTargetOffset > mLoadMoreFinalDragOffset) {
yScrolled = -mLoadMoreFinalDragOffset - mTargetOffset;
}
if (STATUS.isRefreshStatus(mStatus)) {
mRefreshCallback.onMove(mTargetOffset, false, false);
} else if (STATUS.isLoadMoreStatus(mStatus)) {
mLoadMoreCallback.onMove(mTargetOffset, false, false);
}
updateScroll(yScrolled);
}
private void updateScroll(final float yScrolled) {
if (yScrolled == 0) {
return;
}
mTargetOffset += yScrolled;
if (STATUS.isRefreshStatus(mStatus)) {
mHeaderOffset = mTargetOffset;
mFooterOffset = 0;
} else if (STATUS.isLoadMoreStatus(mStatus)) {
mFooterOffset = mTargetOffset;
mHeaderOffset = 0;
}
if (mDebug) {
Log.i(TAG, "mTargetOffset = " + mTargetOffset);
}
layoutChildren();
invalidate();
}
最后就是执行结束刷新操作,完成闭环。结束的时候,refreshing值为false,执行onComplete()回调,同时回滚到初始位置
public void setRefreshing(boolean refreshing) {
if (!isRefreshEnabled() || mHeaderView == null) {
return;
}
this.mAutoLoading = refreshing;
if (refreshing) {
if (STATUS.isStatusDefault(mStatus)) {
setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
scrollDefaultToRefreshing();
}
} else {
if (STATUS.isRefreshing(mStatus)) {
mRefreshCallback.onComplete();
postDelayed(new Runnable() {
@Override
public void run() {
scrollRefreshingToDefault();
}
}, mRefreshCompleteDelayDuration);
}
}
}
这里还有一个补充,关于自动滑动方面。自动滚动一般都是通过AutoScroller类,调用其autoScroll()方法来完成,而实际上也是调用Scroller.startScroll()。但是不知道你有没有注意到post(this),它在反复调用这个Runnable的run()来判断滑动是否已经结束。如果没有结束,则通过autoScroll()方法来调用move()回调;如果已经结束,则通过autoScrollFinished()方法来判断下一步应该到达何种状态
private class AutoScroller implements Runnable {
private Scroller mScroller;
private int mmLastY;
private boolean mRunning = false;
private boolean mAbort = false;
public AutoScroller() {
mScroller = new Scroller(getContext());
}
@Override
public void run() {
boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished();
int currY = mScroller.getCurrY();
int yDiff = currY - mmLastY;
if (finish) {
finish();
} else {
mmLastY = currY;
SwipeToLoadLayout.this.autoScroll(yDiff);
post(this);
}
}
/**
* remove the post callbacks and reset default values
*/
private void finish() {
mmLastY = 0;
mRunning = false;
removeCallbacks(this);
// if abort by user, don't call
if (!mAbort) {
autoScrollFinished();
}
}
/**
* abort scroll if it is scrolling
*/
public void abortIfRunning() {
if (mRunning) {
if (!mScroller.isFinished()) {
mAbort = true;
mScroller.forceFinished(true);
}
finish();
mAbort = false;
}
}
/**
* The param yScrolled here isn't final pos of y.
* It's just like the yScrolled param in the
* {@link #updateScroll(float yScrolled)}
*
* @param yScrolled
* @param duration
*/
private void autoScroll(int yScrolled, int duration) {
removeCallbacks(this);
mmLastY = 0;
if (!mScroller.isFinished()) {
mScroller.forceFinished(true);
}
mScroller.startScroll(0, 0, 0, yScrolled, duration);
post(this);
mRunning = true;
}
}
如果是松开以刷新,则进入刷新状态,同时回调onRefresh()方法
如果是正在刷新状态,则复原,执行onReset()方法
如果是松开以刷新并且通过setRefresh(true)方法进来的,则进入正在刷新状态,执行onRefresh()方法;反之则执行复原操作,执行onReset()方法。
上拉加载更多流程同理
private void autoScrollFinished() {
int mLastStatus = mStatus;
if (STATUS.isReleaseToRefresh(mStatus)) {
setStatus(STATUS.STATUS_REFRESHING);
fixCurrentStatusLayout();
mRefreshCallback.onRefresh();
} else if (STATUS.isRefreshing(mStatus)) {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
mRefreshCallback.onReset();
} else if (STATUS.isSwipingToRefresh(mStatus)) {
if (mAutoLoading) {
mAutoLoading = false;
setStatus(STATUS.STATUS_REFRESHING);
fixCurrentStatusLayout();
mRefreshCallback.onRefresh();
} else {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
mRefreshCallback.onReset();
}
} else if (STATUS.isStatusDefault(mStatus)) {
} else if (STATUS.isSwipingToLoadMore(mStatus)) {
if (mAutoLoading) {
mAutoLoading = false;
setStatus(STATUS.STATUS_LOADING_MORE);
fixCurrentStatusLayout();
mLoadMoreCallback.onLoadMore();
} else {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
mLoadMoreCallback.onReset();
}
} else if (STATUS.isLoadingMore(mStatus)) {
setStatus(STATUS.STATUS_DEFAULT);
fixCurrentStatusLayout();
mLoadMoreCallback.onReset();
} else if (STATUS.isReleaseToLoadMore(mStatus)) {
setStatus(STATUS.STATUS_LOADING_MORE);
fixCurrentStatusLayout();
mLoadMoreCallback.onLoadMore();
} else {
throw new IllegalStateException("illegal state: " + STATUS.getStatus(mStatus));
}
if (mDebug) {
Log.i(TAG, STATUS.getStatus(mLastStatus) + " -> " + STATUS.getStatus(mStatus));
}
}
源码分析到此结束。怎么样,是不是很简单
链接:https://www.jianshu.com/p/fc8c73db72b3
更多文章
相信自己,没有做不到的,只有想不到的
如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :644196190
微信公众号:终端研发部
SwipeRefreshLayout,用最少的代码定制最美的上下拉刷新样式的更多相关文章
- 代码实现Android5.0的下拉刷新效果
如图所示,实现类似与gmail的下拉刷新. 项目地址:https://github.com/stormzhang/SwipeRefreshLayoutDemo 一.在xml文件中定义 这个控件在sup ...
- Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理
RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用 和 Android 5.X新特性之为RecyclerView添加Header ...
- Android如何定制一个下拉刷新,上滑加载更多的容器
前言 下拉刷新和上滑加载更多,是一种比较常用的列表数据交互方式. android提供了原生的下拉刷新容器 SwipeRefreshLayout,可惜样式不能定制. 于是打算自己实现一个专用的.但是下拉 ...
- Android SwipeRefreshLayout 下拉刷新——Hi_博客 Android App 开发笔记
以前写下拉刷新 感觉好费劲,要判断ListView是否滚到顶部,还要加载头布局,还要控制 头布局的状态,等等一大堆.感觉麻烦死了.今天学习了SwipeRefreshLayout 的用法,来分享一下,有 ...
- android官方下拉刷新控件SwipeRefreshLayout的使用
可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介:SwipeRefreshLayout组件只 ...
- android SwipeRefreshLayout google官方下拉刷新控件
下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...
- Google自己的下拉刷新组件SwipeRefreshLayout
SwipeRefreshLayout SwipeRefreshLayout字面意思就是下拉刷新的布局,继承自ViewGroup,在support v4兼容包下,但必须把你的support librar ...
- SwipeRefreshLayout下拉刷新
1.SwipeRefreshLayout是Google在support v4 19.1版本的library更新的一个下拉刷新组件,实现刷新效果更方便. 弊端:只有下拉 //设置刷新控件圈圈的颜色 sw ...
- Android 编程下如何调整 SwipeRefreshLayout 的下拉刷新距离
SwipeRefreshLayout 的下拉刷新距离比较短,并且也没有提供设置下拉距离的 API,但是看 SwipeRefreshLayout 的源码,会发现有一个内部变量 mDistanceToTr ...
随机推荐
- Linux或UNIX系统配置检查
1. Linux或UNIX系统配置检查 系统配置的扫描是基于被动式策略进行扫描,主要检测主机上是否存在配置错误或者不符合预定义的安全策略的配置,通常需要管理员权限才能执行的扫描. 在Linux或UNI ...
- bean的装配方式(注入方式,构造注入,setter属性注入)
bean的装配方式有两种,构造注入和setter属性注入. public class User { private String username; private String password; ...
- Restful API学习Day5 - DRF之限制 分页 解析器和渲染器
参考文档: Django REST framework基础:认证.权限.限制 Django REST framework基础:分页 Django REST framework基础:解析器和渲染器 一. ...
- luogu P2520 [HAOI2011]向量
传送门 一堆人说数论只会gcd,我连gcd都不会,菜死算了qwq Orzyyb 这题欺负我数学不好qwq 首先可以发现实际上有如下操作:x或y±2a,x或y±2b,x+a y+b,x+b y+a(后面 ...
- luogu P2144 [FJOI2007]轮状病毒
传送门 随便摸一发题解算了 打表找规律 前五个答案是 1 5 16 45 121 其实是 1^2 3^2-4 4^2 7^2-4 11^2 底数就是类似于斐波那契数列,还有偶数项要减4 #includ ...
- mysql 8.0 ~ innodb与变量优化
一 innodb的优化 1 已完全不支持myisam引擎 2 将自增主键的计数器持久化到redo log中.每次计数器发生改变,都会将其写入到redo log中.如果数据库发生重启,InnoDB ...
- 工具方法 .js
1. 获取url问号后面,name的值 /** * *?id=123&a=b * @return object */export function urlParse(){ let url = ...
- 第一次发博,发个简单的Java程序发送手机短信验证
最近在准备一个项目,想的登录时候用手机验证,就通过上网查阅了一下手机验证的实现方法,原来超级简单,下面将一步一步介绍. 1.去中国网建注册一个账号密码,首次注册送五条免费短信和3条免费彩信.具体的网址 ...
- protobuf 安装与卸载
方法一:可以FQ 安装 下载https://github.com/google/protobuf/releases ##Source code (zip)## ./autogen.sh ./confi ...
- git-bisect last updated in 2.19.1【转】
转自:https://git-scm.com/docs/git-bisect NAME git-bisect - Use binary search to find the commit that i ...