編輯:關於android開發
這一篇主要來講一下自定義控件中的自定義viewgroup,我們以項目中最常用的下拉刷新和加載更多組件為例
簡單介紹一下自定義viewgroup時應該怎麼做。
分析:下拉刷新和加載更多的原理和步驟
自定義一個viewgroup,將headerview、contentview和footerview從上到下依次布局,然後在初始化的時候
通過Scrooller滾動使得該組件在y軸方向上滾動headerview的高度,這樣headerview就被隱藏了。而contentview的
寬度和高度都是match_parent的,因此屏幕上 headerview和footerview就都被隱藏在屏幕之外了。當contentview被
滾動到頂部,如果此時用戶繼續下拉,那麼下拉刷新組件將攔截觸摸事件,然後根據用戶的觸摸事件獲取到手指滑動的
y軸距離,並通過scroller將該下拉組件在y軸上滾動手指滑動的距離,實現headerview的顯示和隱藏,從而達到下拉的效果
。當用戶滑動到最底部時會觸發加載更多的操作,此時會通過scroller滾動該下拉刷新組件,將footerview顯示出來,實現加載更多
的效果。具體步驟如下:
第一步:初始化View即headerView contentView和footerView
第二步:測量三個view的大小,並計算出viewgroup的大小
第三步:布局,將三個view在界面上布局,按照上中下的順序
第四步:監聽屏幕的觸摸事件,判斷是否下拉刷新或者加載更多
第五步:觸發下拉刷新和加載更多事件執行下拉刷新和加載更多
第六步:下拉刷新和加載更多執行完後的重置操作
示例代碼:
自定義的viewgroup
1 package com.jiao.simpleimageview.view;
2
3 import android.content.Context;
4 import android.graphics.Color;
5 import android.support.v4.view.MotionEventCompat;
6 import android.util.AttributeSet;
7 import android.view.LayoutInflater;
8 import android.view.MotionEvent;
9 import android.view.View;
10 import android.view.ViewGroup;
11 import android.view.animation.RotateAnimation;
12 import android.widget.AbsListView;
13 import android.widget.AbsListView.OnScrollListener;
14 import android.widget.ImageView;
15 import android.widget.ProgressBar;
16 import android.widget.Scroller;
17 import android.widget.TextView;
18
19 import com.jiao.simpleimageview.R;
20 import com.jiao.simpleimageview.listener.OnLoadListener;
21 import com.jiao.simpleimageview.listener.OnRefreshListener;
22
23 import java.text.SimpleDateFormat;
24 import java.util.Date;
25
26 /**
27 * Created by jiaocg on 2016/3/24.
28 */
29 public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements
30 OnScrollListener {
31
32 /**
33 *
34 */
35 protected Scroller mScroller;
36
37 /**
38 * 下拉刷新時顯示的header view
39 */
40 protected View mHeaderView;
41
42 /**
43 * 上拉加載更多時顯示的footer view
44 */
45 protected View mFooterView;
46
47 /**
48 * 本次觸摸滑動y坐標上的偏移量
49 */
50 protected int mYOffset;
51
52 /**
53 * 內容視圖, 即用戶觸摸導致下拉刷新、上拉加載的主視圖. 比如ListView, GridView等.
54 */
55 protected T mContentView;
56
57 /**
58 * 最初的滾動位置.第一次布局時滾動header的高度的距離
59 */
60 protected int mInitScrollY = 0;
61 /**
62 * 最後一次觸摸事件的y軸坐標
63 */
64 protected int mLastY = 0;
65
66 /**
67 * 空閒狀態
68 */
69 public static final int STATUS_IDLE = 0;
70
71 /**
72 * 下拉或者上拉狀態, 還沒有到達可刷新的狀態
73 */
74 public static final int STATUS_PULL_TO_REFRESH = 1;
75
76 /**
77 * 下拉或者上拉狀態
78 */
79 public static final int STATUS_RELEASE_TO_REFRESH = 2;
80 /**
81 * 刷新中
82 */
83 public static final int STATUS_REFRESHING = 3;
84
85 /**
86 * LOADING中
87 */
88 public static final int STATUS_LOADING = 4;
89
90 /**
91 * 當前狀態
92 */
93 protected int mCurrentStatus = STATUS_IDLE;
94
95 /**
96 * header中的箭頭圖標
97 */
98 private ImageView mArrowImageView;
99 /**
100 * 箭頭是否向上
101 */
102 private boolean isArrowUp;
103 /**
104 * header 中的文本標簽
105 */
106 private TextView mTipsTextView;
107 /**
108 * header中的時間標簽
109 */
110 private TextView mTimeTextView;
111 /**
112 * header中的進度條
113 */
114 private ProgressBar mProgressBar;
115 /**
116 * 屏幕高度
117 */
118 private int mScreenHeight;
119 /**
120 * Header 高度
121 */
122 private int mHeaderHeight;
123 /**
124 * 下拉刷新監聽器
125 */
126 protected OnRefreshListener mOnRefreshListener;
127 /**
128 * 加載更多回調
129 */
130 protected OnLoadListener mLoadListener;
131
132 /**
133 * @param context
134 */
135 public RefreshLayoutBase(Context context) {
136 this(context, null);
137 }
138
139 /**
140 * @param context
141 * @param attrs
142 */
143 public RefreshLayoutBase(Context context, AttributeSet attrs) {
144 this(context, attrs, 0);
145 }
146
147 /**
148 * @param context
149 * @param attrs
150 * @param defStyle
151 */
152 public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyle) {
153 super(context, attrs);
154
155 // 初始化Scroller對象
156 mScroller = new Scroller(context);
157
158 // 獲取屏幕高度
159 mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
160 // header 的高度為屏幕高度的 1/4
161 mHeaderHeight = mScreenHeight / 4;
162
163 // 初始化整個布局
164 initLayout(context);
165 }
166
167 /**
168 * 第一步:初始化整個布局
169 *
170 * @param context
171 */
172 private final void initLayout(Context context) {
173 // header view
174 setupHeaderView(context);
175 // 設置內容視圖
176 setupContentView(context);
177 // 設置布局參數
178 setDefaultContentLayoutParams();
179 // 添加mContentView
180 addView(mContentView);
181 // footer view
182 setupFooterView(context);
183
184 }
185
186 /**
187 * 初始化 header view
188 */
189 protected void setupHeaderView(Context context) {
190 mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,
191 false);
192 mHeaderView
193 .setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
194 mHeaderHeight));
195 mHeaderView.setBackgroundColor(Color.RED);
196 mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
197 addView(mHeaderView);
198
199 // HEADER VIEWS
200 mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
201 mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
202 mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
203 mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
204 }
205
206
207 /**
208 * 初始化Content View, 子類覆寫.
209 */
210 protected abstract void setupContentView(Context context);
211
212 /**
213 * 設置Content View的默認布局參數
214 */
215 protected void setDefaultContentLayoutParams() {
216 ViewGroup.LayoutParams params =
217 new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
218 LayoutParams.MATCH_PARENT);
219 mContentView.setLayoutParams(params);
220 }
221
222 /**
223 * 初始化footer view
224 */
225 protected void setupFooterView(Context context) {
226 mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
227 this, false);
228 addView(mFooterView);
229 }
230
231
232 /**
233 * 第二步:測量
234 * 丈量視圖的寬、高。寬度為用戶設置的寬度,高度則為header,
235 * content view, footer這三個子控件的高度之和。
236 *
237 * @param widthMeasureSpec
238 * @param heightMeasureSpec
239 */
240 @Override
241 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
242 int width = MeasureSpec.getSize(widthMeasureSpec);
243 int childCount = getChildCount();
244 int finalHeight = 0;
245 for (int i = 0; i < childCount; i++) {
246 View child = getChildAt(i);
247 // measure
248 measureChild(child, widthMeasureSpec, heightMeasureSpec);
249 // 該view所需要的總高度
250 finalHeight += child.getMeasuredHeight();
251 }
252 setMeasuredDimension(width, finalHeight);
253 }
254
255
256 /**
257 * 第三步:布局
258 * 布局函數,將header, content view,
259 * footer這三個view從上到下布局。布局完成後通過Scroller滾動到header的底部,
260 * 即滾動距離為header的高度 +本視圖的paddingTop,從而達到隱藏header的效果.
261 */
262 @Override
263 protected void onLayout(boolean changed, int l, int t, int r, int b) {
264
265 int childCount = getChildCount();
266 int top = getPaddingTop();
267 for (int i = 0; i < childCount; i++) {
268 View child = getChildAt(i);
269 child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
270 top += child.getMeasuredHeight();
271 }
272
273 // 計算初始化滑動的y軸距離
274 mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
275 // 滑動到header view高度的位置, 從而達到隱藏header view的效果
276 scrollTo(0, mInitScrollY);
277 }
278
279
280 /**
281 * 第四步:監聽滑動事件
282 * 與Scroller合作,實現平滑滾動。在該方法中調用Scroller的computeScrollOffset來判斷滾動是否結束。
283 * 如果沒有結束,
284 * 那麼滾動到相應的位置,並且調用postInvalidate方法重繪界面,
285 * 從而再次進入到這個computeScroll流程,直到滾動結束。
286 */
287 @Override
288 public void computeScroll() {
289 if (mScroller.computeScrollOffset()) {
290 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
291 postInvalidate();
292 }
293 }
294
295 /*
296 * 在適當的時候攔截觸摸事件,這裡指的適當的時候是當mContentView滑動到頂部,
297 * 並且是下拉時攔截觸摸事件,否則不攔截,交給其child
298 * view 來處理。
299 */
300 @Override
301 public boolean onInterceptTouchEvent(MotionEvent ev) {
302
303 final int action = MotionEventCompat.getActionMasked(ev);
304 // Always handle the case of the touch gesture being complete.
305 if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
306 // Do not intercept touch event, let the child handle it
307 return false;
308 }
309
310 switch (action) {
311
312 case MotionEvent.ACTION_DOWN:
313 mLastY = (int) ev.getRawY();
314 break;
315
316 case MotionEvent.ACTION_MOVE:
317 // int yDistance = (int) ev.getRawY() - mYDown;
318 mYOffset = (int) ev.getRawY() - mLastY;
319 // 如果拉到了頂部, 並且是下拉,則攔截觸摸事件,從而轉到onTouchEvent來處理下拉刷新事件
320 if (isTop() && mYOffset > 0) {
321 return true;
322 }
323 break;
324
325 }
326 // Do not intercept touch event, let the child handle it
327 return false;
328 }
329
330 /**
331 * 第五步:下拉刷新
332 * 1、滑動view顯示出headerview
333 * 2、進度條滾動,修改標題內容
334 * 3、執行下拉刷新監聽
335 * 4、刷新成功或失敗後重置:隱藏headerview 修改標題內容
336 * 在這裡處理觸摸事件以達到下拉刷新或者上拉自動加載的問題
337 *
338 * @see android.view.View#onTouchEvent(android.view.MotionEvent)
339 */
340 @Override
341 public boolean onTouchEvent(MotionEvent event) {//下拉刷新的處理
342 switch (event.getAction()) {
343 case MotionEvent.ACTION_MOVE:
344 int currentY = (int) event.getRawY();
345 mYOffset = currentY - mLastY;
346 if (mCurrentStatus != STATUS_LOADING) {
347 changeScrollY(mYOffset);
348 }
349
350 rotateHeaderArrow();//旋轉箭頭
351 changeTips();//重置文本
352 mLastY = currentY;
353 break;
354
355 case MotionEvent.ACTION_UP:
356 // 下拉刷新的具體操作
357 doRefresh();
358 break;
359 default:
360 break;
361 }
362 return true;
363 }
364
365 /**
366 * 設置滾動的參數
367 *
368 * @param yOffset
369 */
370 private void startScroll(int yOffset) {
371 mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
372 invalidate();
373 }
374
375 /**
376 * y軸上滑動到指定位置
377 *
378 * @param distance
379 * @return
380 */
381 protected void changeScrollY(int distance) {
382 // 最大值為 scrollY(header 隱藏), 最小值為0 ( header 完全顯示).
383 int curY = getScrollY();
384 // 下拉
385 if (distance > 0 && curY - distance > getPaddingTop()) {
386 scrollBy(0, -distance);
387 } else if (distance < 0 && curY - distance <= mInitScrollY) {
388 // 上拉過程
389 scrollBy(0, -distance);
390 }
391
392 curY = getScrollY();
393 int slop = mInitScrollY / 2;
394 //
395 if (curY > 0 && curY < slop) {
396 mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
397 } else if (curY > 0 && curY > slop) {
398 mCurrentStatus = STATUS_PULL_TO_REFRESH;
399 }
400 }
401
402
403 /**
404 * 旋轉箭頭圖標
405 */
406 protected void rotateHeaderArrow() {
407
408 if (mCurrentStatus == STATUS_REFRESHING) {
409 return;
410 } else if (mCurrentStatus == STATUS_PULL_TO_REFRESH && !isArrowUp) {
411 return;
412 } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH && isArrowUp) {
413 return;
414 }
415
416 mProgressBar.setVisibility(View.GONE);
417 mArrowImageView.setVisibility(View.VISIBLE);
418 float pivotX = mArrowImageView.getWidth() / 2f;
419 float pivotY = mArrowImageView.getHeight() / 2f;
420 float fromDegrees = 0f;
421 float toDegrees = 0f;
422 if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
423 fromDegrees = 180f;
424 toDegrees = 360f;
425 } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
426 fromDegrees = 0f;
427 toDegrees = 180f;
428 }
429
430 RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
431 animation.setDuration(100);
432 animation.setFillAfter(true);
433 mArrowImageView.startAnimation(animation);
434
435 if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
436 isArrowUp = true;
437 } else {
438 isArrowUp = false;
439 }
440 }
441
442 /**
443 * 根據當前狀態修改header view中的文本標簽
444 */
445 protected void changeTips() {
446 if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
447 mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
448 } else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
449 mTipsTextView.setText(R.string.pull_to_refresh_release_label);
450 }
451 }
452
453
454 /**
455 * 手指抬起時,根據用戶下拉的高度來判斷是否是有效的下拉刷新操作。
456 * 如果下拉的距離超過header view的
457 * 1/2那麼則認為是有效的下拉刷新操作,否則恢復原來的視圖狀態.
458 */
459 private void changeHeaderViewStaus() {
460 int curScrollY = getScrollY();
461 // 超過1/2則認為是有效的下拉刷新, 否則還原
462 if (curScrollY < mInitScrollY / 2) {
463 mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()
464 - curScrollY);
465 mCurrentStatus = STATUS_REFRESHING;
466 mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);
467 mArrowImageView.clearAnimation();
468 mArrowImageView.setVisibility(View.GONE);
469 mProgressBar.setVisibility(View.VISIBLE);
470 } else {
471 mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
472 mCurrentStatus = STATUS_IDLE;
473 }
474
475 invalidate();
476 }
477
478 /**
479 * 執行下拉刷新
480 */
481 protected void doRefresh() {
482 changeHeaderViewStaus();
483 // 執行刷新操作
484 if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
485 mOnRefreshListener.onRefresh();
486 }
487 }
488
489 /**
490 * 刷新結束,恢復狀態
491 */
492 public void refreshComplete() {
493 mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
494 mCurrentStatus = STATUS_IDLE;
495 invalidate();
496 updateHeaderTimeStamp();
497
498 // 200毫秒後處理arrow和progressbar,免得太突兀
499 this.postDelayed(new Runnable() {
500
501 @Override
502 public void run() {
503 mArrowImageView.setVisibility(View.VISIBLE);
504 mProgressBar.setVisibility(View.GONE);
505 }
506 }, 100);
507
508 }
509
510 /**
511 * 修改header上的最近更新時間
512 */
513 private void updateHeaderTimeStamp() {
514 // 設置更新時間
515 mTimeTextView.setText(R.string.pull_to_refresh_update_time_label);
516 SimpleDateFormat sdf = (SimpleDateFormat) SimpleDateFormat.getInstance();
517 sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
518 mTimeTextView.append(sdf.format(new Date()));
519 }
520
521
522 /**
523 * 第六步:加載更多
524 * 滾動監聽,當滾動到最底部,且用戶設置了加載更多的監聽器時觸發加載更多操作.
525 * AbsListView, int, int, int)
526 */
527 @Override
528 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
529 int totalItemCount) {
530 // 用戶設置了加載更多監聽器,且到了最底部,並且是上拉操作,那麼執行加載更多.
531 if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY
532 && mYOffset <= 0
533 && mCurrentStatus == STATUS_IDLE) {
534 showFooterView();
535 doLoadMore();
536 }
537 }
538
539
540 @Override
541 public void onScrollStateChanged(AbsListView view, int scrollState) {
542
543 }
544
545 /**
546 * 執行下拉(自動)加載更多的操作
547 */
548 protected void doLoadMore() {
549 if (mLoadListener != null) {
550 mLoadListener.onLoadMore();
551 }
552 }
553 /**
554 * 顯示footer view
555 */
556 private void showFooterView() {
557 startScroll(mFooterView.getMeasuredHeight());
558 mCurrentStatus = STATUS_LOADING;
559 }
560
561 /**
562 * 加載結束,恢復狀態
563 */
564 public void loadCompelte() {
565 // 隱藏footer
566 startScroll(mInitScrollY - getScrollY());
567 mCurrentStatus = STATUS_IDLE;
568 }
569
570
571 /**
572 * 設置下拉刷新監聽器
573 *
574 * @param listener
575 */
576 public void setOnRefreshListener(OnRefreshListener listener) {
577 mOnRefreshListener = listener;
578 }
579
580 /**
581 * 設置滑動到底部時自動加載更多的監聽器
582 *
583 * @param listener
584 */
585 public void setOnLoadListener(OnLoadListener listener) {
586 mLoadListener = listener;
587 }
588
589
590 /**
591 * 是否已經到了最頂部,子類需覆寫該方法,使得mContentView滑動到最頂端時返回true, 如果到達最頂端用戶繼續下拉則攔截事件;
592 *
593 * @return
594 */
595 protected abstract boolean isTop();
596
597 /**
598 * 是否已經到了最底部,子類需覆寫該方法,使得mContentView滑動到最底端時返回true;從而觸發自動加載更多的操作
599 *
600 * @return
601 */
602 protected abstract boolean isBottom();
603
604
605 /**
606 * 返回Content View
607 *
608 * @return
609 */
610 public T getContentView() {
611 return mContentView;
612 }
613
614 /**
615 * @return
616 */
617 public View getHeaderView() {
618 return mHeaderView;
619 }
620
621 /**
622 * @return
623 */
624 public View getFooterView() {
625 return mFooterView;
626 }
627
628 }
實現下拉刷新的listview
1 package com.jiao.simpleimageview.view;
2
3 import android.content.Context;
4 import android.util.AttributeSet;
5 import android.widget.ListAdapter;
6 import android.widget.ListView;
7
8 /**
9 * Created by jiaocg on 2016/3/25.
10 */
11 public class RefreshListView extends RefreshLayoutBase<ListView> {
12 /**
13 * @param context
14 */
15 public RefreshListView(Context context) {
16 this(context, null);
17 }
18
19 /**
20 * @param context
21 * @param attrs
22 */
23 public RefreshListView(Context context, AttributeSet attrs) {
24 this(context, attrs, 0);
25 }
26
27 /**
28 * @param context
29 * @param attrs
30 * @param defStyle
31 */
32 public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
33 super(context, attrs, defStyle);
34 }
35
36 @Override
37 protected void setupContentView(Context context) {
38 mContentView = new ListView(context);
39 // 設置滾動監聽器
40 mContentView.setOnScrollListener(this);
41
42 }
43
44 @Override
45 protected boolean isTop() {
46
47 //當第一個可見項是第一項時表示已經拉倒了頂部
48 return mContentView.getFirstVisiblePosition() == 0
49 && getScrollY() <= mHeaderView.getMeasuredHeight();
50 }
51
52 @Override
53 protected boolean isBottom() {
54 //當最後一個可見項是最後一項時表示已經拉倒了底部
55 return mContentView != null && mContentView.getAdapter() != null
56 && mContentView.getLastVisiblePosition() ==
57 mContentView.getAdapter().getCount() - 1;
58 }
59
60 /**
61 * 設置adapter
62 */
63 public void setAdapter(ListAdapter adapter) {
64 mContentView.setAdapter(adapter);
65 }
66
67 public ListAdapter getAdapter() {
68 return mContentView.getAdapter();
69 }
70
71 }
然後直接在xml文件中引用使用即可實現,另外這種方式的下拉刷新擴展性很強
也可以實現TextView和GridView的刷新,只需繼承該base實現其中的抽象方法即可
源碼下載:https://yunpan.cn/cqKRSr2r2MsEk 提取密碼:d177
百度導航Android版問題集
百度導航Android版問題集軟硬件環境Macbook Pro MGX 72Android Studio 1.4酷比魔方7寸平板百度導航SDK 3.0.0運行導航Demo
My First Android Application Project 第一個安卓應用,android安卓
My First Android Application Project 第一個安卓應用,android安卓一、前言: 安卓(Android):是一種基於Linu
Android官方文檔翻譯 十七 4.1Starting an Activity
Android官方文檔翻譯 十七 4.1Starting an Activity Starting an Activity 開啟一個Activity This les
Android應用開發教程之五:EditText詳解
EditText在API中的結構 java.lang.Object android.view.View android.widget.Text