Roll up your sleeves and write an Android universal refresh control

Roll up your sleeves and write an Android universal refresh control

In our projects, we often have the need to pull up and pull down to refresh. Almost all listView and RecyclerView will be accompanied by the need to pull up and pull down to refresh. If we use some open source controls, we have to update them when we change the controls. Now we roll up our sleeves and write a universal refresh control.

Ideas:

  • Write a RefreshLayout that inherits RelativeLayout
  • Add header and footer controls as refresh controls
  • Refresh operation through event distribution
  • Controlling the movement of controls through animation

Purpose: Make all its child controls available, even a TextView

  1. public class RefreshLayout extends RelativeLayout {
  2.  
  3. /**
  4. * The speed ratio when sliding the control
  5. */
  6. private final int V_REFRESH = 2;
  7. /**
  8. * Is it a refresh process?
  9. * true
  10. * false is not
  11. * Refresh can only be performed when it is false
  12. */
  13. private boolean mIsRefreshDuring;
  14. /**
  15. * You can pull down to refresh
  16. */
  17. private boolean mCanDownPull;
  18. /**
  19. * Pull-up refresh is possible
  20. */
  21. private boolean mCanUpPull;
  22. /**
  23. * Determine whether it is the first move after touching
  24. */
  25. private boolean mIsFirstMove;
  26. /**
  27. * The distance of translation on the y-axis
  28. */
  29. private int mDistanceY;
  30. /**
  31. * Refresh interface object
  32. */
  33. privateOnRefresh mOnRefresh;
  34. /**
  35. * Variables used to control event interception
  36. */
  37. private boolean mCanIntercept;
  38. private int mTouchSlop;
  39. private int mDistance;
  40. private LayoutParams mHeaderParams;
  41. private View mHeaderView;
  42. private View mFootView;
  43. private int mHeaderMaxHeight;
  44. private int mStartY;
  45. private LayoutParams mFootParams;
  46. private int mFootMaxHeight;
  47. private PullCallBack mCallBack;
  48. private View mChildView;
  49. private ObjectAnimator mAnimator;
  50.  
  51. public RefreshLayout(Context context) {
  52. super(context);
  53. initData();
  54. }
  55.  
  56. public RefreshLayout(Context context, AttributeSet attrs) {
  57. super(context, attrs);
  58. initData();
  59. }
  60.  
  61. public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
  62. super(context, attrs, defStyleAttr);
  63. initData();
  64. }
  65.  
  66. /**
  67. * The interface that the head and tail controls must implement
  68. */
  69. public interface HeadAndFootCallBack {
  70. //Set properties
  71. void setAttribute();
  72.  
  73. //Start refreshing
  74. void startPull();
  75.  
  76. //Stop refreshing
  77. void stopPull();
  78. }
  79.  
  80. /**
  81. * The dragged control subclass must implement
  82. */
  83. public interface PullCallBack {
  84. boolean canDownPull();
  85.  
  86. boolean canUpPull();
  87. }
  88.  
  89. private void initData() {
  90. //Drawing cannot be performed without calling this method
  91. setWillNotDraw( false );
  92. }
  93.  
  94. /**
  95. * This method must be used after the pull-down refresh is completed
  96. */
  97. public void downPullFinish() {
  98. mAnimator.setFloatValues(mChildView.getTranslationY(), 0);
  99. mAnimator.start();
  100. ((HeadAndFootCallBack) mHeaderView).stopPull();
  101. }
  102.  
  103. /**
  104. * This method must be called after the pull-up is completed
  105. */
  106. public void upPullFinish() {
  107. mAnimator.setFloatValues(mChildView.getTranslationY(), 0);
  108. mAnimator.start();
  109. ((HeadAndFootCallBack) mFootView).stopPull();
  110. }
  111.  
  112. /**
  113. * Automatic pull-down refresh
  114. */
  115. public void autoDownPullForHead() {
  116. postDelayed(new Runnable() {
  117. @Override
  118. public void run() {
  119. mCanDownPull = true ;
  120. mCanUpPull = false ;
  121. mAnimator.setFloatValues(10, mHeaderMaxHeight);
  122. mAnimator.start();
  123. ((HeadAndFootCallBack) mHeaderView).startPull();
  124. mOnRefresh.onDownPullRefresh();
  125. }
  126. }, 500);
  127. }
  128.  
  129. /**
  130. * Automatic pull-down refresh
  131. */
  132. public void autoUpPullForHead() {
  133. postDelayed(new Runnable() {
  134. @Override
  135. public void run() {
  136. mCanDownPull = false ;
  137. mCanUpPull = true ;
  138. mAnimator.setFloatValues(0, mFootMaxHeight);
  139. mAnimator.start();
  140. ((HeadAndFootCallBack) mFootView).startPull();
  141. mOnRefresh.onUpPullRefresh();
  142. }
  143. }, 500);
  144. }
  145.  
  146. @Override
  147. public boolean onInterceptTouchEvent(MotionEvent ev) {
  148. return mCanIntercept;
  149. }
  150.  
  151. @Override
  152. public boolean onTouchEvent(MotionEvent event) {
  153. return   true ;
  154. }
  155.  
  156. @Override
  157. public boolean dispatchTouchEvent(MotionEvent event) {
  158. Log.e( "shen" , "mIsRefreshDuring=" + mIsRefreshDuring);
  159. if (mIsRefreshDuring)/*If refresh is in progress, MotionEvent will not be obtained*/ {
  160. return super.dispatchTouchEvent(event);
  161. }
  162. switch (event.getAction()) {
  163. case MotionEvent.ACTION_DOWN:
  164. mStartY = ( int ) event.getY();
  165. initPull();
  166. break;
  167. case MotionEvent.ACTION_MOVE:
  168. if (event.getPointerCount() == 1) {
  169. int moveY = ( int ) event.getY();
  170. mDistanceY = (moveY - mStartY) / V_REFRESH;
  171. if (!mIsFirstMove && mDistanceY != 0 && mDistanceY < mTouchSlop) {
  172. mCanDownPull = mDistanceY > 0;
  173. mCanUpPull = !mCanDownPull;
  174. mIsFirstMove = true ;
  175. }
  176. if (mCanDownPull && mCallBack.canDownPull()) {
  177. upDataForDownPull(); //Pull down to refresh
  178. mChildView.setEnabled( false );
  179. mCanIntercept = true ;
  180. }
  181. if (mCanUpPull && mCallBack.canUpPull()) {
  182. upDataForUpPull(); //Pull up to load
  183. mChildView.setEnabled( false );
  184. mCanIntercept = true ;
  185. }
  186. mStartY = moveY;
  187. }
  188. break;
  189. case MotionEvent.ACTION_UP:
  190. mIsRefreshDuring = true ;
  191. mIsFirstMove = false ;
  192. if (mHeaderParams.height >= mHeaderMaxHeight)/*You can pull down to refresh*/ {
  193. ((HeadAndFootCallBack) mHeaderView).startPull();
  194. mOnRefresh.onDownPullRefresh();
  195. } else if (mFootParams.height >= mFootMaxHeight)/*You can pull up to refresh*/ {
  196. ((HeadAndFootCallBack) mFootView).startPull();
  197. mOnRefresh.onUpPullRefresh();
  198. } else if (mHeaderParams.height > 0 && mHeaderParams.height < mHeaderMaxHeight)/*Cannot pull down to refresh, retract*/ {
  199. releaseForDownFinished();
  200. } else if (mFootParams.height > 0 && mFootParams.height < mFootMaxHeight)/*Cannot pull down to refresh, retract*/ {
  201. releaseForUpFinished();
  202. } else {
  203. mIsRefreshDuring = false ;
  204. mCanIntercept = false ;
  205. }
  206. break;
  207. }
  208. super.dispatchTouchEvent(event);
  209. return   true ;
  210. }
  211.  
  212. /**
  213. * Each touch requires initialization
  214. */
  215. private void initPull() {
  216. mCanDownPull = false ;
  217. mCanUpPull = false ;
  218. }
  219.  
  220. /**
  221. * No pull-up refresh required
  222. */
  223. private void releaseForUpFinished() {
  224. mAnimator.setFloatValues(mChildView.getTranslationY(), 0);
  225. mAnimator.start();
  226. }
  227.  
  228. /**
  229. * No need to pull down to refresh
  230. */
  231. private void releaseForDownFinished() {
  232. mAnimator.setFloatValues(mChildView.getTranslationY(), 0);
  233. mAnimator.start();
  234. }
  235.  
  236. /**
  237. * Handle gestures when pulling up
  238. */
  239. private void upDataForUpPull() {
  240. if (mDistanceY != 0) {
  241. mFootParams.height -= mDistanceY;
  242. if (mFootParams.height <= 0) {
  243. mFootParams.height = 0;
  244. }
  245. if (mFootParams.height >= mFootMaxHeight) {
  246. mFootParams.height = mFootMaxHeight;
  247. }
  248. mChildView.setTranslationY(-mFootParams.height);
  249. mFootView.requestLayout();
  250. }
  251. }
  252.  
  253. /**
  254. * Handle gestures when pulling down
  255. */
  256. private void upDataForDownPull() {
  257. if (mDistanceY != 0) {
  258. mHeaderParams.height += mDistanceY;
  259. if (mHeaderParams.height >= mHeaderMaxHeight) { //***
  260. mHeaderParams.height = mHeaderMaxHeight;
  261. }
  262. if (mHeaderParams.height <= 0) { //Minimum
  263. mHeaderParams.height = 0;
  264. }
  265. mChildView.setTranslationY(mHeaderParams.height);
  266. mHeaderView.requestLayout();
  267. }
  268. }
  269.  
  270. @Override
  271. protected void onAttachedToWindow() {
  272. super.onAttachedToWindow();
  273. }
  274.  
  275. @Override
  276. protected void onFinishInflate() {
  277. super.onFinishInflate();
  278. // Load header
  279. mHeaderView = getChildAt(0);
  280. if (!(mHeaderView instanceof HeadAndFootCallBack)) {
  281. new IllegalStateException( "HeaderView must implement the HeadAndFootCallBack interface" );
  282. }
  283. ((HeadAndFootCallBack) mHeaderView).setAttribute();
  284. mHeaderParams = (LayoutParams) mHeaderView.getLayoutParams();
  285. mHeaderParams.addRule(RelativeLayout.ALIGN_PARENT_TOP);
  286.  
  287. //Load end
  288. mFootView = getChildAt(2);
  289. if (!(mFootView instanceof HeadAndFootCallBack)) {
  290. new IllegalStateException( "FootView must implement the HeadAndFootCallBack interface" );
  291. }
  292. ((HeadAndFootCallBack) mFootView).setAttribute();
  293. mFootParams = (LayoutParams) mFootView.getLayoutParams();
  294. mFootParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
  295.  
  296. mChildView = getChildAt(1);
  297. if (!(mChildView instanceof HeadAndFootCallBack)) {
  298. new IllegalStateException( "ChildView must implement the PullCallBack interface" );
  299. }
  300. mCallBack = (PullCallBack) getChildAt(1);
  301.  
  302. //Set up the animation
  303. mAnimator = ObjectAnimator.ofFloat(mChildView, "translationY" , 0);
  304. mAnimator.setInterpolator(new DecelerateInterpolator());
  305. mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
  306. @Override
  307. public void onAnimationUpdate(ValueAnimator animation) {
  308. int translationY = ( int ) mChildView.getTranslationY();
  309. if (mCanUpPull) { //Slide down from the position moved to
  310. mFootParams.height = Math. abs (translationY);
  311. mFootView.requestLayout();
  312. } else if (mCanDownPull) {
  313. mHeaderParams.height = Math. abs (translationY);
  314. mHeaderView.requestLayout();
  315. }
  316. Log.e( "shen" , "translationY=" + translationY);
  317. Log.e( "shen" , "mHeaderParams.height=" + mHeaderParams.height);
  318. if (translationY == 0) {
  319. mChildView.setEnabled( true );
  320. mDistanceY = 0; // reset
  321. mIsRefreshDuring = false ; //Reset
  322. mCanIntercept = false ;
  323. } else {
  324. mIsRefreshDuring = true ;
  325. }
  326. }
  327. });
  328. }
  329.  
  330. @Override
  331. protected void onSizeChanged( int w, int h, int oldw, int oldh) {
  332. super.onSizeChanged(w, h, oldw, oldh);
  333. mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  334. mDistance = mTouchSlop * 5;
  335. //Set the initial properties of the pull-down header
  336. mHeaderMaxHeight = mHeaderParams.height;
  337. mHeaderParams.height = 0;
  338. mHeaderView.requestLayout();
  339. //Set the initial properties of the pull-up tail
  340. mFootMaxHeight = mFootParams.height;
  341. mFootParams.height = 0;
  342. mFootView.requestLayout();
  343. }
  344.  
  345. /**
  346. * Pull-down/pull-up event monitoring
  347. */
  348. public interface OnRefresh {
  349. /**
  350. * Pull down to refresh
  351. */
  352. void onDownPullRefresh();
  353.  
  354. /**
  355. * Pull-up loading
  356. */
  357. void onUpPullRefresh();
  358. }
  359.  
  360. public void setOnRefresh(OnRefresh onRefresh) {
  361. mOnRefresh = onRefresh;
  362. }
  363.  
  364. }

Add three controls to it. The first and last controls are for refreshing the head and footer, and the second one is for normal display. The head and footer must implement the HeadAndFootCallBack interface to set properties and notify the start and end of refreshing.

Difficulty: Now let’s talk about the difficulties encountered during development

  • Since the judgment is in dispatchTouchEvent, if the control and its child controls do not consume the event, the event will not be sent to it, because if the DOWN event is not consumed, all subsequent events will not be received. The solution is to let the control's onTouchEvent method return true. When the child control does not consume the event, it will return to the control for consumption, which will not cause the failure to receive the event due to the non-consumption of the DOWN event, resulting in the dispatchTouchEvent not consuming the event.
  • Animation, animation is my pain. I am learning about estimators recently.

I think this control is well written. It can help us learn event distribution, animation, and interface callback, which is also of certain learning significance.

<<:  Goodbye, the good days of making money without doing anything using Alipay and WeChat Pay!

>>:  Implementation of simple SMS verification function on Android

Recommend

Mobile promotion conversion effect is poor? 2 steps to get everything done~

They say mobile now dominates the marketing battl...

WeChat is a communication tool for micro-businesses, not a marketing tool

The popularity of WeChat has also gradually broug...

Facebook advertising optimization tips!

For many businesses, peak season performance may ...

Earthworms don’t live in water, so why do fish like to eat them?

Author: Su Chengyu The article comes from the Sci...

Automatic iOS updates can make you late for work the next day

According to the latest news from foreign media, ...