打造通用的Android下拉刷新组件(适用于ListView、GridView等各类View)

最近在做项目时,使用了一个开源的下拉刷新ListView组件,极其的不稳定,bug还多。稳定的组件又写得太复杂了,jar包较大。在我的一篇博客中也讲述过下拉刷新的实现,即Android打造(ListView、GridView等)通用的下拉刷新、上拉自动加载的组件。但是这种通过修改Margin的形式感觉不是特别的流畅,因此在这漫长的国庆长假又花了点时间用另外的原理实现了一遍,特此分享出来。


基本原理

原理就是自定义一个ViewGroup,将Header View, Content View, Footer View从上到下依次布局,如图1 (红色区域为屏幕的显示区域)。在初始时通过滚动,使得该组件在Y轴方向上滚动HeaderView的高度的距离,这样HeaderView就被隐藏掉了,如图2。而Content View的宽度和高度都是match_parent的,因此此时屏幕上只显示Content View, HeaderView 和 FooterView都被隐藏在屏幕外了。当组件被滚动到顶端时,如果用户继续下拉,那么拦截触摸事件,然后通过Scroller来滚动y轴的偏移量,实现逐步的显示HeaderView,从而到达下拉的效果,如图3。当用户滑动到最底部时会触发加载更多的操作。

                 

图 1 (红色区域为屏幕) 图2 (红色区域为屏幕) 图 3(红色区域为屏幕)

通过使用Scroller使得整个滚动更加的平滑,而使用Margin来实现的话需要自己来计算滚动时间和margin值,并不是很流畅,而且频繁的修改布局参数效率也不高。使用Scroller只是滚动位置,而没有修改布局参数,因此有点较为突出。


Scroller的使用

为了更好的理解下拉刷的实现,我们先要了解Scroller的作用以及如何使用。这里我们将做一个简单的示例来说明。

Scroller是一个帮助View滚动的辅助类,在使用它之前用户需要通过startScroll来设置滚动的参数,即起始点坐标和x,y轴上要滚动的距离。Scroller它封装了滚动时间、要滚动的目标x轴和y轴,以及在每个时间内view应该滚动到的x,y轴的坐标点,这样用户就可以在有效的滚动周期内通过Scroller的getCurX()和getCurY()来获取当前时刻View应该滚动的位置,然后通过调用View的scrollTo或者ScrollBy方法进行滚动。那么如何判断滚动是否结束呢 ? 我们只需要覆写View类的computeScroll方法,该方法会在View绘制的时候被调用,在里面调用Scroller的computeScrollOffset来判断滚动是否完成,如果返回true表明滚动未完成,否则滚动完成。上述说的scrollTo或者ScrollBy的调用就是在computeScrollOffset为true的情况下调用,并且最后还要调用目标view的postInvalidate()或者invalidate()以实现View的重绘。View的重绘又会导致computeScroll方法被调用,从而继续整个滚动过程,直至computeScrollOffset返回false, 即滚动结束。整个过程有点绕,我们看一个例子吧。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. public class ScrollLayout extends FrameLayout {  
  2.   
  3.     private String TAG = ScrollLayout.class.getSimpleName();  
  4.   
  5.   
  6.     Scroller mScroller ;  
  7.   
  8.   
  9.     public ScrollLayout(Context context) {  
  10.         super(context);  
  11.           
  12.         mScroller = new Scroller(context) ;  
  13.     }  
  14.   
  15.       
  16.     // 该函数会在View重绘之时被调用  
  17.     @Override  
  18.     public void computeScroll() {  
  19.         if ( mScroller.computeScrollOffset() ) {  
  20.             // 滚动到此刻View应该滚动到的x,y坐标上.  
  21.             this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  22.             // 请求重绘该View,从而又会导致computeScroll被调用,然后继续滚动,直到computeScrollOffset返回false  
  23.             this.postInvalidate();  
  24.         }  
  25.     }  
  26.   
  27.   
  28.     // 调用这个方法进行滚动,这里我们只滚动竖直方向,  
  29.     public void scrollTo(int y) {  
  30.             // 参数1和参数2分别为滚动的起始点水平、竖直方向的滚动偏移量  
  31.             // 参数3和参数4为在水平和竖直方向上滚动的距离  
  32.             mScroller.startScroll(getScrollX(), getScrollY(), 0, y);  
  33.             this.invalidate();  
  34.     }  
  35. }  

滚动该视图的代码 : 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. ScrollLayout scrollView = new ScrollLayout(getContext()) ;  
  2. scrollView.scrollTo(100);  
通过上面这段代码会让scrollView在y轴上向下滚动100个像素点。我们结合代码来分析一下。首先调用scrollTo(int y)方法,然后我们在该方法中通过mScroller.startScroll()方法来设置了滚动的参数,然后调用invalidate()方法使得该View重绘。重绘时会调用computeScroll方法,在该方法中通过mScroller.computeScrollOffset()判断滚动是否完成,如果返回true那代表没有滚动完成,此时把该View滚动到此刻View应该滚动到的x, y位置,这个位置通过mScroller的getCurX, getCurY获得。然后继续调用重绘方法,继续执行滚动过程,直至滚动完成。

了解了Scroller原理后,我们继续看通用的下拉刷新组件的实现吧。


下拉刷新实现

代码量不算多,但是也挺长的,我们这里只拿出重要的点来分析,完成的源码在博文最后会给出。以下是重要的代码段 : 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * @author mrsimple 
  3.  */  
  4. public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements  
  5.         OnScrollListener {  
  6.   
  7.     /** 
  8.      *  
  9.      */  
  10.     protected Scroller mScroller;  
  11.   
  12.     /** 
  13.      * 下拉刷新时显示的header view 
  14.      */  
  15.     protected View mHeaderView;  
  16.   
  17.     /** 
  18.      * 上拉加载更多时显示的footer view 
  19.      */  
  20.     protected View mFooterView;  
  21.   
  22.     /** 
  23.      * 本次触摸滑动y坐标上的偏移量 
  24.      */  
  25.     protected int mYOffset;  
  26.   
  27.     /** 
  28.      * 内容视图, 即用户触摸导致下拉刷新、上拉加载的主视图. 比如ListView, GridView等. 
  29.      */  
  30.     protected T mContentView;  
  31.   
  32.     /** 
  33.      * 最初的滚动位置.第一次布局时滚动header的高度的距离 
  34.      */  
  35.     protected int mInitScrollY = 0;  
  36.     /** 
  37.      * 最后一次触摸事件的y轴坐标 
  38.      */  
  39.     protected int mLastY = 0;  
  40.   
  41.     /** 
  42.      * 空闲状态 
  43.      */  
  44.     public static final int STATUS_IDLE = 0;  
  45.   
  46.     /** 
  47.      * 下拉或者上拉状态, 还没有到达可刷新的状态 
  48.      */  
  49.     public static final int STATUS_PULL_TO_REFRESH = 1;  
  50.   
  51.     /** 
  52.      * 下拉或者上拉状态 
  53.      */  
  54.     public static final int STATUS_RELEASE_TO_REFRESH = 2;  
  55.     /** 
  56.      * 刷新中 
  57.      */  
  58.     public static final int STATUS_REFRESHING = 3;  
  59.   
  60.     /** 
  61.      * LOADING中 
  62.      */  
  63.     public static final int STATUS_LOADING = 4;  
  64.   
  65.     /** 
  66.      * 当前状态 
  67.      */  
  68.     protected int mCurrentStatus = STATUS_IDLE;  
  69.   
  70.     /** 
  71.      * 下拉刷新监听器 
  72.      */  
  73.     protected OnRefreshListener mOnRefreshListener;  
  74.   
  75.     /** 
  76.      * header中的箭头图标 
  77.      */  
  78.     private ImageView mArrowImageView;  
  79.     /** 
  80.      * 箭头是否向上 
  81.      */  
  82.     private boolean isArrowUp;  
  83.     /** 
  84.      * header 中的文本标签 
  85.      */  
  86.     private TextView mTipsTextView;  
  87.     /** 
  88.      * header中的时间标签 
  89.      */  
  90.     private TextView mTimeTextView;  
  91.     /** 
  92.      * header中的进度条 
  93.      */  
  94.     private ProgressBar mProgressBar;  
  95.     /** 
  96.      *  
  97.      */  
  98.     private int mScreenHeight;  
  99.     /** 
  100.      *  
  101.      */  
  102.     private int mHeaderHeight;  
  103.     /** 
  104.      *  
  105.      */  
  106.     protected OnLoadListener mLoadListener;  
  107.   
  108.     /** 
  109.      * @param context 
  110.      */  
  111.     public RefreshLayoutBase(Context context) {  
  112.         this(context, null);  
  113.     }  
  114.   
  115.     /** 
  116.      * @param context 
  117.      * @param attrs 
  118.      */  
  119.     public RefreshLayoutBase(Context context, AttributeSet attrs) {  
  120.         this(context, attrs, 0);  
  121.     }  
  122.   
  123.     /** 
  124.      * @param context 
  125.      * @param attrs 
  126.      * @param defStyle 
  127.      */  
  128.     public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyle) {  
  129.         super(context, attrs);  
  130.   
  131.         // 初始化Scroller对象  
  132.         mScroller = new Scroller(context);  
  133.   
  134.         // 获取屏幕高度  
  135.         mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;  
  136.         // header 的高度为屏幕高度的 1/4  
  137.         mHeaderHeight = mScreenHeight / 4;  
  138.   
  139.         // 初始化整个布局  
  140.         initLayout(context);  
  141.     }  
  142.   
  143.     /** 
  144.      * 初始化整个布局 
  145.      *  
  146.      * @param context 
  147.      */  
  148.     private final void initLayout(Context context) {  
  149.   
  150.         // header view  
  151.         setupHeaderView(context);  
  152.   
  153.         // 设置内容视图  
  154.         setupContentView(context);  
  155.         // 设置布局参数  
  156.         setDefaultContentLayoutParams();  
  157.         //  
  158.         addView(mContentView);  
  159.   
  160.         // footer view  
  161.         setupFooterView(context);  
  162.   
  163.     }  
  164.   
  165.     /** 
  166.      * 初始化 header view 
  167.      */  
  168.     protected void setupHeaderView(Context context) {  
  169.         mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this,  
  170.                 false);  
  171.         mHeaderView  
  172.                 .setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,  
  173.                         mHeaderHeight));  
  174.         mHeaderView.setBackgroundColor(Color.RED);  
  175.         // header的高度整个为1/4的屏幕高度,但是它只有100px是有效的显示区域,取余取余为paddingTop,这样是为了达到下拉的效果  
  176.         mHeaderView.setPadding(0, mHeaderHeight - 10000);  
  177.         addView(mHeaderView);  
  178.   
  179.         // HEADER VIEWS  
  180.         mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);  
  181.         mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);  
  182.         mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);  
  183.         mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);  
  184.     }  
  185.   
  186.     /** 
  187.      * 初始化Content View, 子类覆写. 
  188.      */  
  189.     protected abstract void setupContentView(Context context);  
  190.   
  191.   
  192.     /** 
  193.      * 与Scroller合作,实现平滑滚动。在该方法中调用Scroller的computeScrollOffset来判断滚动是否结束。如果没有结束, 
  194.      * 那么滚动到相应的位置,并且调用postInvalidate方法重绘界面,从而再次进入到这个computeScroll流程,直到滚动结束。 
  195.      */  
  196.     @Override  
  197.     public void computeScroll() {  
  198.         if (mScroller.computeScrollOffset()) {  
  199.             scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
  200.             postInvalidate();  
  201.         }  
  202.     }  
  203.   
  204.     /* 
  205.      * 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,并且是下拉时拦截触摸事件,否则不拦截,交给其child 
  206.      * view 来处理。 
  207.      * @see 
  208.      * android.view.ViewGroup#onInterceptTouchEvent(android.view.MotionEvent) 
  209.      */  
  210.     @Override  
  211.     public boolean onInterceptTouchEvent(MotionEvent ev) {  
  212.   
  213.         /* 
  214.          * This method JUST determines whether we want to intercept the motion. 
  215.          * If we return true, onTouchEvent will be called and we do the actual 
  216.          * scrolling there. 
  217.          */  
  218.         final int action = MotionEventCompat.getActionMasked(ev);  
  219.         // Always handle the case of the touch gesture being complete.  
  220.         if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {  
  221.             // Do not intercept touch event, let the child handle it  
  222.             return false;  
  223.         }  
  224.   
  225.         switch (action) {  
  226.   
  227.             case MotionEvent.ACTION_DOWN:  
  228.                 mLastY = (int) ev.getRawY();  
  229.                 break;  
  230.   
  231.             case MotionEvent.ACTION_MOVE:  
  232.                 // int yDistance = (int) ev.getRawY() - mYDown;  
  233.                 mYOffset = (int) ev.getRawY() - mLastY;  
  234.                 // 如果拉到了顶部, 并且是下拉,则拦截触摸事件,从而转到onTouchEvent来处理下拉刷新事件  
  235.                 if (isTop() && mYOffset > 0) {  
  236.                     return true;  
  237.                 }  
  238.                 break;  
  239.   
  240.         }  
  241.   
  242.         // Do not intercept touch event, let the child handle it  
  243.         return false;  
  244.     }  
  245.   
  246.     /** 
  247.      * 是否已经到了最顶部,子类需覆写该方法,使得mContentView滑动到最顶端时返回true, 如果到达最顶端用户继续下拉则拦截事件; 
  248.      *  
  249.      * @return 
  250.      */  
  251.     protected abstract boolean isTop();  
  252.   
  253.     /** 
  254.      * 是否已经到了最底部,子类需覆写该方法,使得mContentView滑动到最底端时返回true;从而触发自动加载更多的操作 
  255.      *  
  256.      * @return 
  257.      */  
  258.     protected abstract boolean isBottom();  
  259.   
  260.     /** 
  261.      * 显示footer view 
  262.      */  
  263.     private void showFooterView() {  
  264.         startScroll(mFooterView.getMeasuredHeight());  
  265.         mCurrentStatus = STATUS_LOADING;  
  266.     }  
  267.   
  268.     /** 
  269.      * 设置滚动的参数 
  270.      *  
  271.      * @param yOffset 
  272.      */  
  273.     private void startScroll(int yOffset) {  
  274.         mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);  
  275.         invalidate();  
  276.     }  
  277.   
  278.     /* 
  279.      * 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题 
  280.      * @see android.view.View#onTouchEvent(android.view.MotionEvent) 
  281.      */  
  282.     @Override  
  283.     public boolean onTouchEvent(MotionEvent event) {  
  284.   
  285.         Log.d(VIEW_LOG_TAG, "@@@ onTouchEvent : action = " + event.getAction());  
  286.         switch (event.getAction()) {  
  287.             case MotionEvent.ACTION_DOWN:  
  288.                 mLastY = (int) event.getRawY();  
  289.                 break;  
  290.   
  291.             case MotionEvent.ACTION_MOVE:  
  292.                 int currentY = (int) event.getRawY();  
  293.                 mYOffset = currentY - mLastY;  
  294.                 if (mCurrentStatus != STATUS_LOADING) {  
  295.                     //  
  296.                     changeScrollY(mYOffset);  
  297.                 }  
  298.   
  299.                 rotateHeaderArrow();  
  300.                 changeTips();  
  301.                 mLastY = currentY;  
  302.                 break;  
  303.   
  304.             case MotionEvent.ACTION_UP:  
  305.                 // 下拉刷新的具体操作  
  306.                 doRefresh();  
  307.                 break;  
  308.             default:  
  309.                 break;  
  310.   
  311.         }  
  312.   
  313.         return true;  
  314.     }  
  315.   
  316.     /** 
  317.      * 修改y轴上的滚动值,从而实现header被下拉的效果 
  318.      * @param distance 
  319.      * @return 
  320.      */  
  321.     private void changeScrollY(int distance) {  
  322.         // 最大值为 scrollY(header 隐藏), 最小值为0 ( header 完全显示).  
  323.         int curY = getScrollY();  
  324.         // 下拉  
  325.         if (distance > 0 && curY - distance > getPaddingTop()) {  
  326.             scrollBy(0, -distance);  
  327.         } else if (distance < 0 && curY - distance <= mInitScrollY) {  
  328.             // 上拉过程  
  329.             scrollBy(0, -distance);  
  330.         }  
  331.   
  332.         curY = getScrollY();  
  333.         int slop = mInitScrollY / 2;  
  334.         //  
  335.         if (curY > 0 && curY < slop) {  
  336.             mCurrentStatus = STATUS_RELEASE_TO_REFRESH;  
  337.         } else if (curY > 0 && curY > slop) {  
  338.             mCurrentStatus = STATUS_PULL_TO_REFRESH;  
  339.         }  
  340.     }  
  341.   
  342.   
  343.   
  344.     /** 
  345.      * 刷新结束,恢复状态 
  346.      */  
  347.     public void refreshComplete() {  
  348.         mCurrentStatus = STATUS_IDLE;  
  349.         // 隐藏header view  
  350.         mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());  
  351.         invalidate();  
  352.         updateHeaderTimeStamp();  
  353.   
  354.         // 200毫秒后处理arrow和progressbar,免得太突兀  
  355.         this.postDelayed(new Runnable() {  
  356.   
  357.             @Override  
  358.             public void run() {  
  359.                 mArrowImageView.setVisibility(View.VISIBLE);  
  360.                 mProgressBar.setVisibility(View.GONE);  
  361.             }  
  362.         }, 100);  
  363.   
  364.     }  
  365.   
  366.     /** 
  367.      * 加载结束,恢复状态 
  368.      */  
  369.     public void loadCompelte() {  
  370.         // 隐藏footer  
  371.         startScroll(mInitScrollY - getScrollY());  
  372.         mCurrentStatus = STATUS_IDLE;  
  373.     }  
  374.   
  375.     /** 
  376.      * 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作。如果下拉的距离超过header view的 
  377.      * 1/2那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态. 
  378.      */  
  379.     private void changeHeaderViewStaus() {  
  380.         int curScrollY = getScrollY();  
  381.         // 超过1/2则认为是有效的下拉刷新, 否则还原  
  382.         if (curScrollY < mInitScrollY / 2) {  
  383.             // 滚动到能够正常显示header的位置  
  384.             mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()  
  385.                     - curScrollY);  
  386.             mCurrentStatus = STATUS_REFRESHING;  
  387.             mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);  
  388.             mArrowImageView.clearAnimation();  
  389.             mArrowImageView.setVisibility(View.GONE);  
  390.             mProgressBar.setVisibility(View.VISIBLE);  
  391.         } else {  
  392.             mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);  
  393.             mCurrentStatus = STATUS_IDLE;  
  394.         }  
  395.   
  396.         invalidate();  
  397.     }  
  398.   
  399.     /** 
  400.      * 执行下拉刷新 
  401.      */  
  402.     private void doRefresh() {  
  403.         changeHeaderViewStaus();  
  404.         // 执行刷新操作  
  405.         if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {  
  406.             mOnRefreshListener.onRefresh();  
  407.         }  
  408.     }  
  409.   
  410.     /** 
  411.      * 执行下拉(自动)加载更多的操作 
  412.      */  
  413.     private void doLoadMore() {  
  414.         if (mLoadListener != null) {  
  415.             mLoadListener.onLoadMore();  
  416.         }  
  417.     }  
  418.   
  419.   
  420.     /* 
  421.      * 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header, content view, footer这三个子控件的高度只和。 
  422.      * @see android.view.View#onMeasure(int, int) 
  423.      */  
  424.     @Override  
  425.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  426.   
  427.         int width = MeasureSpec.getSize(widthMeasureSpec);  
  428.   
  429.         int childCount = getChildCount();  
  430.   
  431.         int finalHeight = 0;  
  432.   
  433.         for (int i = 0; i < childCount; i++) {  
  434.             View child = getChildAt(i);  
  435.             // measure  
  436.             measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  437.             // 该view所需要的总高度  
  438.             finalHeight += child.getMeasuredHeight();  
  439.         }  
  440.   
  441.         setMeasuredDimension(width, finalHeight);  
  442.     }  
  443.   
  444.     /* 
  445.      * 布局函数,将header, content view, 
  446.      * footer这三个view从上到下布局。布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度 + 
  447.      * 本视图的paddingTop,从而达到隐藏header的效果. 
  448.      * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int) 
  449.      */  
  450.     @Override  
  451.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  452.   
  453.         int childCount = getChildCount();  
  454.         int top = getPaddingTop();  
  455.         for (int i = 0; i < childCount; i++) {  
  456.             View child = getChildAt(i);  
  457.             child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);  
  458.             top += child.getMeasuredHeight();  
  459.         }  
  460.   
  461.         // 计算初始化滑动的y轴距离  
  462.         mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();  
  463.         // 滑动到header view高度的位置, 从而达到隐藏header view的效果  
  464.         scrollTo(0, mInitScrollY);  
  465.     }  
  466.   
  467.   
  468.     /* 
  469.      * 滚动监听,当滚动到最底部,且用户设置了加载更多的监听器时触发加载更多操作. 
  470.      * @see android.widget.AbsListView.OnScrollListener#onScroll(android.widget. 
  471.      * AbsListView, int, int, int) 
  472.      */  
  473.     @Override  
  474.     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,  
  475.             int totalItemCount) {  
  476.         // 用户设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多.  
  477.         if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY  
  478.                 && mYOffset <= 0  
  479.                 && mCurrentStatus == STATUS_IDLE) {  
  480.             showFooterView();  
  481.             doLoadMore();  
  482.         }  
  483.     }  
  484.   
  485. }  
在构造函数中会调用initLayout来添加Header View, Content View, Footer View这三个区域的视图, 其中Content View就是我们的核心组件,比如ListView、GridView,这个区域的视图默认宽高都是match_parent的。Header的高度为屏幕宽度的1/4,但它的有效显示区域只有100像素,其他的都是paddingTop,这样就是的内容显示区域显示在最下面。这样当用户一直下拉时,首先会显示内容区域,继续下拉则会显示PaddingTop区域,此时就达到header view高度被拉伸的效果。如下图 : 

      

图 4 图5

不断下拉,y轴的偏移量不断减小,使得header越来越多的部分显示出来。只有白色的内容显示区域是有效的显示区,上面的绿色都是paddingTop区,这样就形成了被拉伸的效果。
添加这三个view之后,我们在onMeasure中对这几个子view进行丈量。使得该组件的宽度为用户设置的宽度,高度为header, content view, footer的高度之和。得到各个子视图的宽高和该组件的总宽高以后,会进行布局操作,即会调用onLayout方法。我们把这个几个视图从上到下排列。最后将该组件在y方向上滚动与header view的高度同样大小的像素值,使得header view隐藏掉,使得Content View完全显示出来。

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /* 
  2.  * 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header, content view, footer这三个子控件的高度只和。 
  3.  * @see android.view.View#onMeasure(int, int) 
  4.  */  
  5. @Override  
  6. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  7.   
  8.     int width = MeasureSpec.getSize(widthMeasureSpec);  
  9.   
  10.     int childCount = getChildCount();  
  11.   
  12.     int finalHeight = 0;  
  13.   
  14.     for (int i = 0; i < childCount; i++) {  
  15.         View child = getChildAt(i);  
  16.         // measure  
  17.         measureChild(child, widthMeasureSpec, heightMeasureSpec);  
  18.         // 该view所需要的总高度  
  19.         finalHeight += child.getMeasuredHeight();  
  20.     }  
  21.   
  22.     setMeasuredDimension(width, finalHeight);  
  23. }  
  24.   
  25. /* 
  26.  * 布局函数,将header, content view, 
  27.  * footer这三个view从上到下布局。布局完成后通过Scroller滚动到header的底部,即滚动距离为header的高度 + 
  28.  * 本视图的paddingTop,从而达到隐藏header的效果. 
  29.  * @see android.view.ViewGroup#onLayout(boolean, int, int, int, int) 
  30.  */  
  31. @Override  
  32. protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  33.   
  34.     int childCount = getChildCount();  
  35.     int top = getPaddingTop();  
  36.     for (int i = 0; i < childCount; i++) {  
  37.         View child = getChildAt(i);  
  38.         child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);  
  39.         top += child.getMeasuredHeight();  
  40.     }  
  41.   
  42.     // 计算初始化滑动的y轴距离  
  43.     mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();  
  44.     // 滑动到header view高度的位置, 从而达到隐藏header view的效果  
  45.     scrollTo(0, mInitScrollY);  
  46. }  
然后就是下拉刷新触发点了。在onInterceptTouchEvent方法中,对于ACTION_MOVE事件我们会判断,如果已经滑到了Content View的顶部,并且还继续下拉,那么拦截触摸事件,使得事件转到onTouchEvent方法中处理。事件拦截的关键点如下 :

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. case MotionEvent.ACTION_MOVE:  
  2.           // int yDistance = (int) ev.getRawY() - mYDown;  
  3.           mYOffset = (int) ev.getRawY() - mLastY;  
  4.           // 如果拉到了顶部, 并且是下拉,则拦截触摸事件,从而转到onTouchEvent来处理下拉刷新事件  
  5.           if (isTop() && mYOffset > 0) {  
  6.               return true;  
  7.           }  
  8.           break;  
如果在onTouchEvent中我们根据用户当前触摸事件的y轴位置与上一次的y轴位置的偏移量来修改该组件在y轴上的滚动值,调用的方法为changeScrollY()函数,并且会修改header中的文本内容。当用户抬起手指时,会判断用户在y轴上滑动的距离是否大于header view的1/2, 如果大于header view的1/2那么为有效的下拉刷新,此时滚动到刚好显示header view的内容y轴位置,然后触发刷新操作,直到用户调用refreshCompete()位置,最后完全隐藏header。否则视为无效的下拉刷新操作,然后通过Scroller滚动来隐藏header view。

而加载更多操作为用户滑动到了最底部,并且继续上拉,那么会触发加载更多的操作。在操作在onScroll方法中被触发。

基本原理就是通过一个ViewGroup来组织header view, content view, footer view, 使它们从上到下排列,并且在初始化时滚动y轴,使得header 和 footer完全隐藏,只显示content view。用户下拉或者上拉时,通过判断是否显示header 或者 footer, 也是通过Scroller来滚动y轴的偏移量来实现HeaderView, Footer View的显示和隐藏,不需要修改margin值,这样效率更高,滚动也更平滑。当用户的上拉或者下拉操作满足了条件时,则会触发相应的操作,即下拉刷新、上拉加载更多。如有不明白的地方,就对比参考Android打造(ListView、GridView等)通用的下拉刷新、上拉自动加载的组件吧,原理都差不多。


下拉刷新的ListView

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. /** 
  2.  * @author mrsimple 
  3.  */  
  4. public class RefreshListView extends RefreshLayoutBase<ListView> {  
  5.   
  6.     /** 
  7.      * @param context 
  8.      */  
  9.     public RefreshListView(Context context) {  
  10.         this(context, null);  
  11.     }  
  12.   
  13.     /** 
  14.      * @param context 
  15.      * @param attrs 
  16.      */  
  17.     public RefreshListView(Context context, AttributeSet attrs) {  
  18.         this(context, attrs, 0);  
  19.     }  
  20.   
  21.     /** 
  22.      * @param context 
  23.      * @param attrs 
  24.      * @param defStyle 
  25.      */  
  26.     public RefreshListView(Context context, AttributeSet attrs, int defStyle) {  
  27.         super(context, attrs, defStyle);  
  28.     }  
  29.   
  30.     @Override  
  31.     protected void setupContentView(Context context) {  
  32.         mContentView = new ListView(context);  
  33.         // 设置滚动监听器  
  34.         mContentView.setOnScrollListener(this);  
  35.   
  36.     }  
  37.   
  38.     @Override  
  39.     protected boolean isTop() {  
  40.   
  41.         // Log.d(VIEW_LOG_TAG,  
  42.         // "### first pos = " + mContentView.getFirstVisiblePosition()  
  43.         // + ", getScrollY= " + getScrollY());  
  44.         return mContentView.getFirstVisiblePosition() == 0  
  45.                 && getScrollY() <= mHeaderView.getMeasuredHeight();  
  46.     }  
  47.   
  48.     @Override  
  49.     protected boolean isBottom() {  
  50.         // Log.d(VIEW_LOG_TAG, "### last position = " +  
  51.         // contentView.getLastVisiblePosition()  
  52.         // + ", count = " + contentView.getAdapter().getCount());  
  53.         return mContentView != null && mContentView.getAdapter() != null  
  54.                 && mContentView.getLastVisiblePosition() ==  
  55.                 mContentView.getAdapter().getCount() - 1;  
  56.     }  
  57. }  
需要下拉刷新的组件只需要实现isTop来判断是否滑动到最顶端、isBottom是否滑动到最底部,已经通过setupContentView设置mContentView对象即可。

使用示例

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. final RefreshListView refreshLayout = new RefreshListView(this);  
  2. String[] dataStrings = new String[20];  
  3. for (int i = 0; i < dataStrings.length; i++) {  
  4.     dataStrings[i] = "item - " +  
  5.             i;  
  6. }  
  7. // 获取ListView, 这里的listview就是Content view  
  8. refreshLayout.setAdapter(new ArrayAdapter<String>(this,  
  9.         android.R.layout.simple_list_item_1, dataStrings));  
  10. // 设置下拉刷新监听器  
  11. refreshLayout.setOnRefreshListener(new OnRefreshListener() {  
  12.   
  13.     @Override  
  14.     public void onRefresh() {  
  15.         Toast.makeText(getApplicationContext(), "refreshing", Toast.LENGTH_SHORT)  
  16.                 .show();  
  17.   
  18.         refreshLayout.postDelayed(new Runnable() {  
  19.   
  20.             @Override  
  21.             public void run() {  
  22.                 refreshLayout.refreshComplete();  
  23.             }  
  24.         }, 1500);  
  25.     }  
  26. });  
  27.   
  28. // 不设置的话到底部不会自动加载  
  29. refreshLayout.setOnLoadListener(new OnLoadListener() {  
  30.   
  31.     @Override  
  32.     public void onLoadMore() {  
  33.         Toast.makeText(getApplicationContext(), "loading", Toast.LENGTH_SHORT)  
  34.                 .show();  
  35.   
  36.         refreshLayout.postDelayed(new Runnable() {  
  37.   
  38.             @Override  
  39.             public void run() {  
  40.                 refreshLayout.loadCompelte();  
  41.             }  
  42.         }, 1500);  
  43.     }  
  44. });  

效果图 : 


效果图中含有下拉刷新的ListView, GridView, TextView,可以看到即使实在模拟器中,下拉刷新的效果都挺流畅的。上拉加载更多在ListView中正常显示,GridView中在模拟器上没有触发,但是在真机上是正常的。


代码地址

github在此  通用的下拉刷新组件,前言中提到的版本也在该仓库中,两个版本所在的包不一样。这篇文章的在com/uit/pullrefresh/scroller包下,前言中提到的版本在com/uit/pullrefresh/base包下。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值