之前那個帳號,之後可能不用了,把文章搬過來!!!java
接受hi大頭鬼hi的建議,來一個動態圖,方便你們知道這是個什麼東西。android
項目中,須要一個支持任意View的下拉刷新+上拉加載控件,GitHub上有不少現成的實現,如Android-PullToRefresh, android-Ultra-Pull-To-Refresh等,這些Library都很是優秀,可是Android-PullToRefresh 已經不在維護了,android-Ultra-Pull-To-Refresh自己並不支持上拉加載更多,通過一番糾結後決定本身寫一個。git
不管是下拉刷新仍是上拉加載更多,原理都是在內容View(ListView、RecyclerView...)不能下拉或者上劃時響應用戶的觸摸事件,在頂部或者底部顯示一個刷新視圖,在程序刷新操做完成後再隱藏掉。github
既然要在頭部和頂部添加刷新視圖,咱們的控件應該是個ViewGroup,我是直接繼承FrameLayout,這個控件的名字叫NsRefreshLayout。而後咱們須要定義一些屬性,如是否自動觸發上拉加載更多、刷新視圖中的文字顏色等。ide
<declare-styleable name="NsRefreshLayout"> <!--Loading視圖背景顏色--> <attr name="load_view_bg_color" format="color|reference"/> <!--進度條顏色--> <attr name="progress_bar_color" format="color|reference"/> <!--進度條背景色--> <attr name="progress_bg_color" format="color|reference"/> <!--Loading視圖中文字顏色--> <attr name="load_text_color" format="color|reference"/> <!--下拉刷新問題描述--> <attr name="pull_refresh_text" format="string|reference"/> <!--上拉加載文字描述--> <attr name="pull_load_text" format="string|reference"/> <!--是否自動觸發加載更多--> <attr name="auto_load_more" format="boolean"/> <!--下拉刷新是否可用--> <attr name="pull_refresh_enable" format="boolean"/> <!--上拉加載是否可用--> <attr name="pull_load_enable" format="boolean"/> </declare-styleable>
/** * 初始化控件屬性 */ private void initAttrs(Context context, AttributeSet attrs) { if (getChildCount() > 1) { throw new RuntimeException("can only have one child"); } loadingViewFinalHeight = NrlUtils.dipToPx(context, LOADING_VIEW_FINAL_HEIGHT_DP); loadingViewOverHeight = loadingViewFinalHeight * 2; if (isInEditMode() && attrs == null) { return; } int resId; TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.NsRefreshLayout); Resources resources = context.getResources(); //LoadView背景顏色 resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_view_bg_color, -1); if (resId == -1) { mLoadViewBgColor = ta.getColor(R.styleable.NsRefreshLayout_load_view_bg_color, Color.WHITE); } else { mLoadViewBgColor = resources.getColor(resId); } //加載文字顏色 resId = ta.getResourceId(R.styleable.NsRefreshLayout_load_text_color, -1); if (resId == -1) { mLoadViewTextColor = ta.getColor(R.styleable.NsRefreshLayout_load_text_color, Color.BLACK); } else { mLoadViewTextColor = resources.getColor(resId); } //進度條背景顏色 resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bg_color, -1); if (resId == -1) { mProgressBgColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bg_color, Color.WHITE); } else { mProgressBgColor = resources.getColor(resId); } //進度條顏色 resId = ta.getResourceId(R.styleable.NsRefreshLayout_progress_bar_color, -1); if (resId == -1) { mProgressColor = ta.getColor(R.styleable.NsRefreshLayout_progress_bar_color, Color.RED); } else { mProgressColor = resources.getColor(resId); } //下拉刷新文字描述 resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_refresh_text, -1); if (resId == -1) { mPullRefreshText = ta.getString(R.styleable.NsRefreshLayout_pull_refresh_text); } else { mPullRefreshText = resources.getString(resId); } //上拉加載文字描述 resId = ta.getResourceId(R.styleable.NsRefreshLayout_pull_load_text, -1); if (resId == -1) { mPullLoadText = ta.getString(R.styleable.NsRefreshLayout_pull_load_text); } else { mPullLoadText = resources.getString(resId); } mAutoLoadMore = ta.getBoolean(R.styleable.NsRefreshLayout_auto_load_more, false); mPullRefreshEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_refresh_enable, true); mPullLoadEnable = ta.getBoolean(R.styleable.NsRefreshLayout_pull_load_enable, true); ta.recycle(); }
在內容View佈局完成後(onFinishInflate),根據設置的屬性,來肯定是否須要添加下拉刷新視圖、上拉加載更多視圖,以及視圖中的文字顏色、進度條顏色等。函數
@Override protected void onFinishInflate() { super.onFinishInflate(); mContentView = getChildAt(0); setupViews(); } private void setupViews() { //下拉刷新視圖 LayoutParams lp; if (mPullRefreshEnable) { lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); headerView = new LoadView(getContext()); headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); headerView.setStartEndTrim(0, 0.75f); headerView.setBackgroundColor(mLoadViewBgColor); headerView.setLoadTextColor(mLoadViewTextColor); headerView.setProgressBgColor(mProgressBgColor); headerView.setProgressColor(mProgressColor); addView(headerView, lp); } if (mPullLoadEnable) { //上拉加載更多視圖 lp = new LayoutParams(LayoutParams.MATCH_PARENT, 0); lp.gravity = Gravity.BOTTOM; footerView = new LoadView(getContext()); footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? getContext().getString(R.string.default_pull_load_text) : mPullLoadText); footerView.setStartEndTrim(0.5f, 1.25f); footerView.setBackgroundColor(mLoadViewBgColor); footerView.setLoadTextColor(mLoadViewTextColor); footerView.setProgressBgColor(mProgressBgColor); footerView.setProgressColor(mProgressColor); addView(footerView, lp); } }
有這樣一種需求,一個列表分頁加載,每一頁10條,若是在上拉加載更多後只返回8條,說明已經沒有更多數據了,因此在列表達到底部,用戶再次上劃時就不須要觸發上拉加載更多了。基於這種需求,我設計了一個接口NsRefreshLayoutController。佈局
public interface NsRefreshLayoutController { /** * 當前下拉刷新是否可用 */ boolean isPullRefreshEnable(); /** * 當前上拉加載是否可用,好比列表已無更多數據,可禁用上拉加載功能 */ boolean isPullLoadEnable(); }
使用時,實現這個接口,根據當前數據的狀況返回True或者False啓用或者禁用兩個功能了。控件內部,咱們在用戶每次觸發觸摸事件的時候獲取接口返回值。post
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (refreshLayoutController != null) { mPullRefreshEnable = refreshLayoutController.isPullRefreshEnable(); mPullLoadEnable = refreshLayoutController.isPullLoadEnable(); } return super.onInterceptTouchEvent(ev); }
咱們須要作到對Touch事件的處理不影響內容視圖的功能,因此咱們只處理Touch事件,不消耗Touch事件,一個合適的回調很重要,找來找去我選擇了dispatchTouchEvent,官方文檔對這個函數的描述以下:動畫
處理Touch事件的流程以下,ACTION_DOWN、ACTION_MOVE時記錄Touch的位置,ACTION_MOVE時用當前Touch的位置減去上次DOWN或者MOVE的位置,獲得手指滑動的距離,用這個距離來控制內容視圖、刷新視圖的顯示位置,當達到觸發刷新的位置後,提示用戶鬆手觸發刷新,用戶鬆手後開始刷新動畫並通知程序開始刷新。代碼以下:ui
@Override public boolean dispatchTouchEvent(MotionEvent event) { if (!mPullRefreshEnable && !mPullLoadEnable) { return super.dispatchTouchEvent(event); } if (isRefreshing) { return super.dispatchTouchEvent(event); } switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: { preY = event.getY(); preX = event.getX(); break; } case MotionEvent.ACTION_MOVE: { float currentY = event.getY(); float currentX = event.getX(); float dy = currentY - preY; float dx = currentX - preX; preY = currentY; preX = currentX; if (!actionDetermined) { //判斷是下拉刷新仍是上拉加載更多 if (dy > 0 && !canChildScrollUp() && mPullRefreshEnable) { mCurrentAction = ACTION_PULL_DOWN_REFRESH; actionDetermined = true; } else if (dy < 0 && !canChildScrollDown() && mPullLoadEnable) { mCurrentAction = ACTION_PULL_UP_LOAD_MORE; actionDetermined = true; } } handleScroll(dy); observerArriveBottom(); break; } case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: { //用戶鬆手後須要判斷當前的滑動距離是否知足觸發刷新的條件 if (releaseTouch()) { MotionEvent cancelEvent = MotionEvent.obtain(event); cancelEvent.setAction(MotionEvent.ACTION_CANCEL); return super.dispatchTouchEvent(cancelEvent); } break; } } return super.dispatchTouchEvent(event); } /** * 處理滾動 */ private boolean handleScroll(float distanceY) { if (!canChildScrollUp() && mCurrentAction == ACTION_PULL_DOWN_REFRESH && mPullRefreshEnable) { //下拉刷新 LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); lp.height += distanceY; if (lp.height < 0) { lp.height = 0; } else if (lp.height > loadingViewOverHeight) { lp.height = (int) loadingViewOverHeight; } headerView.setLayoutParams(lp); if (lp.height < loadingViewOverHeight) { headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); } else { headerView.setLoadText(getContext().getString(R.string.release_to_refresh)); } headerView.setProgressRotation(lp.height / loadingViewOverHeight); adjustContentViewHeight(lp.height); return true; } else if (!canChildScrollDown() && mCurrentAction == ACTION_PULL_UP_LOAD_MORE && mPullLoadEnable) { //上拉加載更多 LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); lp.height -= distanceY; if (lp.height < 0) { lp.height = 0; } else if (lp.height > loadingViewOverHeight) { lp.height = (int) loadingViewOverHeight; } footerView.setLayoutParams(lp); if (lp.height < loadingViewOverHeight) { footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? getContext().getString(R.string.default_pull_load_text) : mPullLoadText); } else { footerView.setLoadText(getContext().getString(R.string.release_to_load)); } footerView.setProgressRotation(lp.height / loadingViewOverHeight); adjustContentViewHeight(-lp.height); return true; } return false; } private void adjustContentViewHeight(float h) { mContentView.setTranslationY(h); //下面的方式能夠看到完整內容,可是有掉幀現象 /*if (mCurrentAction == ACTION_PULL_DOWN_REFRESH) { mContentView.setTranslationY(h); } LayoutParams lp = (LayoutParams) mContentView.getLayoutParams(); lp.height = (int) (getMeasuredHeight() - Math.abs(h)); mContentView.setLayoutParams(lp);*/ } private boolean releaseTouch() { boolean result = false; LayoutParams lp; if (mPullRefreshEnable && mCurrentAction == ACTION_PULL_DOWN_REFRESH) { lp = (LayoutParams) headerView.getLayoutParams(); if (lp.height >= loadingViewOverHeight) { //觸發下拉刷新 startPullDownRefresh(lp.height); result = true; } else if (lp.height > 0) { //未知足下拉刷新觸發條件,重置狀態 resetPullDownRefresh(lp.height); result = lp.height >= CLICK_TOUCH_DEVIATION; } else { resetPullRefreshState(); } } if (mPullLoadEnable && mCurrentAction == ACTION_PULL_UP_LOAD_MORE) { lp = (LayoutParams) footerView.getLayoutParams(); if (lp.height >= loadingViewOverHeight) { //觸發上拉加載更多 startPullUpLoadMore(lp.height); result = true; } else if (lp.height > 0) { //未知足上拉加載更多觸發條件,重置狀態 resetPullUpLoadMore(lp.height); result = lp.height >= CLICK_TOUCH_DEVIATION; } else { resetPullLoadState(); } } return result; } private void startPullDownRefresh(int headerViewHeight) { isRefreshing = true; ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); headerView.setLayoutParams(lp); adjustContentViewHeight(lp.height); } }); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { headerView.start(); headerView.setLoadText(getContext().getString(R.string.refresh_text)); if (refreshLayoutListener != null) { refreshLayoutListener.onRefresh(); } } }); animator.setDuration(300); animator.start(); } /** * 重置下拉刷新狀態 * * @param headerViewHeight 當前下拉刷新視圖的高度 */ private void resetPullDownRefresh(int headerViewHeight) { headerView.stop(); //headerView.setStartEndTrim(0, 0.75f); ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { LayoutParams lp = (LayoutParams) headerView.getLayoutParams(); lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); headerView.setLayoutParams(lp); adjustContentViewHeight(lp.height); } }); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { resetPullRefreshState(); } }); animator.setDuration(300); animator.start(); } private void resetPullRefreshState() { //重置動畫結束纔算徹底完成刷新動做 isRefreshing = false; actionDetermined = false; mCurrentAction = -1; headerView.setLoadText(TextUtils.isEmpty(mPullRefreshText) ? getContext().getString(R.string.default_pull_refresh_text) : mPullRefreshText); } private void startPullUpLoadMore(int headerViewHeight) { isRefreshing = true; ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, loadingViewFinalHeight); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); footerView.setLayoutParams(lp); adjustContentViewHeight(-lp.height); } }); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { footerView.start(); footerView.setLoadText(getContext().getString(R.string.load_text)); if (refreshLayoutListener != null) { refreshLayoutListener.onLoadMore(); } } }); animator.setDuration(300); animator.start(); } /** * 重置下拉刷新狀態 * * @param headerViewHeight 當前下拉刷新視圖的高度 */ private void resetPullUpLoadMore(int headerViewHeight) { footerView.stop(); //footerView.setStartEndTrim(0.5f, 1.25f); ValueAnimator animator = ValueAnimator.ofFloat(headerViewHeight, 0); animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { LayoutParams lp = (LayoutParams) footerView.getLayoutParams(); lp.height = (int) ((Float) animation.getAnimatedValue()).floatValue(); footerView.setLayoutParams(lp); adjustContentViewHeight(-lp.height); } }); animator.addListener(new SimpleAnimatorListener() { @Override public void onAnimationEnd(Animator animation) { resetPullLoadState(); } }); animator.setDuration(300); animator.start(); } private void resetPullLoadState() { //重置動畫結束纔算徹底完成刷新動做 isRefreshing = false; actionDetermined = false; mCurrentAction = -1; footerView.setLoadText(TextUtils.isEmpty(mPullLoadText) ? getContext().getString(R.string.default_pull_load_text) : mPullLoadText); } /** * @return 子視圖是否能夠下拉 */ public boolean canChildScrollUp() { if (mContentView == null) { return false; } if (Build.VERSION.SDK_INT < 14) { if (mContentView instanceof AbsListView) { final AbsListView absListView = (AbsListView) mContentView; return absListView.getChildCount() > 0 && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) .getTop() < absListView.getPaddingTop()); } else { return ViewCompat.canScrollVertically(mContentView, -1) || mContentView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(mContentView, -1); } } /** * @return 子視圖是否能夠上劃 */ public boolean canChildScrollDown() { if (mContentView == null) { return false; } if (Build.VERSION.SDK_INT < 14) { if (mContentView instanceof AbsListView) { final AbsListView absListView = (AbsListView) mContentView; if (absListView.getChildCount() > 0) { int lastChildBottom = absListView.getChildAt(absListView.getChildCount() - 1) .getBottom(); return absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1 && lastChildBottom <= absListView.getMeasuredHeight(); } else { return false; } } else { return ViewCompat.canScrollVertically(mContentView, 1) || mContentView.getScrollY() > 0; } } else { return ViewCompat.canScrollVertically(mContentView, 1); } } public void setRefreshLayoutListener(NsRefreshLayoutListener refreshLayoutListener) { this.refreshLayoutListener = refreshLayoutListener; }
上面代碼中有一個變量CLICK_TOUCH_DEVIATION,這個變量表示對用戶點擊事件的容錯值,用戶進行點擊動做時,會產生很小的滑動距離,若是不作容錯處理會出現刷新視圖抖動出現的問題。
另外還有一個observerArriveBottom(); 這個函數就是處理自動加載更多的關鍵。該函數在Touch事件產生滑動距離後,採起相似輪詢的機制,判斷滑動是否已經中止,滑動事件中止後,根據內容控件當前狀態、用戶配置來肯定是否觸發加載更多事件。代碼以下:
private void observerArriveBottom() { if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { return; } mContentView.getViewTreeObserver().addOnScrollChangedListener( new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged() { mContentView.removeCallbacks(flingRunnable); mContentView.postDelayed(flingRunnable, 6); } }); } private Runnable flingRunnable = new Runnable() { @Override public void run() { if (isRefreshing || !mAutoLoadMore || !mPullLoadEnable) { return; } if (!canChildScrollDown()) { mCurrentAction = ACTION_PULL_UP_LOAD_MORE; isRefreshing = true; startPullUpLoadMore(0); } } };
public interface NsRefreshLayoutListener { void onRefresh(); void onLoadMore(); }
整個控件實現就是這樣的,代碼我已經放到GitHub上了,歡迎你們拍磚。https://github.com/xiaolifan/NsRefreshLayout