最近寫了個下拉控件,和幾個下拉的頭部樣式,下拉控件能夠連續添加疊加幾個頭部視圖java
下面是沒有添加任何頭部尾部的視圖下拉效果android
一步一步來介紹,先介紹這個下拉效果,在介紹自定義的頭部git
首先在使用上,和普通的控件沒有兩樣,拿recyclerview來作例子,由於recyclerview使用比較多,並且能夠替代不少的列表控件github
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <com.fragmentapp.view.refresh.RefreshLayout android:layout_width="match_parent" android:layout_height="match_parent" app:_height="@dimen/dp_60" android:id="@+id/refreshLayout" > <android.support.v7.widget.RecyclerView android:id="@+id/recyclerView" android:divider="@color/color_e1e1e1" android:dividerHeight="10dp" android:background="#ffffff" android:layout_width="match_parent" android:layout_height="match_parent" /> </com.fragmentapp.view.refresh.RefreshLayout> </LinearLayout>
protected void init() { for (int i = 0; i < 20; i++) { list.add("" + i); } recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setAdapter(new HomeAdapter(getActivity(), R.layout.item_home, list)); refreshLayout .setCallBack(new RefreshLayout.CallBack() { @Override public void refreshHeaderView(int state, String stateVal) { switch (state) { case RefreshLayout.DOWN_REFRESH: // 下拉刷新狀態 break; case RefreshLayout.RELEASE_REFRESH: // 鬆開刷新狀態 break; case RefreshLayout.LOADING: // 正在刷新中狀態 break; } } @Override public void pullListener(int y) { } }); }
1 package com.fragmentapp.view.refresh; 2 3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.graphics.Color; 6 import android.graphics.Rect; 7 import android.support.v4.view.ViewCompat; 8 import android.support.v7.widget.LinearLayoutManager; 9 import android.support.v7.widget.RecyclerView; 10 import android.support.v7.widget.StaggeredGridLayoutManager; 11 import android.util.AttributeSet; 12 import android.util.Log; 13 import android.view.Gravity; 14 import android.view.MotionEvent; 15 import android.view.View; 16 import android.view.ViewConfiguration; 17 import android.widget.AbsListView; 18 import android.widget.FrameLayout; 19 20 import com.fragmentapp.R; 21 22 import java.lang.reflect.Field; 23 import java.util.ArrayList; 24 import java.util.List; 25 26 /** 27 * Created by LiuZhen on 2017/3/24. 28 */ 29 public class RefreshLayout extends FrameLayout { 30 31 private String TAG = "tag"; 32 private int downY;// 按下時y軸的偏移量 33 private final static float RATIO = 3f; 34 //頭部的高度 35 protected int mHeadHeight = 120; 36 //頭部layout 37 protected FrameLayout mHeadLayout,mFootLayout;//頭部容器 38 private List<IHeadView> heads = new ArrayList<>();//支持添加多個頭部 39 private List<IFootView> foots = new ArrayList<>(); 40 41 public static final int DOWN_REFRESH = 0;// 下拉刷新狀態 42 public static final int RELEASE_REFRESH = 1;// 鬆開刷新 43 public static final int LOADING = 2;// 正在刷新中 44 private int currentState = DOWN_REFRESH;// 頭佈局的狀態: 默認爲下拉刷新狀態 45 46 private View list;//子節點中的 recyclerview 視圖 47 private LayoutParams listParam,footParam;//用於控制下拉動畫展現 48 private boolean isLoadingMore = false;// 是否進入加載狀態,防止屢次重複的啓動 49 private boolean isStart = false;//表示正在加載刷新中,還沒中止 50 private boolean isTop = false,isBottom = false; 51 private int mTouchSlop; 52 private CallBack callBack; 53 54 public RefreshLayout(Context context) { 55 this(context, null, 0); 56 } 57 58 public RefreshLayout(Context context, AttributeSet attrs) { 59 this(context, attrs, 0); 60 } 61 62 public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { 63 super(context, attrs, defStyleAttr); 64 65 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshLayout, defStyleAttr, 0); 66 try { 67 mHeadHeight = a.getDimensionPixelSize(R.styleable.RefreshLayout__height, 120); 68 } finally { 69 a.recycle(); 70 } 71 mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 72 73 init(); 74 } 75 76 private void initHeaderContainer() { 77 mHeadLayout = new FrameLayout(getContext()); 78 LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); 79 this.addView(mHeadLayout,layoutParams); 80 } 81 82 public void initFootContainer() { 83 footParam = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); 84 mFootLayout = new FrameLayout(getContext());//底部佈局 85 mFootLayout.setBackgroundColor(Color.BLACK); 86 footParam.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 87 footParam.setMargins(0,0,0,-mHeadHeight); 88 this.addView(mFootLayout,footParam); 89 } 90 91 private void init(){ 92 initHeaderContainer(); 93 } 94 95 @Override 96 protected void onFinishInflate() {//佈局加載成xml時觸發 97 super.onFinishInflate(); 98 99 initFootContainer(); 100 if (list == null) { 101 list = getChildAt(1); 102 listParam = (LayoutParams) list.getLayoutParams(); 103 list.setOnTouchListener(new OnTouchListener() { 104 @Override 105 public boolean onTouch(View v, MotionEvent event) { 106 return onFingerTouch(event); 107 } 108 }); 109 } 110 } 111 112 /** 113 * 設置頭部View 114 */ 115 public RefreshLayout setHeaderView(final IHeadView headView) { 116 if (headView != null) { 117 mHeadLayout.addView(headView.getView()); 118 heads.add(headView); 119 } 120 return this; 121 } 122 123 /** 124 * 設置尾部View 125 */ 126 public RefreshLayout setFootView(final IFootView footView) { 127 if (footView != null) { 128 mFootLayout.addView(footView.getView()); 129 foots.add(footView); 130 } 131 return this; 132 } 133 134 public boolean onFingerTouch(MotionEvent ev) { 135 isTop = isViewToTop(list,mTouchSlop); 136 isBottom = isViewToBottom(list,mTouchSlop); 137 // Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom); 138 switch (ev.getAction()) { 139 case MotionEvent.ACTION_DOWN : 140 currentState = LOADING; 141 downY = (int) ev.getY(); 142 break; 143 case MotionEvent.ACTION_MOVE : 144 if (!isTop && !isBottom)//沒有到頂,無需計算操做 145 break; 146 int moveY = (int) ev.getY(); 147 int diff = (int) (((float)moveY - (float)downY) / RATIO); 148 // int paddingTop = -mHeadLayout.getHeight() + diff; 149 int paddingTop = diff; 150 if (paddingTop>0 && isTop) { 151 //向下滑動多少後開始啓動刷新,Margin判斷是爲了限制快速用力滑動的時候致使頭部侵入的高度不夠就開始加載了 152 if (paddingTop >= mHeadHeight && (listParam.topMargin >= mHeadHeight) && currentState == DOWN_REFRESH) { // 徹底顯示了. 153 // Log.i(TAG, "鬆開刷新 RELEASE_REFRESH"); 154 currentState = RELEASE_REFRESH; 155 refreshHeaderView(); 156 start(); 157 } else if (currentState == LOADING) { // 沒有顯示徹底 158 // Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH"); 159 currentState = DOWN_REFRESH; 160 refreshHeaderView(); 161 } 162 if (paddingTop <= (mHeadHeight+10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置 163 listParam.setMargins(0, paddingTop, 0, 0); 164 list.setLayoutParams(listParam); 165 if (callBack != null) 166 callBack.pullListener(paddingTop); 167 } 168 169 }else if (isBottom){ 170 //限制上滑時不能超過底部的寬度,否則會超出邊界 171 //mHeadHeight+20 上滑設置的margin要超過headheight,否則下面判斷的大於headheight不成立,下面的margin基礎上面設置後的參數 172 if (Math.abs(paddingTop) <= (mHeadHeight+10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置 173 listParam.setMargins(0, 0, 0, -paddingTop); 174 footParam.setMargins(0,0,0,-paddingTop-mHeadHeight); 175 list.setLayoutParams(listParam); 176 } 177 //若是滑動的距離大於頭部或者底部的高度,而且設置的margin也大於headheight 178 //listParam用來限制recyclerview列表迅速滑動,footParam用來限制bottom foothead迅速滑動致使沒有達到head的高度就開始加載了 179 if (Math.abs(paddingTop) >= mHeadHeight && (listParam.bottomMargin >= mHeadHeight || footParam.bottomMargin >= 0)) 180 isLoadingMore = true;//頭部是否拉取到位,而後執行加載動畫 181 182 } 183 // Log.e(TAG,"paddingTop "+paddingTop +" mHeadHeight "+mHeadHeight+ " topMargin "+listParam.topMargin+" bottomMargin "+listParam.bottomMargin 184 // +" footParam bottom "+footParam.bottomMargin); 185 // Log.i(TAG,"paddingTop "+paddingTop); 186 break; 187 case MotionEvent.ACTION_UP : 188 currentState = LOADING; 189 refreshHeaderView(); 190 if (isLoadingMore){ 191 isLoadingMore = false; 192 isStart = true;//是否開始加載 193 postDelayed(new Runnable() { 194 @Override 195 public void run() { 196 // Log.i(TAG, "中止 END"); 197 // currentState = END; 198 refreshHeaderView(); 199 listParam.setMargins(0, 0, 0, 0); 200 footParam.setMargins(0,0,0,-mHeadHeight); 201 list.setLayoutParams(listParam); 202 stop(); 203 } 204 },2000); 205 }else{ 206 if (!isStart){ 207 // 隱藏頭佈局 208 listParam.setMargins(0, 0,0,0); 209 footParam.setMargins(0,0,0,-mHeadHeight); 210 list.setLayoutParams(listParam); 211 } 212 } 213 // Log.i(TAG, "鬆開 REFRESHING"); 214 break; 215 default : 216 break; 217 } 218 return super.onTouchEvent(ev); 219 } 220 221 /** 222 * 根據currentState刷新頭佈局的狀態 223 */ 224 private void refreshHeaderView() { 225 if (callBack == null || isStart) 226 return; 227 String val = "準備刷新"; 228 switch (currentState) { 229 case DOWN_REFRESH : // 下拉刷新狀態 230 val = "下拉刷新"; 231 break; 232 case RELEASE_REFRESH : // 鬆開刷新狀態 233 val = "開始刷新..."; 234 break; 235 case LOADING : // 正在刷新中狀態 236 val = "正在刷新中..."; 237 break; 238 } 239 callBack.refreshHeaderView(currentState,val); 240 } 241 242 public static boolean isViewToTop(View view, int mTouchSlop){ 243 if (view instanceof AbsListView) return isAbsListViewToTop((AbsListView) view); 244 if (view instanceof RecyclerView) return isRecyclerViewToTop((RecyclerView) view); 245 return (view != null && Math.abs(view.getScrollY()) <= 2 * mTouchSlop); 246 } 247 248 public static boolean isViewToBottom(View view, int mTouchSlop){ 249 if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view); 250 if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view); 251 // if (view instanceof WebView) return isWebViewToBottom((WebView) view,mTouchSlop); 252 // if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view); 253 return false; 254 } 255 256 public static boolean isAbsListViewToTop(AbsListView absListView) { 257 if (absListView != null) { 258 int firstChildTop = 0; 259 if (absListView.getChildCount() > 0) { 260 // 若是AdapterView的子控件數量不爲0,獲取第一個子控件的top 261 firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop(); 262 } 263 if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) { 264 return true; 265 } 266 } 267 return false; 268 } 269 270 public static boolean isRecyclerViewToTop(RecyclerView recyclerView) { 271 if (recyclerView != null) { 272 RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); 273 if (manager == null) { 274 return true; 275 } 276 if (manager.getItemCount() == 0) { 277 return true; 278 } 279 280 if (manager instanceof LinearLayoutManager) { 281 LinearLayoutManager layoutManager = (LinearLayoutManager) manager; 282 283 int firstChildTop = 0; 284 if (recyclerView.getChildCount() > 0) { 285 // 處理item高度超過一屏幕時的狀況 286 View firstVisibleChild = recyclerView.getChildAt(0); 287 if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { 288 if (android.os.Build.VERSION.SDK_INT < 14) { 289 return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0); 290 } else { 291 return !ViewCompat.canScrollVertically(recyclerView, -1); 292 } 293 } 294 295 // 若是RecyclerView的子控件數量不爲0,獲取第一個子控件的top 296 297 // 解決item的topMargin不爲0時不能觸發下拉刷新 298 View firstChild = recyclerView.getChildAt(0); 299 RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams(); 300 firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop(); 301 } 302 303 if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) { 304 return true; 305 } 306 } else if (manager instanceof StaggeredGridLayoutManager) { 307 StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; 308 309 int[] out = layoutManager.findFirstCompletelyVisibleItemPositions(null); 310 if (out[0] < 1) { 311 return true; 312 } 313 } 314 } 315 return false; 316 } 317 318 /** 319 * 經過反射獲取RecyclerView的item的topInset 320 */ 321 private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) { 322 try { 323 Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets"); 324 field.setAccessible(true); 325 // 開發者自定義的滾動監聽器 326 Rect decorInsets = (Rect) field.get(layoutParams); 327 return decorInsets.top; 328 } catch (Exception e) { 329 e.printStackTrace(); 330 } 331 return 0; 332 } 333 334 public static boolean isAbsListViewToBottom(AbsListView absListView) { 335 if (absListView != null && absListView.getAdapter() != null && absListView.getChildCount() > 0 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1) { 336 View lastChild = absListView.getChildAt(absListView.getChildCount() - 1); 337 338 return lastChild.getBottom() <= absListView.getMeasuredHeight(); 339 } 340 return false; 341 } 342 343 public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) { 344 if (recyclerView != null) { 345 RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); 346 if (manager == null || manager.getItemCount() == 0) { 347 return false; 348 } 349 350 if (manager instanceof LinearLayoutManager) { 351 // 處理item高度超過一屏幕時的狀況 352 View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1); 353 if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { 354 if (android.os.Build.VERSION.SDK_INT < 14) { 355 return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0); 356 } else { 357 return !ViewCompat.canScrollVertically(recyclerView, 1); 358 } 359 } 360 361 LinearLayoutManager layoutManager = (LinearLayoutManager) manager; 362 if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) { 363 return true; 364 } 365 } else if (manager instanceof StaggeredGridLayoutManager) { 366 StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; 367 368 int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null); 369 int lastPosition = layoutManager.getItemCount() - 1; 370 for (int position : out) { 371 if (position == lastPosition) { 372 return true; 373 } 374 } 375 } 376 } 377 return false; 378 } 379 380 public void start(){ 381 isLoadingMore = true; 382 for (IHeadView head : heads) { 383 head.startAnim(); 384 } 385 } 386 387 public void stop(){ 388 isLoadingMore = false; 389 isStart = false; 390 for (IHeadView head : heads) { 391 head.stopAnim(); 392 } 393 } 394 395 public RefreshLayout setCallBack(CallBack callBack) { 396 this.callBack = callBack; 397 return this; 398 } 399 400 public interface CallBack{ 401 /**監聽下拉時的狀態*/ 402 void refreshHeaderView(int state,String stateVal); 403 /**監聽下拉時的距離*/ 404 void pullListener(int y); 405 } 406 407 408 }
註釋估計也沒有我寫的這麼詳細的了,相信不是徹底不懂的話確定一眼就看懂了canvas
至於 refreshLayout 這個下拉控件裏面的代碼是完整的,沒有去截取,不過寫了很是很是詳細的註釋,可是仍是大體的講下實現的流程網絡
首先是繼承一個佈局,方面添加各類節點,比較直接view的話很難實現,代碼量也多,傷不起,也沒時間app
實現構造函數後初始化各類操做,這裏給頭部的下拉提供一個容器,由於須要疊加添加頭部,因此須要一個父容器去控制子頭部的一個添加和刪除的控制,有了這個佈局,那麼就能夠控制這個容器去作一些下拉的效果,還有往裏面添加一些頭部效果了ide
初始化後咱們須要獲取recyclerview控件了,由於須要監聽它的一些操做和狀態,這裏不讓它傳進去,由於這樣依賴性太強了,不傳進來也能夠很好的獲取,比較是子view視圖,能夠直接經過getChildAt獲取函數
獲取到recyclerview那麼能夠監聽它的一個touch事件,而這裏的list是一個view類型,至於爲何不是recycyclerview類型我想就不用多說了,若是直接是recyclerview類型的話那我跟直接傳參數進來就沒什麼兩樣了,這樣的作法就沒意義了工具
重點就是這個監聽事件了,想一想也知道,下拉上拉全部的效果都須要去監聽它的一個滑動狀態來判斷的,因此核心的操做基本在這裏面了
public boolean onFingerTouch(MotionEvent ev) {
isTop = isViewToTop(list, mTouchSlop);
isBottom = isViewToBottom(list, mTouchSlop);
// Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom);
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
currentState = LOADING;
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE:
if (!isTop && !isBottom)//沒有到頂,無需計算操做
break;
int moveY = (int) ev.getY();
/**
* 獲得滑動的距離,能夠打印出日誌查看,若是滑動的速度很快,那麼獲得距離的跨度會很大,好比快速滑動,可能只獲得三個數據,50,400,700,而若是緩慢的
* 滑動,則30,35,39,41...696,700,顯然咱們想獲得的是後面的數據,不然跨度很大會出現閃速的現象
* */
int diff = moveY - downY;
/**這裏是爲了下降跨度存在的,儘可能的讓數值跨度不要那麼大*/
int paddingTop = diff / RATIO;
if (paddingTop > 0 && isTop) {
//向下滑動多少後開始啓動刷新,Margin判斷是爲了限制快速用力滑動的時候致使頭部侵入的高度不夠就開始加載了
if (paddingTop >= mHeadHeight && (listParam.topMargin >= mHeadHeight) && currentState == DOWN_REFRESH) { // 徹底顯示了.
// Log.i(TAG, "鬆開刷新 RELEASE_REFRESH");
currentState = RELEASE_REFRESH;
refreshHeaderView();
start();
} else if (currentState == LOADING) { // 沒有顯示徹底
// Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH");
currentState = DOWN_REFRESH;
refreshHeaderView();
}
if (paddingTop <= (mHeadHeight + 10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置
listParam.setMargins(0, paddingTop, 0, 0);
list.setLayoutParams(listParam);
if (callBack != null)
callBack.pullListener(paddingTop);
}
} else if (isBottom) {
//限制上滑時不能超過底部的寬度,否則會超出邊界
//mHeadHeight+20 上滑設置的margin要超過headheight,否則下面判斷的大於headheight不成立,下面的margin基礎上面設置後的參數
if (Math.abs(paddingTop) <= (mHeadHeight + 10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置
listParam.setMargins(0, 0, 0, -paddingTop);
footParam.setMargins(0, 0, 0, -paddingTop - mHeadHeight);
list.setLayoutParams(listParam);
}
//若是滑動的距離大於頭部或者底部的高度,而且設置的margin也大於headheight
//listParam用來限制recyclerview列表迅速滑動,footParam用來限制bottom foothead迅速滑動致使沒有達到head的高度就開始加載了
if (Math.abs(paddingTop) >= mHeadHeight && (listParam.bottomMargin >= mHeadHeight || footParam.bottomMargin >= 0))
isLoadingMore = true;//頭部是否拉取到位,而後執行加載動畫
}
break;
case MotionEvent.ACTION_UP:
if (isLoadingMore) {//是否開始加載
currentState = LOADING;
refreshHeaderView();
isLoadingMore = false;
isStart = true;//是否開始加載
// postDelayed(new Runnable() {//沒有網絡訪問時固定2秒加載完成
// @Override
// public void run() {
//// Log.i(TAG, "中止 END");
//// currentState = END;
// stop();
// }
// },2000);
} else {
if (!isStart) {
// 隱藏頭佈局
listParam.setMargins(0, 0, 0, 0);
footParam.setMargins(0, 0, 0, -mHeadHeight);
list.setLayoutParams(listParam);
}
}
// Log.i(TAG, "鬆開 REFRESHING");
break;
default:
break;
}
return super.onTouchEvent(ev);
}
註釋沒得說,大體的一個流程就是分下拉和下拉的判斷
1:按下後獲取getY,就是y軸座標,也就是起點,後面用來計算滑動的距離
2:手指滑動,一樣在獲取getY,而後相減,獲得滑動的距離Y,而後上面recyclerview對象的做用來了,這裏的上滑仍是下滑不採用手指滑動計算來判斷,而是用recyclerview來判斷是否到頂,若是到top頂點,那麼就開始下拉,反之就是上拉操做,畢竟手勢的判斷不是很精確,這樣核心的判斷出來,一個上,一個下,至於具體的邏輯看着代碼根據註釋看了,由於那部分的註釋特別詳細,而值得一說的就是這裏的下拉上拉是採用設置margin來作的
3:手指滑動完畢,擡起操做,到這裏基本就是收尾的工做了,這裏由於是寫死的數據,沒有網絡訪問,因此就是定死,設置拉下時長兩秒,而後完成下拉操做後就還原起點
上拉過程當中提供對外的監聽回調,這裏有兩個回調,第一個回調是帶有狀態和狀態值的一個參數,第二個回調就是拉下的一個座標距離,在後面的頭部視圖會用到,這個能夠自行擴展
而後就是頭部的添加操做,以前說的容器用來放下拉視圖,而要疊加效果添加多個頭部就須要一個集合存放了,用來控制每一個頭部
接下來就是addview的操做了
由於每一個頭部都須要繼承head接口,這是爲了減低耦合度,不直接依賴頭部,能夠自行擴展,建造者模式返回自己對象,這樣方便擴展
這裏提供了三個控制方法,getView,這個必須的,能夠獲取到每一個頭部的實例,而後操做,而後每一個頭部能夠添加動畫,因此有開始動畫和關閉動畫的方法
public interface IHeadView {
View getView();
void startAnim();
void stopAnim();
}
這樣在每一個頭部若是有動畫的話就能在回調接口裏實現了
而啓動和中止動畫確定是跟隨着下拉的狀態變化而啓動的,因此調用都在touch監聽裏面,至於何時調用能夠根據具體需求去調用
而後如今開始定義頭部效果,說到頭部效果,扇形的下拉回彈首選了,由於已經有不少例子了,因此我也模仿作了一個,還有一個緣由是爲了後面的頭部作鋪墊,疊加頭部,那麼最早添加的頭部確定要有當背景的覺悟了,先看效果圖
上代碼,一樣是很是很是詳細的註釋
package com.fragmentapp.view.refresh; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.util.AttributeSet; import android.view.View; import android.view.animation.DecelerateInterpolator; import android.view.animation.OvershootInterpolator; import com.fragmentapp.R; /** * Created by liuzhen on 2017/11/29. */ public class DownHeadView extends View implements IHeadView{ private int pull_height; private Path mPath; private Paint mBackPaint; private int backColor; private int mWidth; private int mHeight; private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(10); public DownHeadView(Context context) { this(context, null); } public DownHeadView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DownHeadView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ setWillNotDraw(false); backColor = getResources().getColor(R.color.color_8b90af); mPath = new Path(); mBackPaint = new Paint(); mBackPaint.setAntiAlias(true); mBackPaint.setStyle(Paint.Style.FILL); mBackPaint.setColor(backColor); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mWidth = getWidth(); mHeight = getHeight(); } } @Override protected void onDraw(Canvas canvas) { canvas.drawRect(0, 0, mWidth, pull_height, mBackPaint); mPath.reset(); mPath.moveTo(0, pull_height);//起點 mPath.quadTo(mWidth/2,pull_height*2,mWidth,pull_height);//控制點和終點 canvas.drawPath(mPath, mBackPaint);//繪製二級貝塞爾弧形 invalidate(); } @Override public View getView() { return this; } @Override public void startAnim() { backColor = getResources().getColor(R.color.color_8babaf); mBackPaint.setColor(backColor); ValueAnimator va = ValueAnimator.ofFloat(mHeight, mHeight/2); va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float val = (float) animation.getAnimatedValue(); val = decelerateInterpolator.getInterpolation(val / mHeight) * val; pull_height = (int)val; requestLayout(); } }); va.setInterpolator(new OvershootInterpolator(3));//甩動差值器 va.setDuration(500); va.start(); } @Override public void stopAnim() { backColor = getResources().getColor(R.color.color_8b90af); mBackPaint.setColor(backColor); } /**改變控制點*/ public void setPull_height(int y){ pull_height = y; invalidate(); } }
扇形下拉頭部實現流程也比較簡單
首先拋開動畫,是先繪製出一個矩形,而後下面的扇形利用二級的杯賽二線繪製,以y抽爲控制點,達到拉伸變長的一個效果
而後拉到最低的時候在開啓一個回彈動畫或者甩動的動畫
使用也是很簡單的一兩行代碼
只須要添加一個頭部和在回調中把拉伸的值傳進去就能夠了,到這裏背景頭部就行了,下面就是繼續疊加頭部效果了
看起來好像是一個視圖,實際上是兩個,第二個頭部的效果也是比較簡單的一個效果,繪製了一個粘性的回彈效果
1 package com.fragmentapp.view.refresh; 2 3 import android.animation.ValueAnimator; 4 import android.content.Context; 5 import android.graphics.Canvas; 6 import android.graphics.Color; 7 import android.graphics.Paint; 8 import android.graphics.Path; 9 import android.graphics.PointF; 10 import android.support.annotation.Nullable; 11 import android.util.AttributeSet; 12 import android.util.Log; 13 import android.view.View; 14 import android.view.animation.OvershootInterpolator; 15 16 import com.fragmentapp.R; 17 18 /** 19 * Created by liuzhen on 2017/12/4. 20 */ 21 22 public class StickyHeadView extends View implements IHeadView { 23 24 private Paint mPaint; 25 private int bg; 26 private float mStaicRadius = 20f;// 直徑 27 private float mMoveRadius = 20f;// 直徑 28 29 // 存儲靜態的兩個點 30 private PointF[] mStaticPointFs = new PointF[2]; 31 // 存儲移動的兩個點 32 private PointF[] mMovewPointFs = new PointF[2]; 33 // 控制點 34 private PointF mControlPointF = new PointF(); 35 // 靜態點 36 private PointF staticCenterPointF = null; 37 // 移動點 38 private PointF movewCenterPointF = null; 39 40 public StickyHeadView(Context context) { 41 this(context, null, 0); 42 } 43 44 public StickyHeadView(Context context, @Nullable AttributeSet attrs) { 45 this(context, attrs, 0); 46 } 47 48 public StickyHeadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 49 super(context, attrs, defStyleAttr); 50 init(); 51 } 52 53 private void init() { 54 mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 55 bg = getResources().getColor(R.color.color_8babaf); 56 mPaint.setColor(bg); 57 } 58 59 @Override 60 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 61 super.onLayout(changed, left, top, right, bottom); 62 if (changed) { 63 staticCenterPointF = new PointF(getWidth() / 2, getHeight()/4); 64 movewCenterPointF = new PointF(getWidth() / 2, getHeight()/4); 65 } 66 } 67 68 @Override 69 protected void onDraw(Canvas canvas) { 70 // 一、得到偏移量 71 float yOffset = staticCenterPointF.y - movewCenterPointF.y; 72 float xOffset = staticCenterPointF.x - movewCenterPointF.x; 73 // 二、有了偏移量就能夠求出兩點斜率了 74 Double lineK = 0.0; 75 if (xOffset != 0f) { 76 lineK = (double) (yOffset / xOffset); 77 } 78 // 三、經過工具求得兩個點的集合 79 mMovewPointFs = getIntersectionPoints(movewCenterPointF, mMoveRadius, lineK); 80 mStaticPointFs = getIntersectionPoints(staticCenterPointF, mStaicRadius, lineK); 81 // 四、經過公式求得控制點 82 mControlPointF = getMiddlePoint(staticCenterPointF, movewCenterPointF); 83 84 // 保存畫布狀態,保存方法以後的代碼,可以調用Canvas的平移、放縮、旋轉、裁剪等操做 85 // canvas.save(); 86 87 // 工型繪製 88 Path path = new Path(); 89 // 左上角點 90 path.moveTo(mStaticPointFs[0].x, mStaticPointFs[0].y); 91 // 上一邊的彎度和右上角點 92 path.quadTo(mControlPointF.x, mControlPointF.y, mMovewPointFs[0].x, mMovewPointFs[0].y); 93 // 右下角點 94 path.lineTo(mMovewPointFs[1].x, mMovewPointFs[1].y); 95 // 下一邊的彎度和左下角點 96 path.quadTo(mControlPointF.x, mControlPointF.y, mStaticPointFs[1].x, mStaticPointFs[1].y); 97 // 關閉後,會回到最開始的地方,造成封閉的圖形 98 path.close(); 99 100 canvas.drawPath(path, mPaint); 101 102 canvas.drawCircle(staticCenterPointF.x, staticCenterPointF.y, mStaicRadius, mPaint); 103 // // 畫移動的大圓 104 canvas.drawCircle(movewCenterPointF.x, movewCenterPointF.y, mMoveRadius, mPaint); 105 106 // 恢復上次的保存狀態 107 // canvas.restore(); 108 109 } 110 /**拉扯移動點*/ 111 public void move(float downY) { 112 //以中間點爲定點 113 updateMoveCenter(getWidth() / 2 + downY, movewCenterPointF.y); 114 updateStaticCenter(getWidth() / 2 - downY, staticCenterPointF.y); 115 } 116 117 118 /** 119 * 更新移動的點 120 */ 121 private void updateMoveCenter(float downX, float downY) { 122 movewCenterPointF.set(downX, downY); 123 invalidate(); 124 } 125 126 private void updateStaticCenter(float downX, float downY) { 127 staticCenterPointF.set(downX, downY); 128 invalidate(); 129 } 130 131 @Override 132 public View getView() { 133 return this; 134 } 135 136 @Override 137 public void startAnim() { 138 bg = getResources().getColor(R.color.color_8b90af); 139 mPaint.setColor(bg); 140 //甩動動畫 141 ValueAnimator vAnim = ValueAnimator.ofFloat(-30,0,50,0); 142 vAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 143 @Override 144 public void onAnimationUpdate(ValueAnimator animation) { 145 // float percent = animation.getAnimatedFraction(); 146 float val = (float)animation.getAnimatedValue(); 147 // Log.e("tag","percent "+percent + " percent*val "+percent*val); 148 updateMoveCenter(getWidth() / 2 + val, movewCenterPointF.y); 149 updateStaticCenter(getWidth() / 2 - val, staticCenterPointF.y); 150 } 151 }); 152 vAnim.setInterpolator(new OvershootInterpolator(2));//甩動差值器 153 vAnim.setDuration(1000); 154 vAnim.start(); 155 } 156 157 @Override 158 public void stopAnim() { 159 bg = getResources().getColor(R.color.color_8babaf); 160 mPaint.setColor(bg); 161 } 162 163 /** 164 * Get the point of intersection between circle and line. 獲取 165 * 經過指定圓心,斜率爲lineK的直線與圓的交點。 166 */ 167 private PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) { 168 PointF[] points = new PointF[2]; 169 170 float radian, xOffset = 0, yOffset = 0; 171 if (lineK != null) { 172 radian = (float) Math.atan(lineK); 173 xOffset = (float) (Math.sin(radian) * radius); 174 yOffset = (float) (Math.cos(radian) * radius); 175 } else { 176 xOffset = radius; 177 yOffset = 0; 178 } 179 points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset); 180 points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset); 181 182 return points; 183 } 184 185 186 /** 187 * Get middle point between p1 and p2. 得到兩點連線的中點 188 */ 189 private PointF getMiddlePoint(PointF p1, PointF p2) { 190 return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f); 191 } 192 193 }
而粘性效果會隨着拉伸而左右拉扯,這裏就是利用咱們以前的回調接口去控制了,在頭部寫控制方法,而後在回調接口裏把座標傳進去,就達到拉伸拉扯的效果了
粘性的動畫實際上是兩個圓形,加上一個工型的貝塞爾線,而後也是經過回調獲取拉伸的距離座標,經過座標,來達到控制兩個圓的拉扯,下拉到底的時候在啓動甩動動畫,由於加入了貝塞爾的緣故,因此看起來就像是兩個球有引力通常相互拉扯
工型貝塞爾的繪製也有詳細的註釋,工的兩頭都是同樣的長度,因此圓也是同樣的大小,拉伸的時候改變兩個的圓的距離
到這裏核心的功能就介紹完了,接下來還有一些收尾的操做,咱們的尾部,而後在定義一個咱們的頭部疊加進去,固然,尾部若是有須要也能作成疊加的一個容器,上面也的確是這麼作的,不過通常的尾部都是比較簡單的,因此沒有作過多的一個樣式,這裏只是作了一個簡單的progress效果
下面看看最終的一個效果
這裏咱們的頭部一共添加了三個,最底層是扇形的下拉頭部,第二層是一個圓形的拉扯粘性效果,第三層是一排小圓點左右晃動的效果,三層疊加
使用上同樣
而尾部基本沒有什麼邏輯代碼,就是一個xml的展現
package com.fragmentapp.view.refresh; import android.content.Context; import android.util.AttributeSet; import android.view.View; import android.widget.ProgressBar; /** * Created by liuzhen on 2017/11/24. */ public class DefFootView extends ProgressBar implements IFootView{ public DefFootView(Context context) { this(context,null,android.R.attr.progressBarStyleSmallInverse); } public DefFootView(Context context, AttributeSet attrs) { this(context, attrs,0); } public DefFootView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init(){ } @Override public View getView() { return this; } @Override public void startAnim() { } @Override public void stopAnim() { } }
接下來是添加實際的網絡訪問和分頁的操做了,仍是先看效果圖
效果圖上是正式的接口訪問,而後上拉後加載下一頁的數據
因爲原來的加載是固定的兩秒,如今加上網絡接口,那麼時間就成了不肯定了,快的話很快,慢的話很滿,因此須要限定一下,否則太快的時候會致使一閃就完了,着顯然不太好
那麼這裏須要固定一個最小的加載時間,若是由於太快而致使接口完了那麼這時候須要延長一點時間,好比正常網速很好,500毫秒加載完畢了,這樣會致使一閃而過,體驗不太好,這裏咱們須要申明一個最小加載時間,假設是2000毫秒,接口加載了500毫秒,
咱們須要延長的時間 = 最小的加載時間2000毫秒 - 接口的實際加載時間500毫秒 ,反過來,若是接口加載時間很慢,大於了2000毫秒,那麼就不用理會,正常中止
開始和介紹計算的過程都放在了自定義的控件裏,自動去完成
這裏添加了一個時間計算的輔助類
package com.fragmentapp.helper; import com.orhanobut.logger.Logger; import java.util.Date; /** * Created by liuzhen on 2018/1/5. */ public class TimeUtil { private static long defaultTime = 2000; private static Date startTime = null,endTime = null; public static void startTime(){ clear(); Logger.d("tag","startTime"); if (startTime == null) startTime = new Date(); } public static void endTime(){ Logger.d("tag","endTime"); if (endTime == null) endTime = new Date(); } /**獲取時間差毫秒數**/ public static long getDateMillis() { if (startTime == null || endTime == null) return 0; long nd = 1000 * 24 * 60 * 60; long nh = 1000 * 60 * 60; long nm = 1000 * 60; long ns = 1000; // 得到兩個時間的毫秒時間差別 long diff = endTime.getTime() - startTime.getTime(); // // 計算差多少天 // long day = diff / nd; // // 計算差多少小時 // long hour = diff % nd / nh; // // 計算差多少分鐘 // long min = diff % nd % nh / nm; // 計算差多少秒//輸出結果 long sec = diff % nd % nh % nm; clear(); return defaultTime - sec; } private static void clear(){ startTime = null; endTime = null; } }
最後在獲取完時間後把時間clear了一下,一來釋放內存,二來重置時間
如今能夠發現之前是直接在up手勢擡起的時候直接postDelayed 2000毫秒的,如今是統一調用stop方法來中止的,最終的代碼以下
package com.fragmentapp.view.refresh; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.Toast; import com.fragmentapp.R; import com.fragmentapp.helper.TimeUtil; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; /** * Created by LiuZhen on 2017/3/24. */ public class RefreshLayout extends FrameLayout { private String TAG = "tag"; private int downY;// 按下時y軸的偏移量 private final static float RATIO = 2f; //頭部的高度 protected int mHeadHeight = 120; //頭部layout protected FrameLayout mHeadLayout, mFootLayout;//頭部容器 private List<IHeadView> heads = new ArrayList<>();//支持添加多個頭部 private List<IFootView> foots = new ArrayList<>(); public static final int DOWN_REFRESH = 0;// 下拉刷新狀態 public static final int RELEASE_REFRESH = 1;// 鬆開刷新 public static final int LOADING = 2;// 正在刷新中 private int currentState = DOWN_REFRESH;// 頭佈局的狀態: 默認爲下拉刷新狀態 private View list;//子節點中的 recyclerview 視圖 private LayoutParams listParam, footParam;//用於控制下拉動畫展現 private boolean isLoadingMore = false;// 是否進入加載狀態,防止屢次重複的啓動 private boolean isStart = false;//表示正在加載刷新中,還沒中止 private boolean isTop = false, isBottom = false; private int mTouchSlop; private CallBack callBack; public RefreshLayout(Context context) { this(context, null, 0); } public RefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshLayout, defStyleAttr, 0); try { mHeadHeight = a.getDimensionPixelSize(R.styleable.RefreshLayout__height, 120); } finally { a.recycle(); } mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); init(); } private void initHeaderContainer() { mHeadLayout = new FrameLayout(getContext()); LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, mHeadHeight); this.addView(mHeadLayout, layoutParams); } public void initFootContainer() { footParam = new LayoutParams(LayoutParams.MATCH_PARENT, mHeadHeight); mFootLayout = new FrameLayout(getContext());//底部佈局 mFootLayout.setBackgroundColor(Color.BLACK); footParam.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; footParam.setMargins(0, 0, 0, -mHeadHeight); this.addView(mFootLayout, footParam); } private void init() { initHeaderContainer(); } @Override protected void onFinishInflate() {//佈局加載成xml時觸發 super.onFinishInflate(); initFootContainer(); if (list == null) { list = getChildAt(1); listParam = (LayoutParams) list.getLayoutParams(); list.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return onFingerTouch(event); } }); } } /** * 設置頭部View */ public RefreshLayout setHeaderView(final IHeadView headView) { if (headView != null) { mHeadLayout.addView(headView.getView()); heads.add(headView); } return this; } /** * 設置尾部View */ public RefreshLayout setFootView(final IFootView footView) { if (footView != null) { mFootLayout.addView(footView.getView()); foots.add(footView); } return this; } public boolean onFingerTouch(MotionEvent ev) { isTop = isViewToTop(list, mTouchSlop); isBottom = isViewToBottom(list, mTouchSlop); // Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: currentState = LOADING; downY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE: if (!isTop && !isBottom)//沒有到頂,無需計算操做 break; int moveY = (int) ev.getY(); int diff = (int) (((float) moveY - (float) downY) / RATIO); int paddingTop = diff; if (paddingTop > 0 && isTop) { //向下滑動多少後開始啓動刷新,Margin判斷是爲了限制快速用力滑動的時候致使頭部侵入的高度不夠就開始加載了 if (paddingTop >= mHeadHeight && (listParam.topMargin >= mHeadHeight) && currentState == DOWN_REFRESH) { // 徹底顯示了. // Log.i(TAG, "鬆開刷新 RELEASE_REFRESH"); currentState = RELEASE_REFRESH; refreshHeaderView(); start(); } else if (currentState == LOADING) { // 沒有顯示徹底 // Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH"); currentState = DOWN_REFRESH; refreshHeaderView(); } if (paddingTop <= (mHeadHeight + 10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置 listParam.setMargins(0, paddingTop, 0, 0); list.setLayoutParams(listParam); if (callBack != null) callBack.pullListener(paddingTop); } } else if (isBottom) { //限制上滑時不能超過底部的寬度,否則會超出邊界 //mHeadHeight+20 上滑設置的margin要超過headheight,否則下面判斷的大於headheight不成立,下面的margin基礎上面設置後的參數 if (Math.abs(paddingTop) <= (mHeadHeight + 10) && !isStart) {//已經處於運行刷新狀態的時候禁止設置 listParam.setMargins(0, 0, 0, -paddingTop); footParam.setMargins(0, 0, 0, -paddingTop - mHeadHeight); list.setLayoutParams(listParam); } //若是滑動的距離大於頭部或者底部的高度,而且設置的margin也大於headheight //listParam用來限制recyclerview列表迅速滑動,footParam用來限制bottom foothead迅速滑動致使沒有達到head的高度就開始加載了 if (Math.abs(paddingTop) >= mHeadHeight && (listParam.bottomMargin >= mHeadHeight || footParam.bottomMargin >= 0)) isLoadingMore = true;//頭部是否拉取到位,而後執行加載動畫 } break; case MotionEvent.ACTION_UP: if (isLoadingMore) {//是否開始加載 currentState = LOADING; refreshHeaderView(); isLoadingMore = false; isStart = true;//是否開始加載 // postDelayed(new Runnable() {//沒有網絡訪問時固定2秒加載完成 // @Override // public void run() { //// Log.i(TAG, "中止 END"); //// currentState = END; // stop(); // } // },2000); } else { if (!isStart) { // 隱藏頭佈局 listParam.setMargins(0, 0, 0, 0); footParam.setMargins(0, 0, 0, -mHeadHeight); list.setLayoutParams(listParam); } } // Log.i(TAG, "鬆開 REFRESHING"); break; default: break; } return super.onTouchEvent(ev); } /** * 根據currentState刷新頭佈局的狀態 */ private void refreshHeaderView() { if (callBack == null || isStart) return; String val = "準備"; switch (currentState) { case DOWN_REFRESH: // 下拉刷新狀態 val = "下拉"; break; case RELEASE_REFRESH: // 鬆開刷新狀態 val = "開始..."; break; case LOADING: // 正在刷新中狀態 val = "刷新中..."; TimeUtil.startTime(); break; } callBack.refreshHeaderView(currentState, val); } public static boolean isViewToTop(View view, int mTouchSlop) { if (view instanceof AbsListView) return isAbsListViewToTop((AbsListView) view); if (view instanceof RecyclerView) return isRecyclerViewToTop((RecyclerView) view); return (view != null && Math.abs(view.getScrollY()) <= 2 * mTouchSlop); } public static boolean isViewToBottom(View view, int mTouchSlop) { if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view); if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view); // if (view instanceof WebView) return isWebViewToBottom((WebView) view,mTouchSlop); // if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view); return false; } public static boolean isAbsListViewToTop(AbsListView absListView) { if (absListView != null) { int firstChildTop = 0; if (absListView.getChildCount() > 0) { // 若是AdapterView的子控件數量不爲0,獲取第一個子控件的top firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop(); } if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) { return true; } } return false; } public static boolean isRecyclerViewToTop(RecyclerView recyclerView) { if (recyclerView != null) { RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager == null) { return true; } if (manager.getItemCount() == 0) { return true; } if (manager instanceof LinearLayoutManager) { LinearLayoutManager layoutManager = (LinearLayoutManager) manager; int firstChildTop = 0; if (recyclerView.getChildCount() > 0) { // 處理item高度超過一屏幕時的狀況 View firstVisibleChild = recyclerView.getChildAt(0); if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { if (android.os.Build.VERSION.SDK_INT < 14) { return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0); } else { return !ViewCompat.canScrollVertically(recyclerView, -1); } } // 若是RecyclerView的子控件數量不爲0,獲取第一個子控件的top // 解決item的topMargin不爲0時不能觸發下拉刷新 View firstChild = recyclerView.getChildAt(0); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams(); firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop(); } if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) { return true; } } else if (manager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; int[] out = layoutManager.findFirstCompletelyVisibleItemPositions(null); if (out[0] < 1) { return true; } } } return false; } /** * 經過反射獲取RecyclerView的item的topInset */ private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) { try { Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets"); field.setAccessible(true); // 開發者自定義的滾動監聽器 Rect decorInsets = (Rect) field.get(layoutParams); return decorInsets.top; } catch (Exception e) { e.printStackTrace(); } return 0; } public static boolean isAbsListViewToBottom(AbsListView absListView) { if (absListView != null && absListView.getAdapter() != null && absListView.getChildCount() > 0 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1) { View lastChild = absListView.getChildAt(absListView.getChildCount() - 1); return lastChild.getBottom() <= absListView.getMeasuredHeight(); } return false; } public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) { if (recyclerView != null) { RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager == null || manager.getItemCount() == 0) { return false; } if (manager instanceof LinearLayoutManager) { // 處理item高度超過一屏幕時的狀況 View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1); if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { if (android.os.Build.VERSION.SDK_INT < 14) { return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0); } else { return !ViewCompat.canScrollVertically(recyclerView, 1); } } LinearLayoutManager layoutManager = (LinearLayoutManager) manager; if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) { return true; } } else if (manager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null); int lastPosition = layoutManager.getItemCount() - 1; for (int position : out) { if (position == lastPosition) { return true; } } } } return false; } /** * 是否下拉操做 **/ public boolean isBottom() { return isBottom; } /** * 開始加載 **/ public void start() { isLoadingMore = true; for (IHeadView head : heads) { head.startAnim(); } } /** * 結束加載 **/ public void stop() { TimeUtil.endTime(); long loadTime = TimeUtil.getDateMillis();//大於0表示小於默認的加載時間 Toast.makeText(getContext(), loadTime + "毫秒", Toast.LENGTH_SHORT).show(); if (loadTime > 0) {//最小限制2秒的動畫加載效果 postDelayed(new Runnable() {//沒有網絡訪問時固定2秒加載完成 @Override public void run() { refreshHeaderView(); listParam.setMargins(0, 0, 0, 0); footParam.setMargins(0, 0, 0, -mHeadHeight); list.setLayoutParams(listParam); isLoadingMore = false; isStart = false; for (IHeadView head : heads) { head.stopAnim(); } } }, loadTime); }else{ refreshHeaderView(); listParam.setMargins(0, 0, 0, 0); footParam.setMargins(0, 0, 0, -mHeadHeight); list.setLayoutParams(listParam); isLoadingMore = false; isStart = false; for (IHeadView head : heads) { head.stopAnim(); } } } public RefreshLayout setCallBack(CallBack callBack) { this.callBack = callBack; return this; } public interface CallBack { /** * 監聽下拉時的狀態 */ void refreshHeaderView(int state, String stateVal); /** * 監聽下拉時的距離 */ void pullListener(int y); } }
接下來在使用也是很小的改動
添加兩個變量控制分頁
刷新的狀態裏調用接口
在接口數據返回時控制填充刷新數據
首頁的代碼以下
package com.fragmentapp.home.fragment; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.fragmentapp.R; import com.fragmentapp.base.LazyFragment; import com.fragmentapp.helper.EmptyLayout; import com.fragmentapp.home.adapter.ArticleAdapter; import com.fragmentapp.home.bean.ArticleDataBean; import com.fragmentapp.home.imple.IArticleView; import com.fragmentapp.home.presenter.ArticlePresenter; import com.fragmentapp.view.refresh.DefFootView; import com.fragmentapp.view.refresh.DefHeaderView; import com.fragmentapp.view.refresh.DownHeadView; import com.fragmentapp.view.refresh.RefreshLayout; import com.fragmentapp.view.refresh.StickyHeadView; import com.fragmentapp.view.refresh.TextHeadView; import com.fragmentapp.view.sticky.DividerDecoration; import com.orhanobut.logger.Logger; import java.util.ArrayList; import java.util.List; import butterknife.BindView; import butterknife.OnClick; /** * Created by liuzhen on 2017/11/8. */ public class HomeFragment extends LazyFragment implements IArticleView { @BindView(R.id.root) View root; @BindView(R.id.recyclerView) RecyclerView recyclerView; @BindView(R.id.refreshLayout) RefreshLayout refreshLayout; private TextHeadView textHeadView; private DownHeadView downHeadView;//扇形頭部 private StickyHeadView stickyHeadView;//粘性頭部 private List<ArticleDataBean.ListBean> list = new ArrayList<>(); private ArticleAdapter adapter; private ArticlePresenter presenter; private int page = 1,lastPage = -1; @Override protected int getLayoutId() { return R.layout.fragment_home; } @Override protected void init() { presenter = new ArticlePresenter(this); page = 1; presenter.getArticleList(page); adapter = new ArticleAdapter(getActivity(), R.layout.item_home, list); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setAdapter(adapter); recyclerView.addItemDecoration(new DividerDecoration(getActivity())); textHeadView = new TextHeadView(getActivity()); downHeadView = new DownHeadView(getActivity()); stickyHeadView = new StickyHeadView(getActivity()); refreshLayout .setHeaderView(downHeadView) .setHeaderView(textHeadView) .setHeaderView(new DefHeaderView(getActivity())) .setHeaderView(stickyHeadView) // .setHeaderView(new WaterHeadView(getActivity())) .setFootView(new DefFootView(getActivity())) .setCallBack(new RefreshLayout.CallBack() { @Override public void refreshHeaderView(int state, String stateVal) { textHeadView.setText(stateVal); switch (state) { case RefreshLayout.DOWN_REFRESH: // 下拉刷新狀態 break; case RefreshLayout.RELEASE_REFRESH: // 鬆開刷新狀態 break; case RefreshLayout.LOADING: // 正在刷新中狀態 if (refreshLayout.isBottom() == false) { page = 1; lastPage = -1; } presenter.getArticleList(page); Logger.e("----------loading"); break; } } @Override public void pullListener(int y) { int pullHeight = y / 2; downHeadView.setPull_height(pullHeight); stickyHeadView.move(pullHeight); // Log.e("tag", pullHeight + ""); } }); emptyLayout.setCallBack(new EmptyLayout.CallBack() { @Override public void click() { presenter.getArticleList(page); } }); } @OnClick({R.id.search}) public void search(View view) { new SearchFragment().show(getFragmentManager(), TAG); } @Override public void success(List<ArticleDataBean.ListBean> list) { if (list.size() == 0){ emptyLayout.showEmpty((ViewGroup) getView(),"empty"); }else { page++;//若是有數據則+1下一頁 if (lastPage != page) { if (refreshLayout.isBottom()) adapter.addList(list); else adapter.setList(list); } lastPage = page; Toast.makeText(getActivity(), "success"+adapter.getItemCount(), Toast.LENGTH_SHORT).show(); } } @Override public void error() { emptyLayout.showEmpty((ViewGroup) root,"error"); Toast.makeText(getActivity(),"error",Toast.LENGTH_SHORT).show(); } @Override public void loadStop() { refreshLayout.stop(); } }
本人在github上也上傳了demo,能夠提供下載,若是能讓你有點感悟,請 star