編輯:關於Android編程
之前,我們介紹了下拉刷新上拉加載RecyclerView的使用,那麼現在,我們就來說一下這個下拉刷新是怎麼實現的。
在開發過程中,我想了兩種方案。一是使用LinearLayout嵌套頭部、recyclerview、尾部的方式,如下圖:
vcq9oaM8L3A+DQo8cD61q7rzwLSjrM7St8XG+sHL1eK49re9sLijrM6qyrLDtMTYo788L3A+DQo8cD7S8s6qtuC0zrOiytS21HJlY3ljbGVydmlld8Tasr+1xGZsaW5nysK8/r340NC0psDto6zX3MrHtO+yu7W919S8us/r0qq1xNCnufujrM7Sz+vSqrXEysejujxiciAvPg0KscjI57Wxx7DV/dTay6LQwqOsztLP8s/CZmxpbmcgUmVjeWNsZXJWaWV3o6zV4sqxuvJSZWN5Y2xlclZpZXfP8snPufa2r7W9tqWyv7rzo6zKo9Pgy9m2yLzM0PjCtrP2UmVmcmVzaEhlYWRlcqOstvjH0s7SsrvPsru2w7+0zra8yKvCtrP2wLSjrLb4ysfSqrjDwra24MnZvs3Ctrbgydmho7zytaW12Mu1o6y+zcrHztLP69KquPjIy9K71tbLotDCzbeyv77NysfBpcr009pSZWN5Y2xlclZpZXe1xKGisru05tTats+y47XEuNC+9aGjPC9wPg0KPHA+tvejrLauztLS4su8wvCjv6OouNW41cXCse2077K7x+Wz/qOszNi12LDRzazKwr3QwLS/tMv7tq6yu7auo6k8L3A+DQo8cD7X3Nauo6zV4tbWt72wuLSmwO21xNCnufvO0rK7wvrS4qOhxMfU9cO0sOzE2KO/1tjAtLDJo6zJvrT6wuso0MTU2rXO0aopoaM8L3A+DQo8cD7T2srH09DBy7Xatv7W1re9sLijujxzdHJvbmc+uPhSZWN5Y2xlclZpZXfM7bzTwb249s23sr+jrLfWsfDKx6O608PT2tTss8nPwsCt0Ke5+7XEuKjW+s23sr+hosui0MLNt7K/o7vM7bzTwb249s6ysr+jrLfWsfDKx6O6vNPU2M6ysr+jrNPD09rU7LPJyc/ArdCnufu1xLio1vrOsrK/oaO1sbustq+1vbalsr/KsaOsuMSx5Lio1vrNt7K/tcS437bIo6yw0cbky/tpdGVtzfnPws3Go6zU7LPJz8LArbXEuNC+9aO7yc/Arc2swO2hozwvc3Ryb25nPjwvcD4NCjxwPs7Su7nKx9TZu6249s28sMmjujwvcD4NCjxwPjxpbWcgYWx0PQ=="第二種方案" src="/uploadfile/Collfiles/20161105/20161105095117268.png" title="\" />
在onLayout中,通過設置RecyclerView的margin,將頭部和尾部偏移出屏幕; 輔助頭部:初始高度為1px;當RecyclerView滑動到頂部時,通過改變高度,造成下拉效果; 輔助尾部:初始高度為1px;當RecyclerView滑動到底部時,通過改變高度,造成上拉的效果思路就是這樣,但在實際的開發過程中,下拉還好,而上拉會遇到各種各樣的問題,不過好在解決了這些問題後,實際的效果完美符合我的要求,所以WZMRecyclerView采用了這個方案進行實現。
接下來我們來依次介紹下拉和上拉,以及開發過程中遇到的問題。
其實下拉刷新是比較簡單的,PullToRefreshRecyclerView繼承於HeaderAndFooterRecyclerView,我們按順序來一一介紹PullToRefreshRecyclerView中的幾個主要方法:
首先介紹下全局變量,免得看代碼的時候吃力:// 當前狀態 private int mState = STATE_DEFAULT; // 初始 public final static int STATE_DEFAULT = 0; // 正在下拉 public final static int STATE_PULLING = 1; // 松手刷新 public final static int STATE_RELEASE_TO_REFRESH = 2; // 刷新中 public final static int STATE_REFRESHING = 3; // 下拉阻尼系數 private float mPullRatio = 0.5f; // 輔助頭部 private View topView; // 刷新頭部 private View mRefreshView; // 刷新頭部的高度 private int mRefreshViewHeight = 0; // 觸摸事件輔助,當RecyclerView滑動到頂部時,記錄觸摸事件的y軸坐標 private float mFirstY = 0; // 當前是否正在下拉 private boolean mPulling = false; // 是否可以下拉刷新 private boolean mRefreshEnable = true; // 回彈動畫 private ValueAnimator valueAnimator; // 刷新監聽 private OnRefreshListener mOnRefreshListener; // 刷新頭部構造器 private RefreshHeaderCreator mRefreshHeaderCreator;在構造函數中初始化,獲得默認的刷新頭部:
private void init(Context context) { if (topView == null) { topView = new View(context); // 該view的高度不能為0,否則將無法判斷是否已滑動到頂部 topView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, 1)); // 設置默認LayoutManager setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)); // 初始化默認的刷新頭部 mRefreshHeaderCreator = new DefaultRefreshHeaderCreator(); mRefreshView = mRefreshHeaderCreator.getRefreshView(context,this); } }在onLayout方法中,獲得刷新頭部的高度,並偏移RecyclerView:
/** * 在layout的時候,隱藏刷新頭部 */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mRefreshView != null && mRefreshViewHeight == 0) { mRefreshViewHeight = mRefreshView.getMeasuredHeight(); ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams(); marginLayoutParams.setMargins(marginLayoutParams.leftMargin, marginLayoutParams.topMargin-mRefreshViewHeight-1, marginLayoutParams.rightMargin, marginLayoutParams.bottomMargin); setLayoutParams(marginLayoutParams); } }觸摸事件:
@Override public boolean onTouchEvent(MotionEvent e) { // 若是不可以下拉 if (!mRefreshEnable) return super.onTouchEvent(e); // 若刷新頭部為空,不處理 if (mRefreshView == null) return super.onTouchEvent(e); // 若回彈動畫正在進行,不處理 if (valueAnimator != null && valueAnimator.isRunning()) return super.onTouchEvent(e); switch (e.getAction()) { case MotionEvent.ACTION_MOVE: if (!mPulling) { if (isTop()) { // 當listview滑動到最頂部時,記錄當前y坐標 mFirstY = e.getRawY(); } // 若listview沒有滑動到最頂部,不處理 else break; } float distance = (int) ((e.getRawY() - mFirstY)*mPullRatio); // 若向上滑動(此時刷新頭部已隱藏),不處理 if (distance < 0) break; mPulling = true; // 若刷新中,距離需加上頭部的高度 if (mState == STATE_REFRESHING) { distance += mRefreshViewHeight; } // 下拉 setState(distance); return true; case MotionEvent.ACTION_UP: // 回彈 replyPull(); break; } return super.onTouchEvent(e); }判斷是否滑動到了頂部:
private boolean isTop() { return !ViewCompat.canScrollVertically(this, -1); }設置當前下拉狀態:
private void setState(float distance) { // 刷新中,狀態不變 if (mState == STATE_REFRESHING) { } else if (distance == 0) { mState = STATE_DEFAULT; } // 松手刷新 else if (distance >= mRefreshViewHeight) { int lastState = mState; mState = STATE_RELEASE_TO_REFRESH; if (mRefreshHeaderCreator != null) if (!mRefreshHeaderCreator.onReleaseToRefresh(distance,lastState)) return; } // 正在拖動 else if (distance < mRefreshViewHeight) { int lastState = mState; mState = STATE_PULLING; if (mRefreshHeaderCreator != null) if (!mRefreshHeaderCreator.onStartPull(distance,lastState)) return; } // 開始下拉 startPull(distance); }
這裡可以看到,當頭部構造器的onStartPull和onReleaseToRefresh返回false時,便不再下拉,其實這裡也是為了應對類似“超過多少就不再下拉了”這種需求。
改變輔助頭部的高度,造成下拉的效果:private void startPull(float distance) { // 輔助頭部的高度不能為0,否則將無法判斷是否已滑動到頂部 if (distance < 1) distance = 1; if (topView != null) { LayoutParams layoutParams = (LayoutParams) topView.getLayoutParams(); layoutParams.height = (int) distance; topView.setLayoutParams(layoutParams); } }松手回彈,在這個方法中,我們需要判斷是直接刷新,還是直接回彈到原來位置:
private void replyPull() { mPulling = false; // 回彈位置 float destinationY = 0; // 判斷當前狀態 // 若是刷新中,回彈 if (mState == STATE_REFRESHING) { destinationY = mRefreshViewHeight; } // 若是松手刷新,刷新,回彈 else if (mState == STATE_RELEASE_TO_REFRESH) { // 改變狀態 mState = STATE_REFRESHING; // 刷新 if (mRefreshHeaderCreator != null) mRefreshHeaderCreator.onStartRefreshing(); if (mOnRefreshListener != null) mOnRefreshListener.onStartRefreshing(); // 若在onStartRefreshing中調用了completeRefresh方法,將不會滾回初始位置,因此這裡需加個判斷 if (mState != STATE_REFRESHING) return; destinationY = mRefreshViewHeight; } else if (mState == STATE_DEFAULT || mState == STATE_PULLING) { mState = STATE_DEFAULT; } LayoutParams layoutParams = (RecyclerView.LayoutParams) topView.getLayoutParams(); float distance = layoutParams.height; if (distance <= 0) return; valueAnimator = ObjectAnimator.ofFloat(distance, destinationY).setDuration((long) (distance * 0.5)); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float nowDistance = (float) animation.getAnimatedValue(); startPull(nowDistance); } }); valueAnimator.start(); }完成刷新:
public void completeRefresh() { if (mRefreshHeaderCreator != null) mRefreshHeaderCreator.onStopRefresh(); mState = STATE_DEFAULT; replyPull(); mRealAdapter.notifyDataSetChanged(); }在設置適配器的時候,添加輔助頭部和刷新頭部:
@Override public void setAdapter(Adapter adapter) { super.setAdapter(adapter); if (mRefreshView != null) { addHeaderView(topView); addHeaderView(mRefreshView); } }設置自定義的頭部:
public void setRefreshViewCreator(RefreshHeaderCreator refreshHeaderCreator) { this.mRefreshHeaderCreator = refreshHeaderCreator; mRefreshView = refreshHeaderCreator.getRefreshView(getContext(),this); // 若有適配器,添加到頭部 if (mAdapter != null) { addHeaderView(topView); addHeaderView(mRefreshView); } mRealAdapter.notifyDataSetChanged(); }
以上就是PullToRefreshRecyclerView主要的幾個方法了,介紹得算比較清楚吧,再加上代碼中已經有注釋了,就不再累贅了。核心就一句話:攔截觸摸事件,改變輔助頭部的高度。 就是這麼easy~~~~
本來上拉加載我想單獨用一篇文章來介紹的,但其實上拉加載的處理和下拉刷新的處理邏輯是一致的,因此在這裡便一起介紹了吧,雙飛更開心呦客官~~
咳咳,說正經的,上面我們說過上拉加載會遇到各種問題,具體有哪些呢?
我們知道偏移RecyclerView是在onLayout函數中,但是在這個時候,你是拿不到加載尾部的高度的,measure(0,0)都沒用,為什麼呢?因為這個時候還不到他出場的時候啊,你催他也沒用。這時候你就會說了,那我getViewTreeObserver().addOnPreDrawListener呢?嘿嘿,我也試過了,這樣的確可以拿到高度,但太晚了,已經來不及偏移了,他已經出現在屏幕中了。
滑動到底部時,繼續上拉,改變輔助底部的高度造成上拉的效果,然後現實很骨感,你會發現(通過調試或打印)輔助底部的高度是在改變,但RecyclerView中的item並沒有擠上去啊,根本就沒有上拉的效果出現。
當你添加FooterView的時候,發現你添加的FooterView居然跑到刷新底部的下面去了,坑了個爹…..
以下是我的解決方法:
在開發下拉刷新的時候,我們並沒有這個問題,很明顯,因為我們的刷新頭部其實是第一二個item,在onLayout的時候,肯定會去測量他的寬高(onMeasure方法在onLayout之前),所以我們可以拿到刷新頭部的高度。這麼一來的話,我們可以把加載尾部添加到頭部中去,等得到了高度,我們再卸磨殺驢,把他remove掉,恩,就是這樣。
這個問題我實在沒想到什麼好辦法,因此用了最粗暴的方式:在改變高度後直接調用scrollToPosition滾動到最底部。這樣做有什麼後果呢?效率肯定是不高的,但為了效果,我可以忍….經過測試,StaggredLayoutManager不會有任何影響,效果溜溜哒。但是但是,LinearLayoutManager上拉時會出現卡頓的現象,這個怎麼忍!當然GridLayoutManager也會卡頓,畢竟他是LinearLayoutManager的兒子啊,遺傳病。為什麼呢?因為LinearLayoutManager對item的layout和StaggredLayoutManager的是不一樣的,既然StaggredLayoutManager沒問題,那麼我們用只有一列的StaggredLayoutManager替代LinearLayoutManager就是最粗暴的方法。當然,更好的方式是直接繼承LayoutManager寫一個自己的LinearLayoutManager,但由於時間和水平的限制,就……采用StaggredLayoutManager吧。這就是為什麼我之前說使用PullToLoadRecyclerView的時候,要用WZMLinearLayout和WZMGridLayoutManager。
這個問題其實最好解決,繼承HeaderAndFooterAdapter寫一個PullToLoadAdapter就可以啦。
雖然解決方法比較坑爹,但不管黑貓還是白貓,能抓老鼠的就是好貓。當然,這麼說有點過分了,所以在這裡,希望有大牛有更好的方法,歡迎到github上提交您的代碼,共同構建這個項目。
PullToLoadRecyclerView和PullToRefreshRecyclerView的代碼邏輯其實基本一致,而PullToLoadAdapter的代碼和HeaderAndFooterAdapter也比較像,因此這裡就不再展開了,有興趣的同學可以去github上把項目clone下來看看。
有沒有遇到過這種情況,當你辛辛苦苦找到一個需要的庫時,卻發現他的UI居然不支持自定義!摔!在實際開發中,產品和設計怎麼會允許你使用那個庫默認的UI設計,這是基本不可能的事。因此,支持自定義的刷新頭部和加載尾部是非常非常重要的事!!
之前在介紹使用方法時,我們就已經介紹了如何使用自定義的刷新頭部和加載尾部,而通過上面的代碼,你應該也已經理解了RefreshHeaderCreator和LoadFooterCreator的工作方式。
其實就是使用這兩個抽象類,把刷新頭部和加載尾部的UI與RecyclerView進行解耦,交給用戶自己去實現,項目中的默認刷新頭部和加載尾部就是很好的例子,相信你看完應該就知道怎麼去構造自己的刷新頭部和加載尾部了。
直接上DefaultRefreshHeaderCreator的代碼:
public class DefaultRefreshHeaderCreator extends RefreshHeaderCreator { private View mRefreshView; private ImageView iv; private TextView tv; private int rotationDuration = 200; private int loadingDuration = 1000; private ValueAnimator ivAnim; @Override public boolean onStartPull(float distance,int lastState) { if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) { iv.setImageResource(R.drawable.arrow_down); iv.setRotation(0f); tv.setText("下拉刷新"); } else if (lastState == PullToRefreshRecyclerView.STATE_RELEASE_TO_REFRESH) { startArrowAnim(0); tv.setText("下拉刷新"); } return true; } @Override public void onStopRefresh() { if (ivAnim != null) { ivAnim.cancel(); } } @Override public boolean onReleaseToRefresh(float distance,int lastState) { if (lastState == PullToRefreshRecyclerView.STATE_DEFAULT ) { iv.setImageResource(R.drawable.arrow_down); iv.setRotation(-180f); tv.setText("松手立即刷新"); } else if (lastState == PullToRefreshRecyclerView.STATE_PULLING) { startArrowAnim(-180f); tv.setText("松手立即刷新"); } return true; } @Override public void onStartRefreshing() { iv.setImageResource(R.drawable.loading); startLoadingAnim(); tv.setText("正在刷新..."); } @Override public View getRefreshView(Context context, RecyclerView recyclerView) { mRefreshView = LayoutInflater.from(context).inflate(R.layout.layout_ptr_ptl,recyclerView,false); iv = (ImageView) mRefreshView.findViewById(R.id.iv); tv = (TextView) mRefreshView.findViewById(R.id.tv); return mRefreshView; } private void startArrowAnim(float roration) { if (ivAnim != null) { ivAnim.cancel(); } float startRotation = iv.getRotation(); ivAnim = ObjectAnimator.ofFloat(startRotation,roration).setDuration(rotationDuration); ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { iv.setRotation((Float) animation.getAnimatedValue()); } }); ivAnim.start(); } private void startLoadingAnim() { if (ivAnim != null) { ivAnim.cancel(); } ivAnim = ObjectAnimator.ofFloat(0,360).setDuration(loadingDuration); ivAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { iv.setRotation((Float) animation.getAnimatedValue()); } }); ivAnim.setRepeatMode(ObjectAnimator.RESTART); ivAnim.setRepeatCount(ObjectAnimator.INFINITE); ivAnim.setInterpolator(new LinearInterpolator()); ivAnim.start(); } }
系不系很簡單?
照例上兩張用爛了的效果圖:
源碼地址:https://github.com/whichname/WZMRecyclerView
GridView跟ListView都是比較常用的多控件布局,而GridView更是實現九宮圖的首選!本文就是介紹如何使用GridView實現九宮圖。GridView的用法
今天這一篇小案例模擬模糊查詢,即輸入一個字符,顯示手機對應的所有存在該字符的路徑。布局:
本文實例講述了Android編程開發之seekBar采用handler消息處理操作的方法。分享給大家供大家參考,具體如下:該案例簡單實現進度條可走,可拖拽的功能,下面請看
一,為什麼說是真正的高仿? 闡述這個問題前,先說下之前網上的,各位可以復制這段字,去百度一下 仿微信打開網頁的進度條效果 ,你會看到有很多類似的文章,不過他