25 | * Author:donkingliang QQ:1043214265
26 | * Dat:2018/05/24
27 | */
28 | public class RefreshLayout extends ViewGroup {
29 |
30 | protected Context mContext;
31 |
32 | protected int mTouchSlop;
33 |
34 | /*
35 | 触发下拉刷新的最小高度。
36 | 一般来说,触发下拉刷新的高度就是头部View的高度
37 | */
38 | private int mHeaderTriggerMinHeight = 100;
39 |
40 | /*
41 | 触发下拉刷新的最大高度。
42 | 一般来说,触发下拉刷新的高度就是头部View的高度
43 | */
44 | private int mHeaderTriggerMaxHeight = 400;
45 |
46 | /*
47 | 触发上拉加载的最小高度。
48 | 一般来说,触发上拉加载的高度就是尾部View的高度
49 | */
50 | private int mFooterTriggerMinHeight = 100;
51 |
52 | /*
53 | 触发上拉加载的最大高度。
54 | 一般来说,触发上拉加载的高度就是尾部View的高度
55 | */
56 | private int mFooterTriggerMaxHeight = 400;
57 |
58 | //头部容器
59 | private LinearLayout mHeaderLayout;
60 |
61 | //头部View
62 | private View mHeaderView;
63 |
64 | //尾部容器
65 | private LinearLayout mFooterLayout;
66 |
67 | //尾部View
68 | private View mFooterView;
69 |
70 | //标记 无状态(既不是上拉 也 不是下拉)
71 | private final int STATE_NOT = -1;
72 |
73 | //标记 上拉状态
74 | private final int STATE_UP = 1;
75 |
76 | //标记 下拉状态
77 | private final int STATE_DOWN = 2;
78 |
79 | //当前状态
80 | private int mCurrentState = STATE_NOT;
81 |
82 | //是否处于正在下拉刷新状态
83 | private boolean mIsRefreshing = false;
84 |
85 | //是否处于正在上拉加载状态
86 | private boolean mIsLoadingMore = false;
87 |
88 | //是否启用下拉功能(默认开启)
89 | private boolean mIsRefresh = true;
90 |
91 | /*
92 | 是否启用上拉功能(默认不开启)
93 | 如果设置了上拉加载监听器OnLoadMoreListener,就会自动开启。
94 | */
95 | private boolean mIsLoadMore = false;
96 |
97 | //上拉、下拉的阻尼 设置上下拉时的拖动阻力效果
98 | private int mDamp = 4;
99 |
100 | //头部状态监听器
101 | private OnHeaderStateListener mOnHeaderStateListener;
102 |
103 | //尾部状态监听器
104 | private OnFooterStateListener mOnFooterStateListener;
105 |
106 | //下拉刷新监听器
107 | private OnRefreshListener mOnRefreshListener;
108 |
109 | //上拉加载监听器
110 | private OnLoadMoreListener mOnLoadMoreListener;
111 |
112 | //是否还有更多数据
113 | private boolean mHasMore = true;
114 |
115 | //是否显示空布局
116 | private boolean mIsEmpty = false;
117 |
118 | //滑动到底部,自动触发加载更多
119 | private boolean mAutoLoadMore = true;
120 |
121 | //是否拦截触摸事件,
122 | private boolean mInterceptTouchEvent = false;
123 |
124 | //---------------- 用于监听手指松开时,屏幕的滑动状态 -------------------//
125 | //手指松开时,不一定是滑动停止,也有可能是Fling,所以需要监听屏幕滑动的情况。
126 | // 每隔50毫秒获取一下页面的滑动距离,如果跟上次没有变化,表示滑动停止。
127 | // 之所以用延时获取滑动距离的方式获取滑动状态,是因为在sdk 23前,无法给所有的View设置OnScrollChangeListener。
128 | private final int SCROLL_DELAY = 50;
129 | private Handler mScrollHandler = new Handler();
130 | private Runnable mScrollChangeListener = new Runnable() {
131 | @Override
132 | public void run() {
133 | if (listenScrollChange()) {
134 | mScrollHandler.postDelayed(mScrollChangeListener, SCROLL_DELAY);
135 | } else {
136 | mFlingOrientation = ORIENTATION_FLING_NONE;
137 | }
138 | }
139 | };
140 |
141 | private int oldOffsetY;
142 | private int mFlingOrientation;
143 | private static final int ORIENTATION_FLING_NONE = 0;
144 | private static final int ORIENTATION_FLING_UP = 1;
145 | private static final int ORIENTATION_FLING_DOWN = 2;
146 |
147 | //手指触摸屏幕时的触摸点
148 | int mTouchX = 0;
149 | int mTouchY = 0;
150 |
151 | public RefreshLayout(Context context) {
152 | this(context, null);
153 | }
154 |
155 | public RefreshLayout(Context context, AttributeSet attrs) {
156 | this(context, attrs, 0);
157 | }
158 |
159 | public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
160 | super(context, attrs, defStyleAttr);
161 | mContext = context;
162 | init();
163 | }
164 |
165 | private void init() {
166 | setClipToPadding(false);
167 | initHeaderLayout();
168 | initFooterLayout();
169 | ViewConfiguration configuration = ViewConfiguration.get(mContext);
170 | mTouchSlop = configuration.getScaledTouchSlop();
171 | }
172 |
173 | /**
174 | * 初始化头部
175 | */
176 | private void initHeaderLayout() {
177 | mHeaderLayout = new LinearLayout(mContext);
178 | LayoutParams lp = new LayoutParams(
179 | LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
180 | mHeaderLayout.setGravity(Gravity.CENTER_HORIZONTAL);
181 | mHeaderLayout.setLayoutParams(lp);
182 | addView(mHeaderLayout);
183 | }
184 |
185 | /**
186 | * 设置头部View
187 | *
188 | * @param headerView 头部View。这个View必须实现{@link OnHeaderStateListener}接口。
189 | */
190 | public void setHeaderView(@NonNull View headerView) {
191 | if (headerView instanceof OnHeaderStateListener) {
192 | mHeaderView = headerView;
193 | mHeaderLayout.removeAllViews();
194 | mHeaderLayout.addView(mHeaderView);
195 | mOnHeaderStateListener = (OnHeaderStateListener) headerView;
196 | } else {
197 | // headerView必须实现OnHeaderStateListener接口,
198 | // 并通过OnHeaderStateListener的回调来更新headerView的状态。
199 | throw new IllegalArgumentException("headerView must implement the OnHeaderStateListener");
200 | }
201 | }
202 |
203 | /**
204 | * 初始化尾部
205 | */
206 | private void initFooterLayout() {
207 | mFooterLayout = new LinearLayout(mContext);
208 | LayoutParams lp = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
209 | mFooterLayout.setGravity(Gravity.CENTER_HORIZONTAL);
210 | mFooterLayout.setLayoutParams(lp);
211 | addView(mFooterLayout);
212 | }
213 |
214 | /**
215 | * 设置尾部View
216 | *
217 | * @param footerView 尾部View。这个View必须实现{@link OnFooterStateListener}接口
218 | */
219 | public void setFooterView(@NonNull View footerView) {
220 | if (footerView instanceof OnFooterStateListener) {
221 | mFooterView = footerView;
222 | mFooterLayout.removeAllViews();
223 | mFooterLayout.addView(mFooterView);
224 | mOnFooterStateListener = (OnFooterStateListener) footerView;
225 | } else {
226 | // footerView必须实现OnFooterStateListener接口,
227 | // 并通过OnFooterStateListener的回调来更新footerView的状态。
228 | throw new IllegalArgumentException("footerView must implement the OnFooterStateListener");
229 | }
230 | }
231 |
232 | @Override
233 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
234 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
235 | //测量头部高度
236 | View headerView = getChildAt(0);
237 | measureChild(headerView, widthMeasureSpec, heightMeasureSpec);
238 |
239 | //测量尾部高度
240 | View footerView = getChildAt(1);
241 | measureChild(footerView, widthMeasureSpec, heightMeasureSpec);
242 |
243 | //测量内容容器宽高
244 | int count = getChildCount();
245 | int contentHeight = 0;
246 | int contentWidth = 0;
247 | if (mIsEmpty) {
248 | //空布局容器
249 | if (count > 3) {
250 | View emptyView = getChildAt(3);
251 | measureChildWithMargins(emptyView, widthMeasureSpec, 0, heightMeasureSpec, 0);
252 | MarginLayoutParams emptyLp = (MarginLayoutParams) emptyView.getLayoutParams();
253 | contentHeight = emptyView.getMeasuredHeight() + emptyLp.topMargin + emptyLp.bottomMargin;
254 | contentWidth = emptyView.getMeasuredWidth() + emptyLp.leftMargin + emptyLp.rightMargin;
255 | }
256 | } else {
257 | //内容布局容器
258 | if (count > 2) {
259 | View content = getChildAt(2);
260 | measureChildWithMargins(content, widthMeasureSpec, 0, heightMeasureSpec, 0);
261 | MarginLayoutParams contentLp = (MarginLayoutParams) content.getLayoutParams();
262 | contentHeight = content.getMeasuredHeight() + contentLp.topMargin + contentLp.bottomMargin;
263 | contentWidth = content.getMeasuredWidth() + contentLp.leftMargin + contentLp.rightMargin;
264 | }
265 | }
266 |
267 | setMeasuredDimension(measureWidth(widthMeasureSpec, contentWidth),
268 | measureHeight(heightMeasureSpec, contentHeight));
269 | }
270 |
271 | @Override
272 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
273 | //布局头部
274 | View headerView = getChildAt(0);
275 | headerView.layout(getPaddingLeft(), -headerView.getMeasuredHeight(),
276 | getPaddingLeft() + headerView.getMeasuredWidth(), 0);
277 |
278 | //布局尾部
279 | View footerView = getChildAt(1);
280 | footerView.layout(getPaddingLeft(), getMeasuredHeight(),
281 | getPaddingLeft() + footerView.getMeasuredWidth(),
282 | getMeasuredHeight() + footerView.getMeasuredHeight());
283 |
284 | int count = getChildCount();
285 | if (mIsEmpty) {
286 | //空布局容器
287 | if (count > 3) {
288 | View emptyView = getChildAt(3);
289 | MarginLayoutParams emptyLp = (MarginLayoutParams) emptyView.getLayoutParams();
290 | emptyView.layout(getPaddingLeft() + emptyLp.leftMargin,
291 | getPaddingTop() + emptyLp.topMargin,
292 | getPaddingLeft() + emptyLp.leftMargin + emptyView.getMeasuredWidth(),
293 | getPaddingTop() + emptyLp.topMargin + emptyView.getMeasuredHeight());
294 | }
295 | } else {
296 | //内容布局容器
297 | if (count > 2) {
298 | View content = getChildAt(2);
299 | MarginLayoutParams contentLp = (MarginLayoutParams) content.getLayoutParams();
300 | content.layout(getPaddingLeft() + contentLp.leftMargin,
301 | getPaddingTop() + contentLp.topMargin,
302 | getPaddingLeft() + contentLp.leftMargin + content.getMeasuredWidth(),
303 | getPaddingTop() + contentLp.topMargin + content.getMeasuredHeight());
304 | }
305 | }
306 | }
307 |
308 | private int measureWidth(int measureSpec, int contentWidth) {
309 | int result = 0;
310 | int specMode = MeasureSpec.getMode(measureSpec);
311 | int specSize = MeasureSpec.getSize(measureSpec);
312 |
313 | if (specMode == MeasureSpec.EXACTLY) {
314 | result = specSize;
315 | } else {
316 | result = contentWidth + getPaddingLeft() + getPaddingRight();
317 | if (specMode == MeasureSpec.AT_MOST) {
318 | result = Math.min(result, specSize);
319 | }
320 | }
321 |
322 | return result;
323 | }
324 |
325 | private int measureHeight(int measureSpec, int contentHeight) {
326 | int result = 0;
327 | int specMode = MeasureSpec.getMode(measureSpec);
328 | int specSize = MeasureSpec.getSize(measureSpec);
329 |
330 | if (specMode == MeasureSpec.EXACTLY) {
331 | result = specSize;
332 | } else {
333 | result = contentHeight + getPaddingTop() + getPaddingBottom();
334 | if (specMode == MeasureSpec.AT_MOST) {
335 | result = Math.min(result, specSize);
336 | }
337 | }
338 |
339 | return result;
340 | }
341 |
342 | @Override
343 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
344 | return new MarginLayoutParams(getContext(), attrs);
345 | }
346 |
347 | int oldY;
348 |
349 | @Override
350 | public boolean dispatchTouchEvent(MotionEvent ev) {
351 |
352 | switch (ev.getAction()) {
353 | case MotionEvent.ACTION_DOWN:
354 | mCurrentState = STATE_NOT;
355 | mTouchX = (int) ev.getX();
356 | mTouchY = (int) ev.getY();
357 | oldY = (int) ev.getY();
358 | mScrollHandler.removeCallbacksAndMessages(null);
359 | break;
360 | case MotionEvent.ACTION_MOVE:
361 | int newY = (int) ev.getY();
362 | if (isNestedScroll()) {
363 | if ((canPullDown() && newY > oldY)
364 | || (canPullUp() && newY < oldY)) {
365 | nestedScroll(oldY - newY);
366 | }
367 |
368 | if ((getScrollY() > 0 && newY > oldY)
369 | || (getScrollY() < 0 && newY < oldY)) {
370 | nestedPreScroll(oldY - newY);
371 | }
372 | }
373 |
374 | if (newY > oldY) {
375 | mFlingOrientation = ORIENTATION_FLING_UP;
376 | } else if (newY < oldY) {
377 | mFlingOrientation = ORIENTATION_FLING_DOWN;
378 | }
379 |
380 | oldY = newY;
381 |
382 | break;
383 | case MotionEvent.ACTION_UP:
384 |
385 | int y = (int) ev.getY();
386 | int x = (int) ev.getX();
387 | //是否是点击事件,如果按下和松开的坐标没有改变,就认为是点击事件
388 | boolean isClick = Math.abs(mTouchX - x) < mTouchSlop && Math.abs(mTouchY - y) < mTouchSlop;
389 |
390 | if (!mInterceptTouchEvent && !isClick && !mIsEmpty) {
391 | //监听手指松开时,屏幕的滑动状态
392 | //手指松开时,不一定是滑动停止,也有可能是Fling,所以需要监听屏幕滑动的情况。
393 | initScrollListen();
394 | mScrollHandler.postDelayed(mScrollChangeListener, SCROLL_DELAY);
395 | }
396 |
397 | mTouchX = 0;
398 | mTouchY = 0;
399 | break;
400 | }
401 | return super.dispatchTouchEvent(ev);
402 | }
403 |
404 | @Override
405 | public boolean onInterceptTouchEvent(MotionEvent ev) {
406 |
407 | int y = (int) ev.getY();
408 |
409 | switch (ev.getAction()) {
410 | case MotionEvent.ACTION_DOWN:
411 | mInterceptTouchEvent = false;
412 | return false;
413 | case MotionEvent.ACTION_MOVE:
414 | if (isRefreshingOrLoading()) {
415 | return false;
416 | }
417 |
418 | if (pullRefresh() && y - mTouchY > mTouchSlop) {
419 | return true;
420 | }
421 |
422 | if (mHasMore && pullLoadMore() && mTouchY - y > mTouchSlop) {
423 | return true;
424 | }
425 |
426 | return false;
427 | case MotionEvent.ACTION_UP:
428 | return false;
429 | }
430 |
431 | return super.onInterceptTouchEvent(ev);
432 | }
433 |
434 | @Override
435 | public boolean onTouchEvent(MotionEvent event) {
436 |
437 | int y = (int) event.getY();
438 |
439 | switch (event.getAction()) {
440 | case MotionEvent.ACTION_DOWN:
441 | return true;
442 | case MotionEvent.ACTION_MOVE:
443 |
444 | if (mCurrentState == STATE_NOT) {
445 | if (pullRefresh() && y - mTouchY > mTouchSlop) {
446 | mCurrentState = STATE_DOWN;
447 | mInterceptTouchEvent = true;
448 | }
449 |
450 | if (mHasMore && pullLoadMore() && mTouchY - y > mTouchSlop) {
451 | mCurrentState = STATE_UP;
452 | mInterceptTouchEvent = true;
453 | }
454 | }
455 |
456 | if (mTouchY > y) {
457 | if (mCurrentState == STATE_UP && !mIsEmpty) {
458 | scroll((mTouchY - y) / mDamp, true);
459 | }
460 | } else if (mCurrentState == STATE_DOWN || mIsEmpty) {
461 | scroll((mTouchY - y) / mDamp, true);
462 | }
463 | break;
464 |
465 | case MotionEvent.ACTION_UP:
466 | if (!mIsRefreshing && !mIsLoadingMore) {
467 | int scrollOffset = Math.abs(getScrollY());
468 | if (mCurrentState == STATE_DOWN || mIsEmpty) {
469 | if (scrollOffset < getHeaderTriggerHeight()) {
470 | restore(true);
471 | } else {
472 | triggerRefresh();
473 | }
474 | } else if (mCurrentState == STATE_UP) {
475 | if (scrollOffset < getFooterTriggerHeight()) {
476 | restore(true);
477 | } else {
478 | triggerLoadMore();
479 | }
480 | }
481 | }
482 | break;
483 |
484 | default:
485 | break;
486 | }
487 | return super.onTouchEvent(event);
488 | }
489 |
490 | /**
491 | * 是否可下拉刷新。
492 | *
493 | * @return 启用了下拉刷新功能,并且可以下拉显示头部,返回true;否则返回false。
494 | */
495 | protected boolean pullRefresh() {
496 | return mIsRefresh && canPullDown();
497 | }
498 |
499 | /**
500 | * 是否可上拉加载更多。
501 | *
502 | * @return 启用了上拉加载更多功能,并且有更多数据,并且可以下拉显示头部,返回true,否则返回false。
503 | */
504 | protected boolean pullLoadMore() {
505 | return mIsLoadMore && mHasMore && canPullUp();
506 | }
507 |
508 | /**
509 | * 是否可下拉显示头部。
510 | *
511 | * @return 如果是空布局或者内容布局已经滑动到顶部,则返回true,否则返回false。
512 | */
513 | protected boolean canPullDown() {
514 |
515 | if (mIsEmpty) {
516 | return true;
517 | }
518 |
519 | if (getChildCount() >= 3) {
520 | return computeVerticalScrollOffset(getChildAt(2)) <= 0;
521 | }
522 | return true;
523 | }
524 |
525 | /**
526 | * 是否可上拉显示尾部。
527 | *
528 | * @return 如果不是空布局,并且内容布局已经滑动到底部,则返回true,否则返回false。
529 | */
530 | protected boolean canPullUp() {
531 |
532 | if (mIsEmpty) {
533 | return false;
534 | }
535 |
536 | if (getChildCount() >= 3) {
537 | View view = getChildAt(2);
538 | return computeVerticalScrollOffset(view) + computeVerticalScrollExtent(view)
539 | >= computeVerticalScrollRange(view);
540 | }
541 | return false;
542 | }
543 |
544 | /**
545 | * 是否正在刷新或者正在加载更多
546 | *
547 | * @return
548 | */
549 | private boolean isRefreshingOrLoading() {
550 | return mIsRefreshing || mIsLoadingMore;
551 | }
552 |
553 | /**
554 | * 利用属性动画实现平滑滑动
555 | *
556 | * @param start 滑动的开始位置
557 | * @param end 滑动的结束位置
558 | * @param duration 滑动的持续时间
559 | * @param isListening 是否监听滑动变化
560 | * @param listener
561 | */
562 | private void smoothScroll(int start, int end, int duration, final boolean isListening,
563 | Animator.AnimatorListener listener) {
564 | ValueAnimator animator = ValueAnimator.ofInt(start, end).setDuration(duration);
565 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
566 | @Override
567 | public void onAnimationUpdate(ValueAnimator animation) {
568 | scroll((int) animation.getAnimatedValue(), isListening);
569 | }
570 | });
571 | if (listener != null) {
572 | animator.addListener(listener);
573 | }
574 | animator.start();
575 | }
576 |
577 | /**
578 | * @param y 滑动y的位置
579 | * @param isListening 是否监听滑动变化,如果为true,滑动的变化将回调到{@link OnHeaderStateListener}
580 | * 或{@link OnFooterStateListener}。
581 | */
582 | private void scroll(int y, boolean isListening) {
583 |
584 | scrollTo(0, y);
585 | if (isListening) {
586 | int scrollOffset = Math.abs(y);
587 |
588 | if (mCurrentState == STATE_DOWN && mOnHeaderStateListener != null) {
589 | int height = getHeaderTriggerHeight();
590 | mOnHeaderStateListener.onScrollChange(mHeaderView, scrollOffset,
591 | scrollOffset >= height ? 100 : scrollOffset * 100 / height);
592 | }
593 |
594 | if (mCurrentState == STATE_UP && mOnFooterStateListener != null && mHasMore) {
595 | int height = getFooterTriggerHeight();
596 | mOnFooterStateListener.onScrollChange(mFooterView, scrollOffset,
597 | scrollOffset >= height ? 100 : scrollOffset * 100 / height);
598 | }
599 | }
600 | }
601 |
602 | /**
603 | * 是否要处理嵌套滑动。
604 | *
605 | * @return
606 | */
607 | private boolean isNestedScroll() {
608 | return isRefreshingOrLoading() || !mHasMore;
609 | }
610 |
611 | /**
612 | * 处理嵌套滑动。
613 | *
614 | * @param dy 滑动偏移量
615 | */
616 | private void nestedPreScroll(int dy) {
617 | if (mIsRefreshing) {
618 | int scrollY = getScrollY();
619 | if (dy > 0 && scrollY < 0) {
620 | scrollBy(0, Math.min(dy, -scrollY));
621 | }
622 | }
623 |
624 | if (mIsLoadingMore || !mHasMore) {
625 | int scrollY = getScrollY();
626 | if (dy < 0 && scrollY > 0) {
627 | scrollBy(0, Math.max(dy, -scrollY));
628 | }
629 | }
630 | }
631 |
632 | /**
633 | * 处理嵌套滑动。
634 | *
635 | * @param dy 滑动偏移量
636 | */
637 | private void nestedScroll(int dy) {
638 | if (mIsRefreshing) {
639 | int height = getHeaderTriggerHeight();
640 | int scrollY = getScrollY();
641 | if (dy < 0 && scrollY > -height) {
642 | int offset = -getScrollY() - height;
643 | if (offset < 0) {
644 | scrollBy(0, Math.max(dy, offset));
645 | }
646 | }
647 | }
648 |
649 | if (mIsLoadingMore || !mHasMore) {
650 | int height = getFooterTriggerHeight();
651 | int scrollY = getScrollY();
652 | if (dy > 0 && scrollY < height) {
653 | scrollBy(0, Math.min(dy, height - scrollY));
654 | }
655 | }
656 | }
657 |
658 | /**
659 | * 手指松开时,监听滑动状态的初始工作
660 | */
661 | private void initScrollListen() {
662 | if (getChildCount() >= 3) {
663 | oldOffsetY = computeVerticalScrollOffset(getChildAt(2));
664 | }
665 | }
666 |
667 | int scrollVelocity = 0;
668 |
669 |
670 | /**
671 | * 监听手指松开时,屏幕的滑动状态
672 | *
673 | * @return 返回true表示正在滑动,继续监听;返回false表示滑动停止或者不需要监听。
674 | */
675 | private boolean listenScrollChange() {
676 | if (getChildCount() >= 3) {
677 | int offsetY = getScrollTopOffset();
678 | int interval = Math.abs(offsetY - oldOffsetY);
679 | if (interval > 0) {
680 | scrollVelocity = interval;
681 | oldOffsetY = offsetY;
682 | if (isNestedScroll()) {
683 | if (canPullDown() && mFlingOrientation == ORIENTATION_FLING_UP) {
684 | nestedScroll(-scrollVelocity);
685 | } else if (canPullUp() && mFlingOrientation == ORIENTATION_FLING_DOWN) {
686 | nestedScroll(scrollVelocity);
687 | }
688 |
689 | if (getScrollY() > 0 && mFlingOrientation == ORIENTATION_FLING_UP) {
690 | nestedPreScroll(-scrollVelocity);
691 | } else if (getScrollY() < 0 && mFlingOrientation == ORIENTATION_FLING_DOWN) {
692 | nestedPreScroll(scrollVelocity);
693 | }
694 | }
695 | return true;
696 | } else {
697 | // 滑动停止
698 |
699 | //滑动停止时,如果已经滑动到底部,自动触发加载更多
700 | if (mFlingOrientation == ORIENTATION_FLING_DOWN && mAutoLoadMore && pullLoadMore()) {
701 | autoLoadMore();
702 | return false;
703 | }
704 |
705 | if (scrollVelocity > 30) {
706 | if (canPullDown() && mIsRefreshing && mFlingOrientation == ORIENTATION_FLING_UP) {
707 | int height = getHeaderTriggerHeight();
708 | smoothScroll(getScrollY(), -height, (int) (1.0f * height * SCROLL_DELAY / scrollVelocity),
709 | false, null);
710 | } else if ((mIsLoadingMore || !mHasMore) && canPullUp()
711 | && mFlingOrientation == ORIENTATION_FLING_DOWN) {
712 | int height = getFooterTriggerHeight();
713 | smoothScroll(getScrollY(), height, (int) (1.0f * height * SCROLL_DELAY / scrollVelocity),
714 | false, null);
715 | }
716 | }
717 | scrollVelocity = 0;
718 | return false;
719 | }
720 | }
721 | return false;
722 | }
723 |
724 | /**
725 | * 获取内容布局滑动到顶部的偏移量
726 | *
727 | * @return
728 | */
729 | private int getScrollTopOffset() {
730 | if (getChildCount() >= 3) {
731 | View view = getChildAt(2);
732 | return computeVerticalScrollOffset(view);
733 | }
734 | return 0;
735 | }
736 |
737 | /**
738 | * 获取内容布局滑动到底部部的偏移量
739 | *
740 | * @return
741 | */
742 | private int getScrollBottomOffset() {
743 | if (getChildCount() >= 3) {
744 | View view = getChildAt(2);
745 | return computeVerticalScrollRange(view) - computeVerticalScrollOffset(view)
746 | - computeVerticalScrollExtent(view);
747 | }
748 | return 0;
749 | }
750 |
751 | private int computeVerticalScrollOffset(View view) {
752 | try {
753 | Method method = View.class.getDeclaredMethod("computeVerticalScrollOffset");
754 | method.setAccessible(true);
755 | return (int) method.invoke(view);
756 | } catch (Exception e) {
757 | e.printStackTrace();
758 | }
759 | return view.getScrollY();
760 | }
761 |
762 | private int computeVerticalScrollRange(View view) {
763 | try {
764 | Method method = View.class.getDeclaredMethod("computeVerticalScrollRange");
765 | method.setAccessible(true);
766 | return (int) method.invoke(view);
767 | } catch (Exception e) {
768 | e.printStackTrace();
769 | }
770 | return view.getHeight();
771 | }
772 |
773 | private int computeVerticalScrollExtent(View view) {
774 | try {
775 | Method method = View.class.getDeclaredMethod("computeVerticalScrollExtent");
776 | method.setAccessible(true);
777 | return (int) method.invoke(view);
778 | } catch (Exception e) {
779 | e.printStackTrace();
780 | }
781 | return view.getHeight();
782 | }
783 |
784 | /**
785 | * @param isRefresh 是否开启下拉刷新功能 默认开启
786 | */
787 | public void setRefreshEnable(boolean isRefresh) {
788 | mIsRefresh = isRefresh;
789 | }
790 |
791 | /**
792 | * @param isLoadMore 是否开启上拉功能 默认不开启
793 | */
794 | public void setLoadMoreEnable(boolean isLoadMore) {
795 | mIsLoadMore = isLoadMore;
796 | }
797 |
798 | /**
799 | * 还原
800 | */
801 | private void restore(boolean isListener) {
802 | smoothScroll(getScrollY(), 0, 200, isListener, null);
803 | }
804 |
805 | /**
806 | * 通知刷新完成。它会回调{@link OnHeaderStateListener#onRetract(View, boolean)}方法
807 | *
808 | * @param isSuccess 是否刷新成功
809 | */
810 | public void finishRefresh(boolean isSuccess) {
811 | if (mIsRefreshing) {
812 | mCurrentState = STATE_NOT;
813 | if (mOnHeaderStateListener != null) {
814 | mOnHeaderStateListener.onRetract(mHeaderView, isSuccess);
815 | }
816 | }
817 | postDelayed(new Runnable() {
818 | @Override
819 | public void run() {
820 | //平滑收起头部。
821 | smoothScroll(getScrollY(), 0, 200, false, null);
822 | mIsRefreshing = false;
823 | }
824 | }, 500);
825 | }
826 |
827 | /**
828 | * 通知加载更多完成。它会回调{@link OnFooterStateListener#onRetract(View, boolean)}方法
829 | * 请使用{@link #finishLoadMore(boolean, boolean)}
830 | */
831 | @Deprecated
832 | //不推荐使用这个方法 因为同时调用它和hasMore(boolean)两个方法时,尾部无法显示“加载完成”的提示。推荐使用finishLoadMore(boolean hasMore);
833 | public void finishLoadMore() {
834 | //为了处理先调用finishLoadMore(),后调用hasMore(boolean)的情况;延时调用finishLoadMore(mHasMore);
835 | postDelayed(new Runnable() {
836 | @Override
837 | public void run() {
838 | finishLoadMore(true, mHasMore);
839 | }
840 | }, 0);
841 | }
842 |
843 | /**
844 | * 通知加载更多完成。它会回调{@link OnFooterStateListener#onRetract(View, boolean)}方法
845 | *
846 | * @param isSuccess 是否加载成功
847 | * @param hasMore 是否还有更多数据
848 | */
849 | public void finishLoadMore(boolean isSuccess, final boolean hasMore) {
850 | if (mIsLoadingMore) {
851 | mCurrentState = STATE_NOT;
852 | if (mOnFooterStateListener != null) {
853 | mOnFooterStateListener.onRetract(mFooterView, isSuccess);
854 | }
855 | }
856 | // 处理尾部的收起。
857 | postDelayed(new Runnable() {
858 | @Override
859 | public void run() {
860 | mIsLoadingMore = false;
861 | hasMore(hasMore);
862 | if (getScrollBottomOffset() > 0) {
863 | // 如果有新的内容加载出来,就收起尾部,并把新内容显示出来。。
864 | if (getChildCount() >= 3) {
865 | View v = getChildAt(2);
866 | if (v instanceof AbsListView) {
867 | AbsListView listView = (AbsListView) v;
868 | listView.smoothScrollBy(getScrollY(), 0);
869 | } else {
870 | v.scrollBy(0, getScrollY());
871 | }
872 | }
873 | scroll(0, false);
874 | } else if (mHasMore) {
875 | smoothScroll(getScrollY(), 0, 200, false, null);
876 | }
877 | }
878 | }, 500);
879 | }
880 |
881 | /**
882 | * 自动触发下拉刷新。只有启用了下拉刷新功能时起作用。
883 | */
884 | public void autoRefresh() {
885 | if (!mIsRefresh || isRefreshingOrLoading()) {
886 | return;
887 | }
888 | post(new Runnable() {
889 | @Override
890 | public void run() {
891 | mCurrentState = STATE_DOWN;
892 | smoothScroll(getScrollY(), -getHeaderTriggerHeight(), 200, true, new AnimatorListenerAdapter() {
893 | @Override
894 | public void onAnimationEnd(Animator animation) {
895 | triggerRefresh();
896 | }
897 | });
898 | }
899 | });
900 | }
901 |
902 | /**
903 | * 触发下拉刷新
904 | */
905 | private void triggerRefresh() {
906 | if (!mIsRefresh || isRefreshingOrLoading()) {
907 | return;
908 | }
909 |
910 | mIsRefreshing = true;
911 | mCurrentState = STATE_NOT;
912 | scroll(-getHeaderTriggerHeight(), false);
913 | if (mOnHeaderStateListener != null) {
914 | mOnHeaderStateListener.onRefresh(mHeaderView);
915 | }
916 |
917 | if (mOnRefreshListener != null) {
918 | mOnRefreshListener.onRefresh();
919 | }
920 | }
921 |
922 | /**
923 | * 自动触发上拉加载更多。只有在启用了上拉加载更多功能并且有更多数据时起作用。
924 | */
925 | public void autoLoadMore() {
926 | if (isRefreshingOrLoading() || !mHasMore || !mIsLoadMore || mIsEmpty) {
927 | return;
928 | }
929 | post(new Runnable() {
930 | @Override
931 | public void run() {
932 | mCurrentState = STATE_UP;
933 | smoothScroll(getScrollY(), getFooterTriggerHeight(), 200, true, new AnimatorListenerAdapter() {
934 | @Override
935 | public void onAnimationEnd(Animator animation) {
936 | triggerLoadMore();
937 | }
938 | });
939 | }
940 | });
941 | }
942 |
943 | /**
944 | * 触发上拉加载更多。
945 | */
946 | private void triggerLoadMore() {
947 | if (isRefreshingOrLoading() || !mHasMore || !mIsLoadMore || mIsEmpty) {
948 | return;
949 | }
950 | mIsLoadingMore = true;
951 | mCurrentState = STATE_NOT;
952 | scroll(getFooterTriggerHeight(), false);
953 | if (mOnFooterStateListener != null) {
954 | mOnFooterStateListener.onRefresh(mFooterView);
955 | }
956 | if (mOnLoadMoreListener != null) {
957 | mOnLoadMoreListener.onLoadMore();
958 | }
959 | }
960 |
961 | /**
962 | * 是否自动触发加载更多。只有在启用了上拉加载更多功能时起作用。
963 | *
964 | * @param autoLoadMore 如果为true,滑动到底部,自动触发加载更多
965 | */
966 | public void setAutoLoadMore(boolean autoLoadMore) {
967 | mAutoLoadMore = autoLoadMore;
968 | }
969 |
970 | /**
971 | * 是否还有更多数据,只有为true是才能上拉加载更多.
972 | * 它会回调{@link OnFooterStateListener#hasMore(boolean)}方法。
973 | *
974 | * @param hasMore 默认为true。
975 | */
976 | public void hasMore(boolean hasMore) {
977 | if (mHasMore != hasMore) {
978 | mHasMore = hasMore;
979 | if (mOnFooterStateListener != null) {
980 | mOnFooterStateListener.onHasMore(mFooterView, hasMore);
981 | }
982 | }
983 | }
984 |
985 | /**
986 | * 获取触发下拉刷新的下拉高度
987 | *
988 | * @return
989 | */
990 | public int getHeaderTriggerHeight() {
991 | int height = mHeaderLayout.getHeight();
992 | height = Math.max(height, mHeaderTriggerMinHeight);
993 | height = Math.min(height, mHeaderTriggerMaxHeight);
994 | return height;
995 | }
996 |
997 | /**
998 | * 获取触发上拉加载的上拉高度
999 | *
1000 | * @return
1001 | */
1002 | public int getFooterTriggerHeight() {
1003 | int height = mFooterLayout.getHeight();
1004 | height = Math.max(height, mFooterTriggerMinHeight);
1005 | height = Math.min(height, mFooterTriggerMaxHeight);
1006 | return height;
1007 | }
1008 |
1009 | /**
1010 | * 设置触发下拉刷新的最小高度。
1011 | *
1012 | * @param headerTriggerMinHeight
1013 | */
1014 | public void setHeaderTriggerMinHeight(int headerTriggerMinHeight) {
1015 | mHeaderTriggerMinHeight = headerTriggerMinHeight;
1016 | }
1017 |
1018 | /**
1019 | * 设置触发下拉刷新的最大高度。
1020 | *
1021 | * @param headerTriggerMaxHeight
1022 | */
1023 | public void setHeaderTriggerMaxHeight(int headerTriggerMaxHeight) {
1024 | mHeaderTriggerMaxHeight = headerTriggerMaxHeight;
1025 | }
1026 |
1027 | /**
1028 | * 设置触发上拉加载的最小高度。
1029 | *
1030 | * @param footerTriggerMinHeight
1031 | */
1032 | public void setFooterTriggerMinHeight(int footerTriggerMinHeight) {
1033 | mFooterTriggerMinHeight = footerTriggerMinHeight;
1034 | }
1035 |
1036 | /**
1037 | * 设置触发上拉加载的最大高度。
1038 | *
1039 | * @param footerTriggerMaxHeight
1040 | */
1041 | public void setFooterTriggerMaxHeight(int footerTriggerMaxHeight) {
1042 | mFooterTriggerMaxHeight = footerTriggerMaxHeight;
1043 | }
1044 |
1045 | /**
1046 | * 设置拉动阻力 (1到10)
1047 | *
1048 | * @param damp
1049 | */
1050 | public void setDamp(int damp) {
1051 | if (damp < 1) {
1052 | mDamp = 1;
1053 | } else if (damp > 10) {
1054 | mDamp = 10;
1055 | } else {
1056 | mDamp = damp;
1057 | }
1058 | }
1059 |
1060 | /**
1061 | * 隐藏内容布局,显示空布局
1062 | */
1063 | public void showEmpty() {
1064 | if (!mIsEmpty) {
1065 | mIsEmpty = true;
1066 | //显示空布局
1067 | if (getChildCount() > 3) {
1068 | getChildAt(3).setVisibility(VISIBLE);
1069 | }
1070 | //隐藏内容布局
1071 | if (getChildCount() > 2) {
1072 | getChildAt(2).setVisibility(GONE);
1073 | }
1074 | }
1075 | }
1076 |
1077 | /**
1078 | * 隐藏空布局,显示内容布局
1079 | */
1080 | public void hideEmpty() {
1081 | if (mIsEmpty) {
1082 | mIsEmpty = false;
1083 | //隐藏空布局
1084 | if (getChildCount() > 3) {
1085 | getChildAt(3).setVisibility(GONE);
1086 | }
1087 | //显示内容布局
1088 | if (getChildCount() > 2) {
1089 | getChildAt(2).setVisibility(VISIBLE);
1090 | }
1091 | }
1092 | }
1093 |
1094 | /**
1095 | * 设置加载更多的监听,触发加载时回调。
1096 | * RefreshLayout默认没有启用上拉加载更多的功能,如果设置了OnLoadMoreListener,则自动启用。
1097 | *
1098 | * @param listener
1099 | */
1100 | public void setOnLoadMoreListener(OnLoadMoreListener listener) {
1101 | mOnLoadMoreListener = listener;
1102 | if (listener != null) {
1103 | setLoadMoreEnable(true);
1104 | }
1105 | }
1106 |
1107 | /**
1108 | * 设置刷新监听,触发刷新时回调
1109 | *
1110 | * @param listener
1111 | */
1112 | public void setOnRefreshListener(OnRefreshListener listener) {
1113 | mOnRefreshListener = listener;
1114 | }
1115 |
1116 |
1117 | //---------------- 监听接口 -------------------//
1118 |
1119 | /**
1120 | * 头部状态监听器
1121 | */
1122 | public interface OnHeaderStateListener {
1123 |
1124 | /**
1125 | * 头部滑动变化
1126 | *
1127 | * @param headerView 头部View
1128 | * @param scrollOffset 滑动距离
1129 | * @param scrollRatio 从开始到触发阀值的滑动比率(0到100)如果滑动到达了阀值,就算再滑动,这个值也是100
1130 | */
1131 | void onScrollChange(View headerView, int scrollOffset, int scrollRatio);
1132 |
1133 | /**
1134 | * 头部处于刷新状态 (触发下拉刷新的时候调用)
1135 | *
1136 | * @param headerView 头部View
1137 | */
1138 | void onRefresh(View headerView);
1139 |
1140 | /**
1141 | * 刷新完成,头部收起
1142 | *
1143 | * @param headerView 头部View
1144 | * @param isSuccess 是否刷新成功
1145 | */
1146 | void onRetract(View headerView, boolean isSuccess);
1147 |
1148 | }
1149 |
1150 | /**
1151 | * 尾部状态监听器
1152 | */
1153 | public interface OnFooterStateListener {
1154 |
1155 | /**
1156 | * 尾部滑动变化
1157 | *
1158 | * @param footerView 尾部View
1159 | * @param scrollOffset 滑动距离
1160 | * @param scrollRatio 从开始到触发阀值的滑动比率(0到100)如果滑动到达了阀值,就算在滑动,这个值也是100
1161 | */
1162 | void onScrollChange(View footerView, int scrollOffset, int scrollRatio);
1163 |
1164 | /**
1165 | * 尾部处于加载状态 (触发上拉加载的时候调用)
1166 | *
1167 | * @param footerView 尾部View
1168 | */
1169 | void onRefresh(View footerView);
1170 |
1171 | /**
1172 | * 加载完成,尾部收起
1173 | *
1174 | * @param footerView 尾部View
1175 | * @param isSuccess 是否加载成功
1176 | */
1177 | void onRetract(View footerView, boolean isSuccess);
1178 |
1179 | /**
1180 | * 是否还有更多(是否可以加载下一页)
1181 | *
1182 | * @param footerView
1183 | * @param hasMore
1184 | */
1185 | void onHasMore(View footerView, boolean hasMore);
1186 | }
1187 |
1188 | /**
1189 | * 上拉加载监听器
1190 | */
1191 | public interface OnLoadMoreListener {
1192 | void onLoadMore();
1193 | }
1194 |
1195 | /**
1196 | * 下拉更新监听器
1197 | */
1198 | public interface OnRefreshListener {
1199 | void onRefresh();
1200 | }
1201 | }
1202 |
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/icon_down_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/icon_down_arrow.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading1.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading10.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading11.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading12.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading2.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading3.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading4.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading5.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading6.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading7.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading8.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable-xhdpi/loading9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/donkingliang/RefreshLayout/6b5f70a5cea3b5384d122a6837525335542b0459/refresh/src/main/res/drawable-xhdpi/loading9.png
--------------------------------------------------------------------------------
/refresh/src/main/res/drawable/progress_round.xml:
--------------------------------------------------------------------------------
1 |
2 |