編輯:關於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版問題集軟硬件環境Macbook Pro MGX 72Android Studio 1.4酷比魔方7寸平板百度導航SDK 3.0.0運行導航Demo
My First Android Application Project 第一個安卓應用,android安卓一、前言: 安卓(Android):是一種基於Linu
Android官方文檔翻譯 十七 4.1Starting an Activity Starting an Activity 開啟一個Activity This les
EditText在API中的結構 java.lang.Object android.view.View android.widget.Text