下拉刷新框架其实有很多,而且质量都比较高。但是在日常开发中,每一款产品都会有一套自己独特的一套刷新样式。相信有很多小伙伴在个性化定制中都或多或少的遇到过麻烦。今天我就给大家推荐一个在定制方面很出彩的一个刷新框架SwipeToLoadLayout,该框架自身完成了下拉刷新与上拉加载功能,同时将顶部视图与底部视图的UI定制功能通过接口很方便的提供给使用者自行定义。
相关代码已经上传到github上,欢迎star、fork

基本流程

先简单了解一下SwipeToLoadLayout的使用流程,以下拉刷新为例:

  1. 完成Header部分,实现SwipeRefreshTrigger与SwipeRefreshTrigger接口
  2. 完成activity或fragment的布局,在SwipeToLoadLayout节点下配置好Header与下拉目标组件(如RecyclerView等)

这里还是要稍微说一下,因为这个布局过程还是有一定的规则的
首先布局的id是固定的,这个我们在ids.xml中就能看出。框架提供三个View:Header、Target、Footer,分别对应三个位置的View

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <resources>
  3. <item name="swipe_target" type="id" />
  4. <item name="swipe_refresh_header" type="id" />
  5. <item name="swipe_load_more_footer" type="id" />
  6. </resources>

其次onFinishInflate()方法告诉我们,最多只能同时存在这三个View,不能有更多的子View了

  1. @Override
  2. protected void onFinishInflate() {
  3. super.onFinishInflate();
  4. final int childNum = getChildCount();
  5. if (childNum == 0) {
  6. // no child return
  7. return;
  8. } else if (0 < childNum && childNum < 4) {
  9. mHeaderView = findViewById(R.id.swipe_refresh_header);
  10. mTargetView = findViewById(R.id.swipe_target);
  11. mFooterView = findViewById(R.id.swipe_load_more_footer);
  12. } else {
  13. // more than three children: unsupported!
  14. throw new IllegalStateException("Children num must equal or less than 3");
  15. }
  16. if (mTargetView == null) {
  17. return;
  18. }
  19. if (mHeaderView != null && mHeaderView instanceof SwipeTrigger) {
  20. mHeaderView.setVisibility(GONE);
  21. }
  22. if (mFooterView != null && mFooterView instanceof SwipeTrigger) {
  23. mFooterView.setVisibility(GONE);
  24. }
  25. }

这样你就能得出下一步该怎么来实现了吧?没错肯定是这样的

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.aspsine.swipetoloadlayout.SwipeToLoadLayout >
  3. <View
  4. android:id="@id/swipe_refresh_header" />
  5. <android.support.v7.widget.RecyclerView
  6. android:id="@id/swipe_target" />
  7. <View
  8. android:id="@id/swipe_load_more_footer" />
  9. </com.aspsine.swipetoloadlayout.SwipeToLoadLayout>

Header的部分尤为重要。我们需在Header上实现SwipeTrigger与SwipeRefreshTrigger接口,接口中的方法分别对应滑动刷新在各个状态下的回调。它们分别为
onPrepare:代表下拉刷新开始的状态
onMove:代表正在滑动过程中的状态
onRelease:代表手指松开后,下拉刷新进入松开刷新的状态
onComplete:代表下拉刷新完成的状态
onReset:代表下拉刷新重置恢复的状态
onRefresh:代表正在刷新中的状态
有了这几个接口,我们就可以完成Header部分的任何动画效果了。当然上拉加载更多的场景,只是把SwipeRefreshTrigger接口换成SwipeLoadMoreTrigger接口而已,其他跟下拉刷新情况完全相同

  1. 在activity或fragment中配置下拉监听事件,并在数据获取完成后主动触发刷新swipeToLoadLayout.setRefreshing(false);完成功能

更深入的部分我们放到源码分析里面再说

看起来好像很简单,那么我们就通过几个小Demo了解一下如何使用吧

仿新浪微博

之所以第一个范例选择新浪微博,是因为它是最传统刷新风格:根据箭头和文字的不同来表明当前不同的状态

 

如果你在早期研究过PullToRefresh,那么很容易在这个框架基础上实现相应的视图更新功能

先完成头部的定义。WeiboRefreshHeaderView作为头,其实际为一个LinearLayout

  1. class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger

头部布局很简单

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent" android:layout_height="60dip"
  4. android:gravity="center"
  5. android:orientation="horizontal">
  6. <RelativeLayout
  7. android:layout_width="wrap_content"
  8. android:layout_height="wrap_content">
  9. <ProgressBar
  10. android:id="@+id/pb_weibo"
  11. style="?android:attr/progressBarStyleSmallInverse"
  12. android:layout_centerInParent="true"
  13. android:visibility="gone"
  14. android:layout_width="wrap_content"
  15. android:layout_height="wrap_content" />
  16. <ImageView
  17. android:id="@+id/iv_weibo"
  18. android:src="@mipmap/tableview_pull_refresh_arrow_down"
  19. android:layout_centerInParent="true"
  20. android:layout_width="wrap_content"
  21. android:layout_height="wrap_content" />
  22. </RelativeLayout>
  23. <TextView
  24. android:id="@+id/tv_weibo"
  25. android:layout_marginStart="10dip"
  26. android:layout_width="wrap_content"
  27. android:layout_height="wrap_content"
  28. android:text="下拉刷新"/>
  29. </com.renyu.swipetoloadlayoutdemo.view.WeiboRefreshHeaderView>

activity的布局也很简单,把头跟身子一起加在SwipeToLoadLayout里

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.aspsine.swipetoloadlayout.SwipeToLoadLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:id="@+id/swipe_weibo">
  6. <include
  7. layout="@layout/header_weibo"
  8. android:id="@id/swipe_refresh_header" />
  9. <TextView
  10. android:id="@id/swipe_target"
  11. android:layout_width="match_parent"
  12. android:layout_height="match_parent"
  13. android:gravity="center"
  14. android:text="下拉刷新"/>
  15. </com.aspsine.swipetoloadlayout.SwipeToLoadLayout>

下面就是完成头部动画效果了。新浪微博的这个效果就是视图被下拉到头部高度之后,将箭头位置旋转一下同时更换文字,刷新时展现progressbar即可

  1. class WeiboRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
  2. var pb_weibo: ProgressBar? = null
  3. var iv_weibo: ImageView? = null
  4. var tv_weibo: TextView? = null
  5. // 是否发生旋转
  6. var rotated = false
  7. private val rotate_up: Animation by lazy {
  8. AnimationUtils.loadAnimation(context, R.anim.rotate_up)
  9. }
  10. private val rotate_down: Animation by lazy {
  11. AnimationUtils.loadAnimation(context, R.anim.rotate_down)
  12. }
  13. constructor(context: Context) : super(context)
  14. constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
  15. constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
  16. override fun onFinishInflate() {
  17. super.onFinishInflate()
  18. pb_weibo = findViewById(R.id.pb_weibo)
  19. iv_weibo = findViewById(R.id.iv_weibo)
  20. tv_weibo = findViewById(R.id.tv_weibo)
  21. }
  22. override fun onReset() {
  23. pb_weibo?.visibility = View.GONE
  24. iv_weibo?.visibility = View.VISIBLE
  25. tv_weibo?.text = "下拉刷新"
  26. }
  27. override fun onComplete() {
  28. tv_weibo?.text = "刷新完成"
  29. pb_weibo?.visibility = View.GONE
  30. }
  31. override fun onRelease() {
  32. }
  33. override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
  34. if (p0 > SizeUtils.dp2px(60f)) {
  35. if (!rotated) {
  36. rotated = true
  37. tv_weibo?.text = "释放更新"
  38. iv_weibo?.clearAnimation()
  39. iv_weibo?.startAnimation(rotate_up)
  40. }
  41. }
  42. else {
  43. if (rotated) {
  44. rotated = false
  45. tv_weibo?.text = "下拉刷新"
  46. iv_weibo?.clearAnimation()
  47. iv_weibo?.startAnimation(rotate_down)
  48. }
  49. }
  50. }
  51. override fun onPrepare() {
  52. }
  53. override fun onRefresh() {
  54. tv_weibo?.text = "加载中"
  55. iv_weibo?.clearAnimation()
  56. iv_weibo?.visibility = View.GONE
  57. pb_weibo?.visibility = View.VISIBLE
  58. }
  59. }

 

对照一下上文的刷新周期,应该很好理解

美团外卖

美团外卖是利用ImageView直接播放一段animation直到刷新完成停止。在下拉过程中,该ImageView随着位移的距离变化而发生相应的大小变化

 

美团外卖动画效果是由一系列的图片组成的,所以与新浪微博效果相比更为简单一些

 

一样要完成头部视图的定义

  1. class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:layout_width="match_parent" android:layout_height="wrap_content"
  4. android:gravity="center"
  5. android:padding="10dip">
  6. <ImageView
  7. android:id="@+id/iv_mt"
  8. android:layout_width="112dp"
  9. android:layout_height="44dp"
  10. android:background="@drawable/animation_list_refresh_mt"
  11. android:transformPivotX="56dp"
  12. android:transformPivotY="22dp"
  13. android:scaleY="0.3"
  14. android:scaleX="0.3"/>
  15. </com.renyu.swipetoloadlayoutdemo.view.MTRefreshHeaderView>

剩下就是完成动画的播放与缩放的处理了

  1. class MTRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
  2. var iv_mt: ImageView? = null
  3. val animationDrawable: AnimationDrawable by lazy {
  4. iv_mt?.background as AnimationDrawable
  5. }
  6. constructor(context: Context) : super(context)
  7. constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
  8. constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
  9. override fun onFinishInflate() {
  10. super.onFinishInflate()
  11. iv_mt = findViewById(R.id.iv_mt)
  12. }
  13. override fun onReset() {
  14. }
  15. override fun onComplete() {
  16. animationDrawable.stop()
  17. }
  18. override fun onRelease() {
  19. }
  20. override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
  21. val percent = if (p0 * 1.0f / SizeUtils.dp2px(44f) > 1) 1f else p0 * 1.0f / SizeUtils.dp2px(44f)
  22. iv_mt?.scaleY = (0.3f + 0.7 * percent).toFloat()
  23. iv_mt?.scaleX = (0.3f + 0.7 * percent).toFloat()
  24. }
  25. override fun onPrepare() {
  26. if (!animationDrawable.isRunning) {
  27. animationDrawable.start()
  28. }
  29. iv_mt?.scaleY = 0.3f
  30. iv_mt?.scaleX = 0.3f
  31. }
  32. override fun onRefresh() {
  33. if (!animationDrawable.isRunning) {
  34. animationDrawable.start()
  35. }
  36. iv_mt?.scaleY = 1f
  37. iv_mt?.scaleX = 1f
  38. }
  39. }

 

代码都很简单,很容易理解

饿了么

饿了么的效果是通过SVG来实现的

 

饿了么app对资源进行了混淆,所以我拿不到图片,只能随便从其他地方找一个了

一样是Header的编写,这里面有一点不同,我用android-pathview这个开源框架实现SVG播放进度控制功能

我需要将这个动画效果在下拉刷新的过程中实现

image
  1. class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:orientation="vertical" android:layout_width="match_parent"
  4. android:layout_height="wrap_content"
  5. android:gravity="center">
  6. <com.eftimoff.androipathview.PathView
  7. xmlns:app="http://schemas.android.com/apk/res-auto"
  8. android:id="@+id/pathView_ele"
  9. android:layout_width="58dp"
  10. android:layout_height="58dp"
  11. app:pathColor="@android:color/black"
  12. app:svg="@raw/issues"
  13. app:pathWidth="2dp"/>
  14. </com.renyu.swipetoloadlayoutdemo.view.ElemeRefreshHeaderView>

下面就是根据滑动偏移量来处理SVG播放的进度

  1. class ElemeRefreshHeaderView : LinearLayout, SwipeTrigger, SwipeRefreshTrigger {
  2. var pathView_ele: PathView? = null
  3. constructor(context: Context) : super(context)
  4. constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
  5. constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
  6. override fun onFinishInflate() {
  7. super.onFinishInflate()
  8. pathView_ele = findViewById(R.id.pathView_ele)
  9. }
  10. override fun onReset() {
  11. }
  12. override fun onComplete() {
  13. pathView_ele?.setPercentage(1f)
  14. }
  15. override fun onRelease() {
  16. }
  17. override fun onMove(p0: Int, p1: Boolean, p2: Boolean) {
  18. val percent = 1 - (SizeUtils.dp2px(58f) - p0) * 1.0f / SizeUtils.dp2px(58f)
  19. val value = if (percent >= 1) 1f else percent
  20. pathView_ele?.setPercentage(value)
  21. }
  22. override fun onPrepare() {
  23. pathView_ele?.setPercentage(0f)
  24. }
  25. override fun onRefresh() {
  26. pathView_ele?.setPercentage(1f)
  27. }
  28. }

 

这里你会发出一个疑问,怎么效果与饿了么有的差距?饿了么是滑动到Header完成展开之后就不再继续下滑了,那咱们这个怎么实现呢?那我只能说不好意思,在现有条件下咱们实现不了,只能通过改源码完成

那我们就顺带来阅读源码,看看这个地方怎么改进吧?

源码分析

之前的onFinishInflate咱们就不说了,那个就是告诉我们只能有三个View,分别是Header、Target、Footer

然后是测量阶段,在测量阶段可以得到两个重要的变量mHeaderHeight与mFooterHeight,他们分别代表Header与Footer的高度。同时如果定义的mRefreshTriggerOffset(松开刷新的高度)比Header或Footer的高度小,则修正这个刷新位置

  1. @Override
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  4. // header
  5. if (mHeaderView != null) {
  6. final View headerView = mHeaderView;
  7. measureChildWithMargins(headerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
  8. MarginLayoutParams lp = ((MarginLayoutParams) headerView.getLayoutParams());
  9. mHeaderHeight = headerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
  10. if (mRefreshTriggerOffset < mHeaderHeight) {
  11. mRefreshTriggerOffset = mHeaderHeight;
  12. }
  13. }
  14. // target
  15. if (mTargetView != null) {
  16. final View targetView = mTargetView;
  17. measureChildWithMargins(targetView, widthMeasureSpec, 0, heightMeasureSpec, 0);
  18. }
  19. // footer
  20. if (mFooterView != null) {
  21. final View footerView = mFooterView;
  22. measureChildWithMargins(footerView, widthMeasureSpec, 0, heightMeasureSpec, 0);
  23. MarginLayoutParams lp = ((MarginLayoutParams) footerView.getLayoutParams());
  24. mFooterHeight = footerView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
  25. if (mLoadMoreTriggerOffset < mFooterHeight) {
  26. mLoadMoreTriggerOffset = mFooterHeight;
  27. }
  28. }
  29. }

在onLayout中对三个视图进行布局

  1. @Override
  2. protected void onLayout(boolean changed, int l, int t, int r, int b) {
  3. layoutChildren();
  4. mHasHeaderView = (mHeaderView != null);
  5. mHasFooterView = (mFooterView != null);
  6. }

这里有一个重要的方法layoutChildren,这个方法就是改变三个视图的位置的。当然这个位置要根据不同的类型来处理,默认情况下我们都是STYLE.CLASSIC类型。

  1. private void layoutChildren() {
  2. final int width = getMeasuredWidth();
  3. final int height = getMeasuredHeight();
  4. final int paddingLeft = getPaddingLeft();
  5. final int paddingTop = getPaddingTop();
  6. final int paddingRight = getPaddingRight();
  7. final int paddingBottom = getPaddingBottom();
  8. if (mTargetView == null) {
  9. return;
  10. }
  11. // layout header
  12. if (mHeaderView != null) {
  13. final View headerView = mHeaderView;
  14. MarginLayoutParams lp = (MarginLayoutParams) headerView.getLayoutParams();
  15. final int headerLeft = paddingLeft + lp.leftMargin;
  16. final int headerTop;
  17. switch (mStyle) {
  18. case STYLE.CLASSIC:
  19. // classic
  20. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
  21. break;
  22. case STYLE.ABOVE:
  23. // classic
  24. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
  25. break;
  26. case STYLE.BLEW:
  27. // blew
  28. headerTop = paddingTop + lp.topMargin;
  29. break;
  30. case STYLE.SCALE:
  31. // scale
  32. headerTop = paddingTop + lp.topMargin - mHeaderHeight / 2 + mHeaderOffset / 2;
  33. break;
  34. case STYLE.BLEW2CLASSIC:
  35. // blew2classic
  36. if (mHeaderOffset > mHeaderHeight) {
  37. headerTop = paddingTop + lp.topMargin;
  38. }
  39. else {
  40. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
  41. }
  42. break;
  43. default:
  44. // classic
  45. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
  46. break;
  47. }
  48. final int headerRight = headerLeft + headerView.getMeasuredWidth();
  49. final int headerBottom = headerTop + headerView.getMeasuredHeight();
  50. headerView.layout(headerLeft, headerTop, headerRight, headerBottom);
  51. }
  52. // layout target
  53. if (mTargetView != null) {
  54. final View targetView = mTargetView;
  55. MarginLayoutParams lp = (MarginLayoutParams) targetView.getLayoutParams();
  56. final int targetLeft = paddingLeft + lp.leftMargin;
  57. final int targetTop;
  58. switch (mStyle) {
  59. case STYLE.CLASSIC:
  60. // classic
  61. targetTop = paddingTop + lp.topMargin + mTargetOffset;
  62. break;
  63. case STYLE.ABOVE:
  64. // above
  65. targetTop = paddingTop + lp.topMargin;
  66. break;
  67. case STYLE.BLEW:
  68. // classic
  69. targetTop = paddingTop + lp.topMargin + mTargetOffset;
  70. break;
  71. case STYLE.SCALE:
  72. // classic
  73. targetTop = paddingTop + lp.topMargin + mTargetOffset;
  74. break;
  75. case STYLE.BLEW2CLASSIC:
  76. // classic
  77. targetTop = paddingTop + lp.topMargin + mTargetOffset;
  78. break;
  79. default:
  80. // classic
  81. targetTop = paddingTop + lp.topMargin + mTargetOffset;
  82. break;
  83. }
  84. final int targetRight = targetLeft + targetView.getMeasuredWidth();
  85. final int targetBottom = targetTop + targetView.getMeasuredHeight();
  86. targetView.layout(targetLeft, targetTop, targetRight, targetBottom);
  87. }
  88. // layout footer
  89. if (mFooterView != null) {
  90. final View footerView = mFooterView;
  91. MarginLayoutParams lp = (MarginLayoutParams) footerView.getLayoutParams();
  92. final int footerLeft = paddingLeft + lp.leftMargin;
  93. final int footerBottom;
  94. switch (mStyle) {
  95. case STYLE.CLASSIC:
  96. // classic
  97. footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
  98. break;
  99. case STYLE.ABOVE:
  100. // classic
  101. footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
  102. break;
  103. case STYLE.BLEW:
  104. // blew
  105. footerBottom = height - paddingBottom - lp.bottomMargin;
  106. break;
  107. case STYLE.SCALE:
  108. // scale
  109. footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight / 2 + mFooterOffset / 2;
  110. break;
  111. case STYLE.BLEW2CLASSIC:
  112. // blew2classic
  113. if (mFooterOffset > mFooterHeight) {
  114. footerBottom = height - paddingBottom - lp.bottomMargin;
  115. }
  116. else {
  117. footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
  118. }
  119. break;
  120. default:
  121. // classic
  122. footerBottom = height - paddingBottom - lp.bottomMargin + mFooterHeight + mFooterOffset;
  123. break;
  124. }
  125. final int footerTop = footerBottom - footerView.getMeasuredHeight();
  126. final int footerRight = footerLeft + footerView.getMeasuredWidth();
  127. footerView.layout(footerLeft, footerTop, footerRight, footerBottom);
  128. }
  129. if (mStyle == STYLE.CLASSIC
  130. || mStyle == STYLE.ABOVE) {
  131. if (mHeaderView != null) {
  132. mHeaderView.bringToFront();
  133. }
  134. if (mFooterView != null) {
  135. mFooterView.bringToFront();
  136. }
  137. } else if (mStyle == STYLE.BLEW || mStyle == STYLE.SCALE || mStyle == STYLE.BLEW2CLASSIC) {
  138. if (mTargetView != null) {
  139. mTargetView.bringToFront();
  140. }
  141. }
  142. }

以下拉刷新为例,看这行代码。
paddingTop与lp.topMargin都是0,mHeaderHeight是Header的高度,mHeaderOffset就是手指滑动的距离(这个稍后会有说明)。在下拉过程中,mHeaderOffset的值会越来越大,所以headerTop的值是从-mHeaderHeight开始逐渐增大的,所以headerView会向下逐步移动

  1. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset

而Target更为简单,你手指滑动多少它就跟着滑动多少

  1. targetTop = paddingTop + lp.topMargin + mTargetOffset;

这样能够想象出饿了么滑动到mHeaderHeight高度之后如何处理的吧,请参考我自己定义的style--BLEW2CLASSIC

  1. if (mHeaderOffset > mHeaderHeight) {
  2. headerTop = paddingTop + lp.topMargin;
  3. }
  4. else {
  5. headerTop = paddingTop + lp.topMargin - mHeaderHeight + mHeaderOffset;
  6. }

继续往下来到事件分发部分了

  1. @Override
  2. public boolean dispatchTouchEvent(MotionEvent ev) {
  3. final int action = MotionEventCompat.getActionMasked(ev);
  4. switch (action) {
  5. case MotionEvent.ACTION_CANCEL:
  6. case MotionEvent.ACTION_UP:
  7. // swipeToRefresh -> finger up -> finger down if the status is still swipeToRefresh
  8. // in onInterceptTouchEvent ACTION_DOWN event will stop the scroller
  9. // if the event pass to the child view while ACTION_MOVE(condition is false)
  10. // in onInterceptTouchEvent ACTION_MOVE the ACTION_UP or ACTION_CANCEL will not be
  11. // passed to onInterceptTouchEvent and onTouchEvent. Instead It will be passed to
  12. // child view's onTouchEvent. So we must deal this situation in dispatchTouchEvent
  13. onActivePointerUp();
  14. break;
  15. }
  16. return super.dispatchTouchEvent(ev);
  17. }

获取事件之后,在手指释放的时候执行onActivePointerUp(),咱们来看看。分别判断了当前是处在下拉以刷新、上拉以加载更多、松开以刷新、松开以加载更多,然后滚动到响应的位置上去。注意在松开状态时,执行了onRelease()回调

  1. private void onActivePointerUp() {
  2. if (STATUS.isSwipingToRefresh(mStatus)) {
  3. // simply return
  4. scrollSwipingToRefreshToDefault();
  5. } else if (STATUS.isSwipingToLoadMore(mStatus)) {
  6. // simply return
  7. scrollSwipingToLoadMoreToDefault();
  8. } else if (STATUS.isReleaseToRefresh(mStatus)) {
  9. // return to header height and perform refresh
  10. mRefreshCallback.onRelease();
  11. scrollReleaseToRefreshToRefreshing();
  12. } else if (STATUS.isReleaseToLoadMore(mStatus)) {
  13. // return to footer height and perform loadMore
  14. mLoadMoreCallback.onRelease();
  15. scrollReleaseToLoadMoreToLoadingMore();
  16. }
  17. }

随后就是事件拦截的判断。只要你向下滑动时Target确实不能再向下移动了或者向上滑动时Target确实不能再向上移动了,那么SwipeRefreshLayout就把事件拦截,执行onTouchEvent里面的位移操作了

  1. @Override
  2. public boolean onInterceptTouchEvent(MotionEvent event) {
  3. final int action = MotionEventCompat.getActionMasked(event);
  4. switch (action) {
  5. case MotionEvent.ACTION_DOWN:
  6. mActivePointerId = MotionEventCompat.getPointerId(event, 0);
  7. mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
  8. mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
  9. // if it isn't an ing status or default status
  10. if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isSwipingToLoadMore(mStatus) ||
  11. STATUS.isReleaseToRefresh(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
  12. // abort autoScrolling, not trigger the method #autoScrollFinished()
  13. mAutoScroller.abortIfRunning();
  14. if (mDebug) {
  15. Log.i(TAG, "Another finger down, abort auto scrolling, let the new finger handle");
  16. }
  17. }
  18. if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)
  19. || STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
  20. return true;
  21. }
  22. // let children view handle the ACTION_DOWN;
  23. // 1\. children consumed:
  24. // if at least one of children onTouchEvent() ACTION_DOWN return true.
  25. // ACTION_DOWN event will not return to SwipeToLoadLayout#onTouchEvent().
  26. // but the others action can be handled by SwipeToLoadLayout#onInterceptTouchEvent()
  27. // 2\. children not consumed:
  28. // if children onTouchEvent() ACTION_DOWN return false.
  29. // ACTION_DOWN event will return to SwipeToLoadLayout's onTouchEvent().
  30. // SwipeToLoadLayout#onTouchEvent() ACTION_DOWN return true to consume the ACTION_DOWN event.
  31. // anyway: handle action down in onInterceptTouchEvent() to init is an good option
  32. break;
  33. case MotionEvent.ACTION_MOVE:
  34. if (mActivePointerId == INVALID_POINTER) {
  35. return false;
  36. }
  37. float y = getMotionEventY(event, mActivePointerId);
  38. float x = getMotionEventX(event, mActivePointerId);
  39. final float yInitDiff = y - mInitDownY;
  40. final float xInitDiff = x - mInitDownX;
  41. mLastY = y;
  42. mLastX = x;
  43. boolean moved = Math.abs(yInitDiff) > Math.abs(xInitDiff)
  44. && Math.abs(yInitDiff) > mTouchSlop;
  45. boolean triggerCondition =
  46. // refresh trigger condition
  47. (yInitDiff > 0 && moved && onCheckCanRefresh()) ||
  48. //load more trigger condition
  49. (yInitDiff < 0 && moved && onCheckCanLoadMore());
  50. if (triggerCondition) {
  51. // if the refresh's or load more's trigger condition is true,
  52. // intercept the move action event and pass it to SwipeToLoadLayout#onTouchEvent()
  53. return true;
  54. }
  55. break;
  56. case MotionEvent.ACTION_POINTER_UP: {
  57. onSecondaryPointerUp(event);
  58. mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
  59. mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
  60. break;
  61. }
  62. case MotionEvent.ACTION_UP:
  63. case MotionEvent.ACTION_CANCEL:
  64. mActivePointerId = INVALID_POINTER;
  65. break;
  66. }
  67. return super.onInterceptTouchEvent(event);
  68. }

下面就是位移过程。
如果当期处于初始STATUS_DEFAULT状态,则进入STATUS_SWIPING_TO_REFRESH,同时回调onPrepare()方法
如果在下拉刷新流程中向上滑动并且滑动偏移量小于0,为了不让Target部分移动到屏幕之外,则将体系流程恢复到初始STATUS_DEFAULT状态,同时使用fixCurrentStatusLayout()方法调整三个View的位置。上拉加载更多流程同理
在正常下拉刷新流程中,如果当期状态是STATUS_SWIPING_TO_REFRESH或者是STATUS_RELEASE_TO_REFRESH,即处于下拉以刷新、松开以刷新状态,如果下拉的距离超过mRefreshTriggerOffset,则进入松开以刷新状态,反之则进入下拉以刷新状态。上拉加载更多流程同理
这时候会触发位移发生fingerScroll()

  1. @Override
  2. public boolean onTouchEvent(MotionEvent event) {
  3. final int action = MotionEventCompat.getActionMasked(event);
  4. switch (action) {
  5. case MotionEvent.ACTION_DOWN:
  6. mActivePointerId = MotionEventCompat.getPointerId(event, 0);
  7. return true;
  8. case MotionEvent.ACTION_MOVE:
  9. // take over the ACTION_MOVE event from SwipeToLoadLayout#onInterceptTouchEvent()
  10. // if condition is true
  11. final float y = getMotionEventY(event, mActivePointerId);
  12. final float x = getMotionEventX(event, mActivePointerId);
  13. final float yDiff = y - mLastY;
  14. final float xDiff = x - mLastX;
  15. mLastY = y;
  16. mLastX = x;
  17. if (Math.abs(xDiff) > Math.abs(yDiff) && Math.abs(xDiff) > mTouchSlop) {
  18. return true;
  19. }
  20. if (STATUS.isStatusDefault(mStatus)) {
  21. if (yDiff > 0 && onCheckCanRefresh()) {
  22. mRefreshCallback.onPrepare();
  23. setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
  24. } else if (yDiff < 0 && onCheckCanLoadMore()) {
  25. mLoadMoreCallback.onPrepare();
  26. setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
  27. }
  28. } else if (STATUS.isRefreshStatus(mStatus)) {
  29. if (mTargetOffset <= 0) {
  30. setStatus(STATUS.STATUS_DEFAULT);
  31. fixCurrentStatusLayout();
  32. return true;
  33. }
  34. } else if (STATUS.isLoadMoreStatus(mStatus)) {
  35. if (mTargetOffset >= 0) {
  36. setStatus(STATUS.STATUS_DEFAULT);
  37. fixCurrentStatusLayout();
  38. return true;
  39. }
  40. }
  41. if (STATUS.isRefreshStatus(mStatus)) {
  42. if (STATUS.isSwipingToRefresh(mStatus) || STATUS.isReleaseToRefresh(mStatus)) {
  43. if (mTargetOffset >= mRefreshTriggerOffset) {
  44. setStatus(STATUS.STATUS_RELEASE_TO_REFRESH);
  45. } else {
  46. setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
  47. }
  48. fingerScroll(yDiff);
  49. }
  50. } else if (STATUS.isLoadMoreStatus(mStatus)) {
  51. if (STATUS.isSwipingToLoadMore(mStatus) || STATUS.isReleaseToLoadMore(mStatus)) {
  52. if (-mTargetOffset >= mLoadMoreTriggerOffset) {
  53. setStatus(STATUS.STATUS_RELEASE_TO_LOAD_MORE);
  54. } else {
  55. setStatus(STATUS.STATUS_SWIPING_TO_LOAD_MORE);
  56. }
  57. fingerScroll(yDiff);
  58. }
  59. }
  60. return true;
  61. case MotionEvent.ACTION_POINTER_DOWN: {
  62. final int pointerIndex = MotionEventCompat.getActionIndex(event);
  63. final int pointerId = MotionEventCompat.getPointerId(event, pointerIndex);
  64. if (pointerId != INVALID_POINTER) {
  65. mActivePointerId = pointerId;
  66. }
  67. mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
  68. mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
  69. break;
  70. }
  71. case MotionEvent.ACTION_POINTER_UP: {
  72. onSecondaryPointerUp(event);
  73. mInitDownY = mLastY = getMotionEventY(event, mActivePointerId);
  74. mInitDownX = mLastX = getMotionEventX(event, mActivePointerId);
  75. break;
  76. }
  77. case MotionEvent.ACTION_UP:
  78. case MotionEvent.ACTION_CANCEL:
  79. if (mActivePointerId == INVALID_POINTER) {
  80. return false;
  81. }
  82. mActivePointerId = INVALID_POINTER;
  83. break;
  84. default:
  85. break;
  86. }
  87. return super.onTouchEvent(event);
  88. }

位移无非就是对mTargetOffset进行赋值,同时调整三个View的位置。注意这里调用了onMove()回调

  1. private void fingerScroll(final float yDiff) {
  2. float ratio = mDragRatio;
  3. float yScrolled = yDiff * ratio;
  4. // make sure (targetOffset>0 -> targetOffset=0 -> default status)
  5. // or (targetOffset<0 -> targetOffset=0 -> default status)
  6. // forbidden fling (targetOffset>0 -> targetOffset=0 ->targetOffset<0 -> default status)
  7. // or (targetOffset<0 -> targetOffset=0 ->targetOffset>0 -> default status)
  8. // I am so smart :)
  9. float tmpTargetOffset = yScrolled + mTargetOffset;
  10. if ((tmpTargetOffset > 0 && mTargetOffset < 0)
  11. || (tmpTargetOffset < 0 && mTargetOffset > 0)) {
  12. yScrolled = -mTargetOffset;
  13. }
  14. if (mRefreshFinalDragOffset >= mRefreshTriggerOffset && tmpTargetOffset > mRefreshFinalDragOffset) {
  15. yScrolled = mRefreshFinalDragOffset - mTargetOffset;
  16. } else if (mLoadMoreFinalDragOffset >= mLoadMoreTriggerOffset && -tmpTargetOffset > mLoadMoreFinalDragOffset) {
  17. yScrolled = -mLoadMoreFinalDragOffset - mTargetOffset;
  18. }
  19. if (STATUS.isRefreshStatus(mStatus)) {
  20. mRefreshCallback.onMove(mTargetOffset, false, false);
  21. } else if (STATUS.isLoadMoreStatus(mStatus)) {
  22. mLoadMoreCallback.onMove(mTargetOffset, false, false);
  23. }
  24. updateScroll(yScrolled);
  25. }
  26. private void updateScroll(final float yScrolled) {
  27. if (yScrolled == 0) {
  28. return;
  29. }
  30. mTargetOffset += yScrolled;
  31. if (STATUS.isRefreshStatus(mStatus)) {
  32. mHeaderOffset = mTargetOffset;
  33. mFooterOffset = 0;
  34. } else if (STATUS.isLoadMoreStatus(mStatus)) {
  35. mFooterOffset = mTargetOffset;
  36. mHeaderOffset = 0;
  37. }
  38. if (mDebug) {
  39. Log.i(TAG, "mTargetOffset = " + mTargetOffset);
  40. }
  41. layoutChildren();
  42. invalidate();
  43. }

最后就是执行结束刷新操作,完成闭环。结束的时候,refreshing值为false,执行onComplete()回调,同时回滚到初始位置

  1. public void setRefreshing(boolean refreshing) {
  2. if (!isRefreshEnabled() || mHeaderView == null) {
  3. return;
  4. }
  5. this.mAutoLoading = refreshing;
  6. if (refreshing) {
  7. if (STATUS.isStatusDefault(mStatus)) {
  8. setStatus(STATUS.STATUS_SWIPING_TO_REFRESH);
  9. scrollDefaultToRefreshing();
  10. }
  11. } else {
  12. if (STATUS.isRefreshing(mStatus)) {
  13. mRefreshCallback.onComplete();
  14. postDelayed(new Runnable() {
  15. @Override
  16. public void run() {
  17. scrollRefreshingToDefault();
  18. }
  19. }, mRefreshCompleteDelayDuration);
  20. }
  21. }
  22. }

这里还有一个补充,关于自动滑动方面。自动滚动一般都是通过AutoScroller类,调用其autoScroll()方法来完成,而实际上也是调用Scroller.startScroll()。但是不知道你有没有注意到post(this),它在反复调用这个Runnable的run()来判断滑动是否已经结束。如果没有结束,则通过autoScroll()方法来调用move()回调;如果已经结束,则通过autoScrollFinished()方法来判断下一步应该到达何种状态

  1. private class AutoScroller implements Runnable {
  2. private Scroller mScroller;
  3. private int mmLastY;
  4. private boolean mRunning = false;
  5. private boolean mAbort = false;
  6. public AutoScroller() {
  7. mScroller = new Scroller(getContext());
  8. }
  9. @Override
  10. public void run() {
  11. boolean finish = !mScroller.computeScrollOffset() || mScroller.isFinished();
  12. int currY = mScroller.getCurrY();
  13. int yDiff = currY - mmLastY;
  14. if (finish) {
  15. finish();
  16. } else {
  17. mmLastY = currY;
  18. SwipeToLoadLayout.this.autoScroll(yDiff);
  19. post(this);
  20. }
  21. }
  22. /**
  23. * remove the post callbacks and reset default values
  24. */
  25. private void finish() {
  26. mmLastY = 0;
  27. mRunning = false;
  28. removeCallbacks(this);
  29. // if abort by user, don't call
  30. if (!mAbort) {
  31. autoScrollFinished();
  32. }
  33. }
  34. /**
  35. * abort scroll if it is scrolling
  36. */
  37. public void abortIfRunning() {
  38. if (mRunning) {
  39. if (!mScroller.isFinished()) {
  40. mAbort = true;
  41. mScroller.forceFinished(true);
  42. }
  43. finish();
  44. mAbort = false;
  45. }
  46. }
  47. /**
  48. * The param yScrolled here isn't final pos of y.
  49. * It's just like the yScrolled param in the
  50. * {@link #updateScroll(float yScrolled)}
  51. *
  52. * @param yScrolled
  53. * @param duration
  54. */
  55. private void autoScroll(int yScrolled, int duration) {
  56. removeCallbacks(this);
  57. mmLastY = 0;
  58. if (!mScroller.isFinished()) {
  59. mScroller.forceFinished(true);
  60. }
  61. mScroller.startScroll(0, 0, 0, yScrolled, duration);
  62. post(this);
  63. mRunning = true;
  64. }
  65. }

如果是松开以刷新,则进入刷新状态,同时回调onRefresh()方法
如果是正在刷新状态,则复原,执行onReset()方法
如果是松开以刷新并且通过setRefresh(true)方法进来的,则进入正在刷新状态,执行onRefresh()方法;反之则执行复原操作,执行onReset()方法。
上拉加载更多流程同理

  1. private void autoScrollFinished() {
  2. int mLastStatus = mStatus;
  3. if (STATUS.isReleaseToRefresh(mStatus)) {
  4. setStatus(STATUS.STATUS_REFRESHING);
  5. fixCurrentStatusLayout();
  6. mRefreshCallback.onRefresh();
  7. } else if (STATUS.isRefreshing(mStatus)) {
  8. setStatus(STATUS.STATUS_DEFAULT);
  9. fixCurrentStatusLayout();
  10. mRefreshCallback.onReset();
  11. } else if (STATUS.isSwipingToRefresh(mStatus)) {
  12. if (mAutoLoading) {
  13. mAutoLoading = false;
  14. setStatus(STATUS.STATUS_REFRESHING);
  15. fixCurrentStatusLayout();
  16. mRefreshCallback.onRefresh();
  17. } else {
  18. setStatus(STATUS.STATUS_DEFAULT);
  19. fixCurrentStatusLayout();
  20. mRefreshCallback.onReset();
  21. }
  22. } else if (STATUS.isStatusDefault(mStatus)) {
  23. } else if (STATUS.isSwipingToLoadMore(mStatus)) {
  24. if (mAutoLoading) {
  25. mAutoLoading = false;
  26. setStatus(STATUS.STATUS_LOADING_MORE);
  27. fixCurrentStatusLayout();
  28. mLoadMoreCallback.onLoadMore();
  29. } else {
  30. setStatus(STATUS.STATUS_DEFAULT);
  31. fixCurrentStatusLayout();
  32. mLoadMoreCallback.onReset();
  33. }
  34. } else if (STATUS.isLoadingMore(mStatus)) {
  35. setStatus(STATUS.STATUS_DEFAULT);
  36. fixCurrentStatusLayout();
  37. mLoadMoreCallback.onReset();
  38. } else if (STATUS.isReleaseToLoadMore(mStatus)) {
  39. setStatus(STATUS.STATUS_LOADING_MORE);
  40. fixCurrentStatusLayout();
  41. mLoadMoreCallback.onLoadMore();
  42. } else {
  43. throw new IllegalStateException("illegal state: " + STATUS.getStatus(mStatus));
  44. }
  45. if (mDebug) {
  46. Log.i(TAG, STATUS.getStatus(mLastStatus) + " -> " + STATUS.getStatus(mStatus));
  47. }
  48. }

源码分析到此结束。怎么样,是不是很简单

参考文章
MNSwipeToLoadDemo

链接:https://www.jianshu.com/p/fc8c73db72b3

更多文章

上半年技术文章集合—184篇文章分类汇总

NDK项目实战—高仿360手机助手之卸载监听

破解Android版微信跳一跳,一招教你挑战高分

高级UI特效仿直播点赞效果—一个优美炫酷的点赞动画

一个实现录音和播放的小案例

相信自己,没有做不到的,只有想不到的

如果你觉得此文对您有所帮助,欢迎入群 QQ交流群 :644196190
微信公众号:终端研发部

技术+职场

SwipeRefreshLayout,用最少的代码定制最美的上下拉刷新样式的更多相关文章

  1. 代码实现Android5.0的下拉刷新效果

    如图所示,实现类似与gmail的下拉刷新. 项目地址:https://github.com/stormzhang/SwipeRefreshLayoutDemo 一.在xml文件中定义 这个控件在sup ...

  2. Android 5.X新特性之为RecyclerView添加下拉刷新和上拉加载及SwipeRefreshLayout实现原理

    RecyclerView已经写过两篇文章了,分别是Android 5.X新特性之RecyclerView基本解析及无限复用 和 Android 5.X新特性之为RecyclerView添加Header ...

  3. Android如何定制一个下拉刷新,上滑加载更多的容器

    前言 下拉刷新和上滑加载更多,是一种比较常用的列表数据交互方式. android提供了原生的下拉刷新容器 SwipeRefreshLayout,可惜样式不能定制. 于是打算自己实现一个专用的.但是下拉 ...

  4. Android SwipeRefreshLayout 下拉刷新——Hi_博客 Android App 开发笔记

    以前写下拉刷新 感觉好费劲,要判断ListView是否滚到顶部,还要加载头布局,还要控制 头布局的状态,等等一大堆.感觉麻烦死了.今天学习了SwipeRefreshLayout 的用法,来分享一下,有 ...

  5. android官方下拉刷新控件SwipeRefreshLayout的使用

    可能开发安卓的人大多数都用过很多下拉刷新的开源组件,但是今天用了官方v4支持包的SwipeRefreshLayout觉得效果也蛮不错的,特拿出来分享. 简介:SwipeRefreshLayout组件只 ...

  6. android SwipeRefreshLayout google官方下拉刷新控件

    下拉刷新功能之前一直使用的是XlistView很方便我前面的博客有介绍 SwipeRefreshLayout是google官方推出的下拉刷新控件使用方法也比较简单 今天就来使用下SwipeRefres ...

  7. Google自己的下拉刷新组件SwipeRefreshLayout

    SwipeRefreshLayout SwipeRefreshLayout字面意思就是下拉刷新的布局,继承自ViewGroup,在support v4兼容包下,但必须把你的support librar ...

  8. SwipeRefreshLayout下拉刷新

    1.SwipeRefreshLayout是Google在support v4 19.1版本的library更新的一个下拉刷新组件,实现刷新效果更方便. 弊端:只有下拉 //设置刷新控件圈圈的颜色 sw ...

  9. Android 编程下如何调整 SwipeRefreshLayout 的下拉刷新距离

    SwipeRefreshLayout 的下拉刷新距离比较短,并且也没有提供设置下拉距离的 API,但是看 SwipeRefreshLayout 的源码,会发现有一个内部变量 mDistanceToTr ...

随机推荐

  1. 使用java poi解析表格

    @Test public void poi() throws Exception { InputStream inputStream=new FileInputStream("C:\\Use ...

  2. postfix 指定用户限制指定域名收发

    main.cf 配置示例: smtpd_restriction_classes = local_in_only, local_out_only local_in_only = check_recipi ...

  3. [C++]Linux之计算内存利用率与辨析

    声明:如需引用或者摘抄本博文源码或者其文章的,请在显著处注明,来源于本博文/作者,以示尊重劳动成果,助力开源精神.也欢迎大家一起探讨,交流,以共同进步,乃至成为朋友- 0.0 /* @url:http ...

  4. python后端从数据库请求数据给到前端的具体实现

    先来贴一窜代码让大家理解前端/后端/数据库的工作原理, 首先简要说明:前端向后端请求数据,后端根据前端请求数据的类别分析其需求,并连接到数据库获取相应数据: 来一段简单的实例代码模拟淘宝商城: 前端代 ...

  5. Qemu-KVM管理

    内容: 一.KVM基本配置 二.KVM网络的桥接 三.创建虚拟机 四.虚拟机的关闭和启动 关于KVM: 1).KVM是开源软件,全称是kernel-based virtual machine(基于内核 ...

  6. 记录linux 命令

    1.du:查询文件或文件夹的磁盘使用空间 如果当前目录下文件和文件夹很多,使用不带参数du的命令,可以循环列出所有文件和文件夹所使用的空间.这对查看究竟是那个地方过大是不利的,所以得指定深入目录的层数 ...

  7. 3D中的旋转变换

    相比 2D 中的旋转变换,3D 中的旋转变换复杂了很多.关于 2D 空间的旋转,可以看这篇文章.本文主要粗略地探讨一下 3D 空间中的旋转. 旋转的要素 所谓旋转要素就是说,我们只有知道了这些条件,才 ...

  8. 8.3版本提示未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0 提供程序

    这个原因是8.3版本推出了64位程序,但是Access驱动在64位系统上默认是没有安装的,需要下载一个组件安装即可. 下载2010 Access 驱动程序:数据连接组件安装 http://www.ba ...

  9. java虚拟机的堆内存配置

    官网文档地址:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html 接录如下: -XX:MaxHeapSize=si ...

  10. TreeGrid 控件集 :delphi 学习群 ---- 166637277 (Delphi学习交流与分享)

    delphi 学习群: 166637277  (Delphi学习交流与分享). 群主QQ: 1936431438 TreeGrid 控件集 收集: 1.https://www.lmd.de/produ ...