你的位置:首页 > 操作系统

[操作系统]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 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