該方法,我們通過完全自定義的方式實現,不添加任何依賴庫和jar包。純原生。
首先看一下我們實現的效果
實現的關鍵點:
- 為
ListView
添加頭布局和底布局。 - 通過改變頭布局的
paddingTop
值,來控制控件的顯示和隱藏 - 根據我們滑動的狀態,動態修改頭部布局和底部布局。
看一下代碼:
1 public class CustomRefreshListView extends ListView implements OnScrollListener{ 2 3 /** 4 * 頭布局 5 */ 6 private View headerView; 7 8 /** 9 * 頭部布局的高度 10 */ 11 private int headerViewHeight; 12 13 /** 14 * 頭部旋轉的圖片 15 */ 16 private ImageView iv_arrow; 17 18 /** 19 * 頭部下拉刷新時狀態的描述 20 */ 21 private TextView tv_state; 22 23 /** 24 * 下拉刷新時間的顯示控件 25 */ 26 private TextView tv_time; 27 28 29 /** 30 * 底部布局 31 */ 32 private View footerView; 33 34 /** 35 * 底部旋轉progressbar 36 */ 37 private ProgressBar pb_rotate; 38 39 40 /** 41 * 底部布局的高度 42 */ 43 private int footerViewHeight; 44 45 46 /** 47 * 按下時的Y坐標 48 */ 49 private int downY; 50 51 private final int PULL_REFRESH = 0;//下拉刷新的狀態 52 private final int RELEASE_REFRESH = 1;//松開刷新的狀態 53 private final int REFRESHING = 2;//正在刷新的狀態 54 55 /** 56 * 當前下拉刷新處於的狀態 57 */ 58 private int currentState = PULL_REFRESH; 59 60 /** 61 * 頭部布局在下拉刷新改變時,圖標的動畫 62 */ 63 private RotateAnimation upAnimation,downAnimation; 64 65 /** 66 * 當前是否在加載數據 67 */ 68 private boolean isLoadingMore = false; 69 70 public CustomRefreshListView(Context context) { 71 this(context,null); 72 } 73 74 public CustomRefreshListView(Context context, AttributeSet attrs) { 75 super(context, attrs); 76 init(); 77 } 78 79 private void init(){ 80 //設置滑動監聽 81 setOnScrollListener(this); 82 //初始化頭布局 83 initHeaderView(); 84 //初始化頭布局中圖標的旋轉動畫 85 initRotateAnimation(); 86 //初始化為尾布局 87 initFooterView(); 88 } 89 90 91 /** 92 * 初始化headerView 93 */ 94 private void initHeaderView() { 95 headerView = View.inflate(getContext(), R.layout.head_custom_listview, null); 96 iv_arrow = (ImageView) headerView.findViewById(R.id.iv_arrow); 97 pb_rotate = (ProgressBar) headerView.findViewById(R.id.pb_rotate); 98 tv_state = (TextView) headerView.findViewById(R.id.tv_state); 99 tv_time = (TextView) headerView.findViewById(R.id.tv_time); 100 101 //測量headView的高度 102 headerView.measure(0, 0); 103 //獲取高度,並保存 104 headerViewHeight = headerView.getMeasuredHeight(); 105 //設置paddingTop = -headerViewHeight;這樣,該控件被隱藏 106 headerView.setPadding(0, -headerViewHeight, 0, 0); 107 //添加頭布局 108 addHeaderView(headerView); 109 } 110 111 /** 112 * 初始化旋轉動畫 113 */ 114 private void initRotateAnimation() { 115 116 upAnimation = new RotateAnimation(0, -180, 117 RotateAnimation.RELATIVE_TO_SELF, 0.5f, 118 RotateAnimation.RELATIVE_TO_SELF, 0.5f); 119 upAnimation.setDuration(300); 120 upAnimation.setFillAfter(true); 121 122 downAnimation = new RotateAnimation(-180, -360, 123 RotateAnimation.RELATIVE_TO_SELF, 0.5f, 124 RotateAnimation.RELATIVE_TO_SELF, 0.5f); 125 downAnimation.setDuration(300); 126 downAnimation.setFillAfter(true); 127 } 128 129 //初始化底布局,與頭布局同理 130 private void initFooterView() { 131 footerView = View.inflate(getContext(), R.layout.foot_custom_listview, null); 132 footerView.measure(0, 0); 133 footerViewHeight = footerView.getMeasuredHeight(); 134 footerView.setPadding(0, -footerViewHeight, 0, 0); 135 addFooterView(footerView); 136 } 137 138 @Override 139 public boolean onTouchEvent(MotionEvent ev) { 140 switch (ev.getAction()) { 141 case MotionEvent.ACTION_DOWN: 142 //獲取按下時y坐標 143 downY = (int) ev.getY(); 144 break; 145 case MotionEvent.ACTION_MOVE: 146 147 if(currentState==REFRESHING){ 148 //如果當前處在滑動狀態,則不做處理 149 break; 150 } 151 //手指滑動偏移量 152 int deltaY = (int) (ev.getY() - downY); 153 154 //獲取新的padding值 155 int paddingTop = -headerViewHeight + deltaY; 156 if(paddingTop>-headerViewHeight && getFirstVisiblePosition()==0){ 157 //向下滑,且處於頂部,設置padding值,該方法實現了頂布局慢慢滑動顯現 158 headerView.setPadding(0, paddingTop, 0, 0); 159 160 if(paddingTop>=0 && currentState==PULL_REFRESH){ 161 //從下拉刷新進入松開刷新狀態 162 currentState = RELEASE_REFRESH; 163 //刷新頭布局 164 refreshHeaderView(); 165 }else if (paddingTop<0 && currentState==RELEASE_REFRESH) { 166 //進入下拉刷新狀態 167 currentState = PULL_REFRESH; 168 refreshHeaderView(); 169 } 170 171 172 return true;//攔截TouchMove,不讓listview處理該次move事件,會造成listview無法滑動 173 } 174 175 176 break; 177 case MotionEvent.ACTION_UP: 178 if(currentState==PULL_REFRESH){ 179 //仍處於下拉刷新狀態,未滑動一定距離,不加載數據,隱藏headView 180 headerView.setPadding(0, -headerViewHeight, 0, 0); 181 }else if (currentState==RELEASE_REFRESH) { 182 //滑倒一定距離,顯示無padding值得headcView 183 headerView.setPadding(0, 0, 0, 0); 184 //設置狀態為刷新 185 currentState = REFRESHING; 186 187 //刷新頭部布局 188 refreshHeaderView(); 189 190 if(listener!=null){ 191 //接口回調加載數據 192 listener.onPullRefresh(); 193 } 194 } 195 break; 196 } 197 return super.onTouchEvent(ev); 198 } 199 200 /** 201 * 根據currentState來更新headerView 202 */ 203 private void refreshHeaderView(){ 204 switch (currentState) { 205 case PULL_REFRESH: 206 tv_state.setText("下拉刷新"); 207 iv_arrow.startAnimation(downAnimation); 208 break; 209 case RELEASE_REFRESH: 210 tv_state.setText("松開刷新"); 211 iv_arrow.startAnimation(upAnimation); 212 break; 213 case REFRESHING: 214 iv_arrow.clearAnimation();//因為向上的旋轉動畫有可能沒有執行完 215 iv_arrow.setVisibility(View.INVISIBLE); 216 pb_rotate.setVisibility(View.VISIBLE); 217 tv_state.setText("正在刷新..."); 218 break; 219 } 220 } 221 222 /** 223 * 完成刷新操作,重置狀態,在你獲取完數據並更新完adater之后,去在UI線程中調用該方法 224 */ 225 public void completeRefresh(){ 226 if(isLoadingMore){ 227 //重置footerView狀態 228 footerView.setPadding(0, -footerViewHeight, 0, 0); 229 isLoadingMore = false; 230 }else { 231 //重置headerView狀態 232 headerView.setPadding(0, -headerViewHeight, 0, 0); 233 currentState = PULL_REFRESH; 234 pb_rotate.setVisibility(View.INVISIBLE); 235 iv_arrow.setVisibility(View.VISIBLE); 236 tv_state.setText("下拉刷新"); 237 tv_time.setText("最后刷新:"+getCurrentTime()); 238 } 239 } 240 241 /** 242 * 獲取當前系統時間,並格式化 243 * @return 244 */ 245 private String getCurrentTime(){ 246 SimpleDateFormat format = new SimpleDateFormat("yy-MM-dd HH:mm:ss"); 247 return format.format(new Date()); 248 } 249 250 private OnRefreshListener listener; 251 public void setOnRefreshListener(OnRefreshListener listener){ 252 this.listener = listener; 253 } 254 public interface OnRefreshListener{ 255 void onPullRefresh(); 256 void onLoadingMore(); 257 } 258 259 /** 260 * SCROLL_STATE_IDLE:閑置狀態,就是手指松開 261 * SCROLL_STATE_TOUCH_SCROLL:手指觸摸滑動,就是按着來滑動 262 * SCROLL_STATE_FLING:快速滑動后松開 263 */ 264 @Override 265 public void onScrollStateChanged(AbsListView view, int scrollState) { 266 if(scrollState==OnScrollListener.SCROLL_STATE_IDLE 267 && getLastVisiblePosition()==(getCount()-1) &&!isLoadingMore){ 268 isLoadingMore = true; 269 270 footerView.setPadding(0, 0, 0, 0);//顯示出footerView 271 setSelection(getCount());//讓listview最后一條顯示出來,在頁面完全顯示出底布局 272 273 if(listener!=null){ 274 listener.onLoadingMore(); 275 } 276 } 277 } 278 279 280 @Override 281 public void onScroll(AbsListView view, int firstVisibleItem, 282 int visibleItemCount, int totalItemCount) { 283 } 284 285 }
代碼的注釋寫的比較詳細,在這里只說明主要的邏輯。
下拉刷新
下拉刷新是通過設置setOnTouchListener()
方法,監聽觸摸事件,通過手指滑動的不同處理實現相應邏輯。
實現比較復雜,分為了三個情況,初始狀態(顯示下拉刷新),釋放刷新狀態,刷新狀態。
其中下拉刷新狀態和釋放狀態的改變,是由於手指滑動的不同距離,是在MotionEvent.ACTION_MOVE
中進行判斷,該判斷不處理任何數據邏輯,只是根據手指滑動的偏移量該表UI的顯示。
刷新狀態的判斷是在MotionEvent.ACTION_UP
手指抬起時判斷的。這很好理解,因為最終下拉刷新是否加載數據的確定,是由我們手指離開屏幕時與初始值的偏移量確定的。如果我們的偏移量小於了頭布局的高度,代表不刷新,繼續隱藏頭布局。如果偏移量大於了頭布局的高度,代表刷新,修改UI,同時通過接口回調,讓其持有者進行加載數據。
上拉加載
上拉加載和下拉刷新不同,他的視線較為簡單,我們通過ListView
的滾動監聽進行處理相應邏輯。即setOnScrollListener(this);
。
該方法需要實現兩個回調方法
public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount)
:滾動監聽的調用。public void onScrollStateChanged(AbsListView view, int scrollState)
:滑動狀態改變的回調。其中scrollState
為回調的狀態,可能值為
- SCROLL_STATE_IDLE:閑置狀態,手指松開后的狀態回調
- SCROLL_STATE_TOUCH_SCROLL:手指觸摸滑動的狀態回調
- SCROLL_STATE_FLING:手指松開后慣性滑動的回調
我們在onScrollStateChanged
中進行判斷,主要判斷一下條件;
- 是否是停止狀態
- 是否滑倒最后
- 是否正在加載數據
如果符合條件,則開始加載數據,通過接口回調。