这一篇主要来讲一下自定义控件中的自定义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 context134 */135 public RefreshLayoutBase(Context context) {136 this(context, null);137 }138 139 /**140 * @param context141 * @param attrs142 */143 public RefreshLayoutBase(Context context, AttributeSet attrs) {144 this(context, attrs, 0);145 }146 147 /**148 * @param context149 * @param attrs150 * @param defStyle151 */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/4161 mHeaderHeight = mScreenHeight / 4;162 163 // 初始化整个布局164 initLayout(context);165 }166 167 /**168 * 第一步:初始化整个布局169 *170 * @param context171 */172 private final void initLayout(Context context) {173 // header view174 setupHeaderView(context);175 // 设置内容视图176 setupContentView(context);177 // 设置布局参数178 setDefaultContentLayoutParams();179 // 添加mContentView180 addView(mContentView);181 // footer view182 setupFooterView(context);183 184 }185 186 /**187 * 初始化 header view188 */189 protected void setupHeaderView(Context context) {190 mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,191 false);192 mHeaderView193 .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 VIEWS200 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 view224 */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 widthMeasureSpec238 * @param heightMeasureSpec239 */240 @Override241 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 // measure248 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 @Override263 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 @Override288 public void computeScroll() {289 if (mScroller.computeScrollOffset()) {290 scrollTo(mScroller.getCurrX(), mScroller.getCurrY());291 postInvalidate();292 }293 }294 295 /*296 * 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,297 * 并且是下拉时拦截触摸事件,否则不拦截,交给其child298 * view 来处理。299 */300 @Override301 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 it307 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 it327 return false;328 }329 330 /**331 * 第五步:下拉刷新332 * 1、滑动view显示出headerview333 * 2、进度条滚动,修改标题内容334 * 3、执行下拉刷新监听335 * 4、刷新成功或失败后重置:隐藏headerview 修改标题内容336 * 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题337 *338 * @see android.view.View#onTouchEvent(android.view.MotionEvent)339 */340 @Override341 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 yOffset369 */370 private void startScroll(int yOffset) {371 mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);372 invalidate();373 }374 375 /**376 * y轴上滑动到指定位置377 *378 * @param distance379 * @return380 */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 @Override502 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 @Override528 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,529 int totalItemCount) {530 // 用户设置了加载更多**,且到了最底部,并且是上拉操作,那么执行加载更多.531 if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY532 && mYOffset <= 0533 && mCurrentStatus == STATUS_IDLE) {534 showFooterView();535 doLoadMore();536 }537 }538 539 540 @Override541 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 view555 */556 private void showFooterView() {557 startScroll(mFooterView.getMeasuredHeight());558 mCurrentStatus = STATUS_LOADING;559 }560 561 /**562 * 加载结束,恢复状态563 */564 public void loadCompelte() {565 // 隐藏footer566 startScroll(mInitScrollY - getScrollY());567 mCurrentStatus = STATUS_IDLE;568 }569 570 571 /**572 * 设置下拉刷新**573 *574 * @param listener575 */576 public void setOnRefreshListener(OnRefreshListener listener) {577 mOnRefreshListener = listener;578 }579 580 /**581 * 设置滑动到底部时自动加载更多的**582 *583 * @param listener584 */585 public void setOnLoadListener(OnLoadListener listener) {586 mLoadListener = listener;587 }588 589 590 /**591 * 是否已经到了最顶部,子类需覆写该方法,使得mContentView滑动到最顶端时返回true, 如果到达最顶端用户继续下拉则拦截事件;592 *593 * @return594 */595 protected abstract boolean isTop();596 597 /**598 * 是否已经到了最底部,子类需覆写该方法,使得mContentView滑动到最底端时返回true;从而触发自动加载更多的操作599 *600 * @return601 */602 protected abstract boolean isBottom();603 604 605 /**606 * 返回Content View607 *608 * @return609 */610 public T getContentView() {611 return mContentView;612 }613 614 /**615 * @return616 */617 public View getHeaderView() {618 return mHeaderView;619 }620 621 /**622 * @return623 */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 context14 */15 public RefreshListView(Context context) {16 this(context, null);17 }18 19 /**20 * @param context21 * @param attrs22 */23 public RefreshListView(Context context, AttributeSet attrs) {24 this(context, attrs, 0);25 }26 27 /**28 * @param context29 * @param attrs30 * @param defStyle31 */32 public RefreshListView(Context context, AttributeSet attrs, int defStyle) {33 super(context, attrs, defStyle);34 }35 36 @Override37 protected void setupContentView(Context context) {38 mContentView = new ListView(context);39 // 设置滚动**40 mContentView.setOnScrollListener(this);41 42 }43 44 @Override45 protected boolean isTop() {46 47 //当第一个可见项是第一项时表示已经拉倒了顶部48 return mContentView.getFirstVisiblePosition() == 049 && getScrollY() <= mHeaderView.getMeasuredHeight();50 }51 52 @Override53 protected boolean isBottom() {54 //当最后一个可见项是最后一项时表示已经拉倒了底部55 return mContentView != null && mContentView.getAdapter() != null56 && mContentView.getLastVisiblePosition() ==57 mContentView.getAdapter().getCount() - 1;58 }59 60 /**61 * 设置adapter62 */63 public void setAdapter(ListAdapter adapter) {64 mContentView.setAdapter(adapter);65 }66 67 public ListAdapter getAdapter() {68 return mContentView.getAdapter();69 }70 71 }
然后直接在
也可以实现TextView和GridView的刷新,只需继承该base实现其中的抽象方法即可
源码下载:https://yunpan.cn/cqKRSr2r2MsEk 提取密码:d177
原标题:Android自定义控件(二)
关键词:Android