Android自定义控件实战——下拉刷新控件终结者:PullToRefreshLayout

简介:

转载请声明出处http://blog.csdn.net/zhongkejingwang/article/details/38340701    

       说到下拉刷新控件,网上版本有很多,很多软件也都有下拉刷新功能。有一个叫XListView的,我看别人用过,没看过是咋实现的,看这名字估计是继承自ListView修改的,不过效果看起来挺丑的,也没什么扩展性,太单调了。看了QQ2014的列表下拉刷新,发现挺好看的,我喜欢,贴一下图看一下qq的下拉刷新效果:

                                          

    不错吧?嗯,是的。一看就知道实现方式不一样。咱们今天就来实现一个下拉刷新控件。由于有时候不仅仅是ListView需要下拉刷新,ExpandableListView和GridView也有这个需求,由于ListView,GridView都是AbsListView的子类,ExpandableListView是ListView的子类所以也是AbsListView的子类。所以我的思路是自定义一个对所有AbsListView的子类通用的下拉管理布局,叫PullToRefreshLayout,如果需要GridView,只需要在布局文件里将ListView换成GridView就行了,ExpandableListView也一样,不需要再继承什么GridView啊ListView啊乱七八糟的。


看上图,主要工作就是定义黑色大布局,红色部分是不下拉的时候的可见部分,可以是任意的AbsListView的子类(GridView,ListView,ExpandableListView等等)。其实我已经写好了,先看一下效果图:

正常拉法:

                          

强迫症拉法:

                 

上面是ListView的,下面是GridView的

                  

再来看一下ExpandableListView的下拉刷新效果:

                              

可以看到,点击事件和长按事件都能正常触发而不会误触发在使用ExpandableListView的时候需要注意禁止展开时自动滚动,否则会出现bug。后面会提供demo源码下载,可以根据自己的需求去修改。

下面讲解PullToRefreshLayout的实现,在贴完整的源码之前先理解整个类的大概思路:

[java]  view plain copy
  1. public class PullToRefreshLayout extends RelativeLayout implements OnTouchListener  
  2. {  
  3.       
  4.     // 下拉的距离  
  5.     public float moveDeltaY = 0;  
  6.     // 是否可以下拉  
  7.     private boolean canPull = true;  
  8.       
  9.      
  10.     private void hideHead()  
  11.     {  
  12.         // 在这里开始异步隐藏下拉头,在松手的时候或这刷新完毕的时候隐藏  
  13.     }  
  14.   
  15.       
  16.     public void refreshFinish(int refreshResult)  
  17.     {  
  18.         // 完成刷新操作,显示刷新结果  
  19.     }  
  20.   
  21.     private void changeState(int to)  
  22.     {  
  23.         // 改变当前所处的状态,有四个状态:下拉刷新、释放刷新、正在刷新、刷新完成  
  24.     }  
  25.   
  26.     /* 
  27.      * (非 Javadoc)由父控件决定是否分发事件,防止事件冲突 
  28.      *  
  29.      * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent) 
  30.      */  
  31.     @Override  
  32.     public boolean dispatchTouchEvent(MotionEvent ev)  
  33.     {  
  34.         switch (ev.getActionMasked())  
  35.         {  
  36.         case MotionEvent.ACTION_DOWN:  
  37.             /*手指按下的时候,无法判断是否将要下拉,所以这时候break让父类把down事件分发给子View 
  38.             记录按下的坐标*/  
  39.             break;  
  40.         case MotionEvent.ACTION_MOVE:  
  41.             /*如果往上滑动且moveDetaY==0则说明不在下拉,break继续将move事件分发给子View 
  42.             如果往下拉,则计算下拉的距离moveDeltaY,根据moveDeltaY重新Layout子控件。但是 
  43.             由于down事件传到了子View,如果不清除子View的事件,会导致子View误触发长按事件和点击事件。所以在这里清除子View的事件回调。 
  44.             下拉超过一定的距离时,改变当前状态*/  
  45.             break;  
  46.         case MotionEvent.ACTION_UP:  
  47.             //根据当前状态执行刷新操作或者hideHead  
  48.         default:  
  49.             break;  
  50.         }  
  51.         // 事件分发交给父类  
  52.         return super.dispatchTouchEvent(ev);  
  53.     }  
  54.   
  55.       
  56.   
  57.     /* 
  58.      * (非 Javadoc)绘制阴影效果,颜色值可以修改 
  59.      *  
  60.      * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas) 
  61.      */  
  62.     @Override  
  63.     protected void dispatchDraw(Canvas canvas)  
  64.     {  
  65.         //在这里用一个渐变绘制分界线阴影  
  66.     }  
  67.   
  68.       
  69.   
  70.     @Override  
  71.     protected void onLayout(boolean changed, int l, int t, int r, int b)  
  72.     {  
  73.         //这个方法就是重新Layout子View了,根据moveDeltaY来定位子View的位置  
  74.     }  
  75.   
  76.       
  77.     @Override  
  78.     public boolean onTouch(View v, MotionEvent event)  
  79.     {  
  80.         //这个是OnTouchListener的方法,只判断AbsListView的状态来决定是否canPull,除此之外不做其他处理  
  81.     }  
  82. }  
可以看到,这里复写了ViewGroup的dispatchTouchEvent,这样就可以掌控事件的分发,如果不了解这个方法可以看一下这篇Android事件分发、View事件Listener全解析。之所以要控制事件分发是因为我们不可能知道手指down在AbsListView上之后将往上滑还是往下拉,所以down事件会分发给AbsListView的,但是在move的时候就需要看情况了,因为我们不想在下拉的同时AbsListView也在滑动,所以在下拉的时候不分发move事件,但这样问题又来了,前面AbsListView已经接收了down事件,如果这时候不分发move事件给它,它会触发长按事件或者点击事件,所以在这里还需要清除AbsListView消息列表中的callback。

onLayout用于重新布置下拉头和AbsListView的位置的,这个不难理解。

理解了大概思路之后,看一下PullToRefreshLayout完整的源码吧~

[java]  view plain copy
  1. package com.jingchen.pulltorefresh;  
  2.   
  3. import java.lang.reflect.Field;  
  4. import java.util.Timer;  
  5. import java.util.TimerTask;  
  6.   
  7. import android.content.Context;  
  8. import android.graphics.Canvas;  
  9. import android.graphics.LinearGradient;  
  10. import android.graphics.Paint;  
  11. import android.graphics.Paint.Style;  
  12. import android.graphics.RectF;  
  13. import android.graphics.Shader.TileMode;  
  14. import android.os.Handler;  
  15. import android.os.Message;  
  16. import android.util.AttributeSet;  
  17. import android.util.Log;  
  18. import android.view.MotionEvent;  
  19. import android.view.View;  
  20. import android.view.View.OnTouchListener;  
  21. import android.view.ViewGroup;  
  22. import android.view.animation.AnimationUtils;  
  23. import android.view.animation.LinearInterpolator;  
  24. import android.view.animation.RotateAnimation;  
  25. import android.widget.AbsListView;  
  26. import android.widget.RelativeLayout;  
  27. import android.widget.TextView;  
  28.   
  29. /** 
  30.  * 整个下拉刷新就这一个布局,用来管理两个子控件,其中一个是下拉头,另一个是包含内容的contentView(可以是AbsListView的任何子类) 
  31.  *  
  32.  * @author 陈靖 
  33.  */  
  34. public class PullToRefreshLayout extends RelativeLayout implements OnTouchListener  
  35. {  
  36.     public static final String TAG = "PullToRefreshLayout";  
  37.     // 下拉刷新  
  38.     public static final int PULL_TO_REFRESH = 0;  
  39.     // 释放刷新  
  40.     public static final int RELEASE_TO_REFRESH = 1;  
  41.     // 正在刷新  
  42.     public static final int REFRESHING = 2;  
  43.     // 刷新完毕  
  44.     public static final int DONE = 3;  
  45.     // 当前状态  
  46.     private int state = PULL_TO_REFRESH;  
  47.     // 刷新回调接口  
  48.     private OnRefreshListener mListener;  
  49.     // 刷新成功  
  50.     public static final int REFRESH_SUCCEED = 0;  
  51.     // 刷新失败  
  52.     public static final int REFRESH_FAIL = 1;  
  53.     // 下拉头  
  54.     private View headView;  
  55.     // 内容  
  56.     private View contentView;  
  57.     // 按下Y坐标,上一个事件点Y坐标  
  58.     private float downY, lastY;  
  59.     // 下拉的距离  
  60.     public float moveDeltaY = 0;  
  61.     // 释放刷新的距离  
  62.     private float refreshDist = 200;  
  63.     private Timer timer;  
  64.     private MyTimerTask mTask;  
  65.     // 回滚速度  
  66.     public float MOVE_SPEED = 8;  
  67.     // 第一次执行布局  
  68.     private boolean isLayout = false;  
  69.     // 是否可以下拉  
  70.     private boolean canPull = true;  
  71.     // 在刷新过程中滑动操作  
  72.     private boolean isTouchInRefreshing = false;  
  73.     // 手指滑动距离与下拉头的滑动距离比,中间会随正切函数变化  
  74.     private float radio = 2;  
  75.     // 下拉箭头的转180°动画  
  76.     private RotateAnimation rotateAnimation;  
  77.     // 均匀旋转动画  
  78.     private RotateAnimation refreshingAnimation;  
  79.     // 下拉的箭头  
  80.     private View pullView;  
  81.     // 正在刷新的图标  
  82.     private View refreshingView;  
  83.     // 刷新结果图标  
  84.     private View stateImageView;  
  85.     // 刷新结果:成功或失败  
  86.     private TextView stateTextView;  
  87.     /** 
  88.      * 执行自动回滚的handler 
  89.      */  
  90.     Handler updateHandler = new Handler()  
  91.     {  
  92.   
  93.         @Override  
  94.         public void handleMessage(Message msg)  
  95.         {  
  96.             // 回弹速度随下拉距离moveDeltaY增大而增大  
  97.             MOVE_SPEED = (float) (8 + 5 * Math.tan(Math.PI / 2 / getMeasuredHeight() * moveDeltaY));  
  98.             if (state == REFRESHING && moveDeltaY <= refreshDist && !isTouchInRefreshing)  
  99.             {  
  100.                 // 正在刷新,且没有往上推的话则悬停,显示"正在刷新..."  
  101.                 moveDeltaY = refreshDist;  
  102.                 mTask.cancel();  
  103.             }  
  104.             if (canPull)  
  105.                 moveDeltaY -= MOVE_SPEED;  
  106.             if (moveDeltaY <= 0)  
  107.             {  
  108.                 // 已完成回弹  
  109.                 moveDeltaY = 0;  
  110.                 pullView.clearAnimation();  
  111.                 // 隐藏下拉头时有可能还在刷新,只有当前状态不是正在刷新时才改变状态  
  112.                 if (state != REFRESHING)  
  113.                     changeState(PULL_TO_REFRESH);  
  114.                 mTask.cancel();  
  115.             }  
  116.             // 刷新布局,会自动调用onLayout  
  117.             requestLayout();  
  118.         }  
  119.   
  120.     };  
  121.   
  122.     public void setOnRefreshListener(OnRefreshListener listener)  
  123.     {  
  124.         mListener = listener;  
  125.     }  
  126.   
  127.     public PullToRefreshLayout(Context context)  
  128.     {  
  129.         super(context);  
  130.         initView(context);  
  131.     }  
  132.   
  133.     public PullToRefreshLayout(Context context, AttributeSet attrs)  
  134.     {  
  135.         super(context, attrs);  
  136.         initView(context);  
  137.     }  
  138.   
  139.     public PullToRefreshLayout(Context context, AttributeSet attrs, int defStyle)  
  140.     {  
  141.         super(context, attrs, defStyle);  
  142.         initView(context);  
  143.     }  
  144.   
  145.     private void initView(Context context)  
  146.     {  
  147.         timer = new Timer();  
  148.         mTask = new MyTimerTask(updateHandler);  
  149.         rotateAnimation = (RotateAnimation) AnimationUtils.loadAnimation(context, R.anim.reverse_anim);  
  150.         refreshingAnimation = (RotateAnimation) AnimationUtils.loadAnimation(context, R.anim.rotating);  
  151.         // 添加匀速转动动画  
  152.         LinearInterpolator lir = new LinearInterpolator();  
  153.         rotateAnimation.setInterpolator(lir);  
  154.         refreshingAnimation.setInterpolator(lir);  
  155.     }  
  156.   
  157.     private void hideHead()  
  158.     {  
  159.         if (mTask != null)  
  160.         {  
  161.             mTask.cancel();  
  162.             mTask = null;  
  163.         }  
  164.         mTask = new MyTimerTask(updateHandler);  
  165.         timer.schedule(mTask, 05);  
  166.     }  
  167.   
  168.     /** 
  169.      * 完成刷新操作,显示刷新结果 
  170.      */  
  171.     public void refreshFinish(int refreshResult)  
  172.     {  
  173.         refreshingView.clearAnimation();  
  174.         refreshingView.setVisibility(View.GONE);  
  175.         switch (refreshResult)  
  176.         {  
  177.         case REFRESH_SUCCEED:  
  178.             // 刷新成功  
  179.             stateImageView.setVisibility(View.VISIBLE);  
  180.             stateTextView.setText(R.string.refresh_succeed);  
  181.             stateImageView.setBackgroundResource(R.drawable.refresh_succeed);  
  182.             break;  
  183.         case REFRESH_FAIL:  
  184.             // 刷新失败  
  185.             stateImageView.setVisibility(View.VISIBLE);  
  186.             stateTextView.setText(R.string.refresh_fail);  
  187.             stateImageView.setBackgroundResource(R.drawable.refresh_failed);  
  188.             break;  
  189.         default:  
  190.             break;  
  191.         }  
  192.         // 刷新结果停留1秒  
  193.         new Handler()  
  194.         {  
  195.             @Override  
  196.             public void handleMessage(Message msg)  
  197.             {  
  198.                 state = PULL_TO_REFRESH;  
  199.                 hideHead();  
  200.             }  
  201.         }.sendEmptyMessageDelayed(01000);  
  202.     }  
  203.   
  204.     private void changeState(int to)  
  205.     {  
  206.         state = to;  
  207.         switch (state)  
  208.         {  
  209.         case PULL_TO_REFRESH:  
  210.             // 下拉刷新  
  211.             stateImageView.setVisibility(View.GONE);  
  212.             stateTextView.setText(R.string.pull_to_refresh);  
  213.             pullView.clearAnimation();  
  214.             pullView.setVisibility(View.VISIBLE);  
  215.             break;  
  216.         case RELEASE_TO_REFRESH:  
  217.             // 释放刷新  
  218.             stateTextView.setText(R.string.release_to_refresh);  
  219.             pullView.startAnimation(rotateAnimation);  
  220.             break;  
  221.         case REFRESHING:  
  222.             // 正在刷新  
  223.             pullView.clearAnimation();  
  224.             refreshingView.setVisibility(View.VISIBLE);  
  225.             pullView.setVisibility(View.INVISIBLE);  
  226.             refreshingView.startAnimation(refreshingAnimation);  
  227.             stateTextView.setText(R.string.refreshing);  
  228.             break;  
  229.         default:  
  230.             break;  
  231.         }  
  232.     }  
  233.   
  234.     /* 
  235.      * (非 Javadoc)由父控件决定是否分发事件,防止事件冲突 
  236.      *  
  237.      * @see android.view.ViewGroup#dispatchTouchEvent(android.view.MotionEvent) 
  238.      */  
  239.     @Override  
  240.     public boolean dispatchTouchEvent(MotionEvent ev)  
  241.     {  
  242.         switch (ev.getActionMasked())  
  243.         {  
  244.         case MotionEvent.ACTION_DOWN:  
  245.             downY = ev.getY();  
  246.             lastY = downY;  
  247.             if (mTask != null)  
  248.             {  
  249.                 mTask.cancel();  
  250.             }  
  251.             /* 
  252.              * 触碰的地方位于下拉头布局,由于我们没有对下拉头做事件响应,这时候它会给咱返回一个false导致接下来的事件不再分发进来。 
  253.              * 所以我们不能交给父类分发,直接返回true 
  254.              */  
  255.             if (ev.getY() < moveDeltaY)  
  256.                 return true;  
  257.             break;  
  258.         case MotionEvent.ACTION_MOVE:  
  259.             // canPull这个值在底下onTouch中会根据ListView是否滑到顶部来改变,意思是是否可下拉  
  260.             if (canPull)  
  261.             {  
  262.                 // 对实际滑动距离做缩小,造成用力拉的感觉  
  263.                 moveDeltaY = moveDeltaY + (ev.getY() - lastY) / radio;  
  264.                 if (moveDeltaY < 0)  
  265.                     moveDeltaY = 0;  
  266.                 if (moveDeltaY > getMeasuredHeight())  
  267.                     moveDeltaY = getMeasuredHeight();  
  268.                 if (state == REFRESHING)  
  269.                 {  
  270.                     // 正在刷新的时候触摸移动  
  271.                     isTouchInRefreshing = true;  
  272.                 }  
  273.             }  
  274.             lastY = ev.getY();  
  275.             // 根据下拉距离改变比例  
  276.             radio = (float) (2 + 2 * Math.tan(Math.PI / 2 / getMeasuredHeight() * moveDeltaY));  
  277.             requestLayout();  
  278.             if (moveDeltaY <= refreshDist && state == RELEASE_TO_REFRESH)  
  279.             {  
  280.                 // 如果下拉距离没达到刷新的距离且当前状态是释放刷新,改变状态为下拉刷新  
  281.                 changeState(PULL_TO_REFRESH);  
  282.             }  
  283.             if (moveDeltaY >= refreshDist && state == PULL_TO_REFRESH)  
  284.             {  
  285.                 changeState(RELEASE_TO_REFRESH);  
  286.             }  
  287.             if (moveDeltaY > 8)  
  288.             {  
  289.                 // 防止下拉过程中误触发长按事件和点击事件  
  290.                 clearContentViewEvents();  
  291.             }  
  292.             if (moveDeltaY > 0)  
  293.             {  
  294.                 // 正在下拉,不让子控件捕获事件  
  295.                 return true;  
  296.             }  
  297.             break;  
  298.         case MotionEvent.ACTION_UP:  
  299.             if (moveDeltaY > refreshDist)  
  300.                 // 正在刷新时往下拉释放后下拉头不隐藏  
  301.                 isTouchInRefreshing = false;  
  302.             if (state == RELEASE_TO_REFRESH)  
  303.             {  
  304.                 changeState(REFRESHING);  
  305.                 // 刷新操作  
  306.                 if (mListener != null)  
  307.                     mListener.onRefresh();  
  308.             } else  
  309.             {  
  310.   
  311.             }  
  312.             hideHead();  
  313.         default:  
  314.             break;  
  315.         }  
  316.         // 事件分发交给父类  
  317.         return super.dispatchTouchEvent(ev);  
  318.     }  
  319.   
  320.     /** 
  321.      * 通过反射修改字段去掉长按事件和点击事件 
  322.      */  
  323.     private void clearContentViewEvents()  
  324.     {  
  325.         try  
  326.         {  
  327.             Field[] fields = AbsListView.class.getDeclaredFields();  
  328.             for (int i = 0; i < fields.length; i++)  
  329.                 if (fields[i].getName().equals("mPendingCheckForLongPress"))  
  330.                 {  
  331.                     // mPendingCheckForLongPress是AbsListView中的字段,通过反射获取并从消息列表删除,去掉长按事件  
  332.                     fields[i].setAccessible(true);  
  333.                     contentView.getHandler().removeCallbacks((Runnable) fields[i].get(contentView));  
  334.                 } else if (fields[i].getName().equals("mTouchMode"))  
  335.                 {  
  336.                     // TOUCH_MODE_REST = -1, 这个可以去除点击事件  
  337.                     fields[i].setAccessible(true);  
  338.                     fields[i].set(contentView, -1);  
  339.                 }  
  340.             // 去掉焦点  
  341.             ((AbsListView) contentView).getSelector().setState(new int[]  
  342.             { 0 });  
  343.         } catch (Exception e)  
  344.         {  
  345.             Log.d(TAG, "error : " + e.toString());  
  346.         }  
  347.     }  
  348.   
  349.     /* 
  350.      * (非 Javadoc)绘制阴影效果,颜色值可以修改 
  351.      *  
  352.      * @see android.view.ViewGroup#dispatchDraw(android.graphics.Canvas) 
  353.      */  
  354.     @Override  
  355.     protected void dispatchDraw(Canvas canvas)  
  356.     {  
  357.         super.dispatchDraw(canvas);  
  358.         if (moveDeltaY == 0)  
  359.             return;  
  360.         RectF rectF = new RectF(00, getMeasuredWidth(), moveDeltaY);  
  361.         Paint paint = new Paint();  
  362.         paint.setAntiAlias(true);  
  363.         // 阴影的高度为26  
  364.         LinearGradient linearGradient = new LinearGradient(0, moveDeltaY, 0, moveDeltaY - 260x660000000x00000000, TileMode.CLAMP);  
  365.         paint.setShader(linearGradient);  
  366.         paint.setStyle(Style.FILL);  
  367.         // 在moveDeltaY处往上变淡  
  368.         canvas.drawRect(rectF, paint);  
  369.     }  
  370.   
  371.     private void initView()  
  372.     {  
  373.         pullView = headView.findViewById(R.id.pull_icon);  
  374.         stateTextView = (TextView) headView.findViewById(R.id.state_tv);  
  375.         refreshingView = headView.findViewById(R.id.refreshing_icon);  
  376.         stateImageView = headView.findViewById(R.id.state_iv);  
  377.     }  
  378.   
  379.     @Override  
  380.     protected void onLayout(boolean changed, int l, int t, int r, int b)  
  381.     {  
  382.         if (!isLayout)  
  383.         {  
  384.             // 这里是第一次进来的时候做一些初始化  
  385.             headView = getChildAt(0);  
  386.             contentView = getChildAt(1);  
  387.             // 给AbsListView设置OnTouchListener  
  388.             contentView.setOnTouchListener(this);  
  389.             isLayout = true;  
  390.             initView();  
  391.             refreshDist = ((ViewGroup) headView).getChildAt(0).getMeasuredHeight();  
  392.         }  
  393.         if (canPull)  
  394.         {  
  395.             // 改变子控件的布局  
  396.             headView.layout(0, (int) moveDeltaY - headView.getMeasuredHeight(), headView.getMeasuredWidth(), (int) moveDeltaY);  
  397.             contentView.layout(0, (int) moveDeltaY, contentView.getMeasuredWidth(), (int) moveDeltaY + contentView.getMeasuredHeight());  
  398.         }else super.onLayout(changed, l, t, r, b);  
  399.     }  
  400.   
  401.     class MyTimerTask extends TimerTask  
  402.     {  
  403.         Handler handler;  
  404.   
  405.         public MyTimerTask(Handler handler)  
  406.         {  
  407.             this.handler = handler;  
  408.         }  
  409.   
  410.         @Override  
  411.         public void run()  
  412.         {  
  413.             handler.sendMessage(handler.obtainMessage());  
  414.         }  
  415.   
  416.     }  
  417.   
  418.     @Override  
  419.     public boolean onTouch(View v, MotionEvent event)  
  420.     {  
  421.         // 第一个item可见且滑动到顶部  
  422.         AbsListView alv = null;  
  423.         try  
  424.         {  
  425.             alv = (AbsListView) v;  
  426.         } catch (Exception e)  
  427.         {  
  428.             Log.d(TAG, e.getMessage());  
  429.             return false;  
  430.         }  
  431.         if (alv.getCount() == 0)  
  432.         {  
  433.             // 没有item的时候也可以下拉刷新  
  434.             canPull = true;  
  435.         } else if (alv.getFirstVisiblePosition() == 0 && alv.getChildAt(0).getTop() >= 0)  
  436.         {  
  437.             // 滑到AbsListView的顶部了  
  438.             canPull = true;  
  439.         } else  
  440.             canPull = false;  
  441.         return false;  
  442.     }  
  443. }  
代码中的注释已经写的很清楚了。

既然PullToRefreshLayout已经写好了,接下来就来使用这个Layout实现下拉刷新~

首先得写个OnRefreshListener接口来回调刷新操作:

[java]  view plain copy
  1. public interface OnRefreshListener {  
  2.     void onRefresh();  
  3. }  
就一个刷新操作的方法,待会儿让Activity实现这个接口就可以在Activity中执行刷新操作了。

看一下MainActivity的布局:

[html]  view plain copy
  1. <com.jingchen.pulltorefresh.PullToRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:id="@+id/refresh_view"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <include layout="@layout/refresh_head" />  
  7.       
  8.     <!-- 支持AbsListView的所有子类 -->  
  9.     <ListView  
  10.         android:id="@+id/content_view"  
  11.         android:layout_width="match_parent"  
  12.         android:layout_height="match_parent"  
  13.         android:background="@color/white"  
  14.         android:divider="@color/gray"  
  15.         android:dividerHeight="1dp" >  
  16.     </ListView>  
  17.   
  18. </com.jingchen.pulltorefresh.PullToRefreshLayout>  

PullToRefreshLayout只能包含两个子控件:refresh_head和content_view。

看一下refresh_head的布局:

[html]  view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/head_view"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="match_parent"  
  6.     android:background="@color/light_blue" >  
  7.   
  8.     <RelativeLayout  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:layout_alignParentBottom="true"  
  12.         android:paddingBottom="20dp"  
  13.         android:paddingTop="20dp" >  
  14.   
  15.         <RelativeLayout  
  16.             android:layout_width="match_parent"  
  17.             android:layout_height="wrap_content"  
  18.             android:layout_centerInParent="true" >  
  19.   
  20.             <ImageView  
  21.                 android:id="@+id/pull_icon"  
  22.                 android:layout_width="wrap_content"  
  23.                 android:layout_height="wrap_content"  
  24.                 android:layout_centerVertical="true"  
  25.                 android:layout_marginLeft="60dp"  
  26.                 android:background="@drawable/pull_icon_big" />  
  27.   
  28.             <ImageView  
  29.                 android:id="@+id/refreshing_icon"  
  30.                 android:layout_width="wrap_content"  
  31.                 android:layout_height="wrap_content"  
  32.                 android:layout_centerVertical="true"  
  33.                 android:layout_marginLeft="60dp"  
  34.                 android:background="@drawable/refreshing"  
  35.                 android:visibility="gone" />  
  36.   
  37.             <TextView  
  38.                 android:id="@+id/state_tv"  
  39.                 android:layout_width="wrap_content"  
  40.                 android:layout_height="wrap_content"  
  41.                 android:layout_centerInParent="true"  
  42.                 android:text="@string/pull_to_refresh"  
  43.                 android:textColor="@color/white"  
  44.                 android:textSize="16sp" />  
  45.   
  46.             <ImageView  
  47.                 android:id="@+id/state_iv"  
  48.                 android:layout_width="wrap_content"  
  49.                 android:layout_height="wrap_content"  
  50.                 android:layout_centerVertical="true"  
  51.                 android:layout_marginRight="8dp"  
  52.                 android:layout_toLeftOf="@id/state_tv"  
  53.                 android:visibility="gone" />  
  54.         </RelativeLayout>  
  55.     </RelativeLayout>  
  56.   
  57. </RelativeLayout>  


可以根据需要修改refresh_head的布局然后在PullToRefreshLayout中处理,但是相关View的id要和PullToRefreshLayout中用到的保持同步!

接下来是MainActivity的代码:

[java]  view plain copy
  1. package com.jingchen.pulltorefresh;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. import android.app.Activity;  
  7. import android.content.Context;  
  8. import android.os.Bundle;  
  9. import android.os.Handler;  
  10. import android.os.Message;  
  11. import android.support.v4.view.PagerAdapter;  
  12. import android.support.v4.view.ViewPager;  
  13. import android.support.v4.view.ViewPager.OnPageChangeListener;  
  14. import android.view.LayoutInflater;  
  15. import android.view.View;  
  16. import android.view.View.OnClickListener;  
  17. import android.view.ViewGroup;  
  18. import android.view.animation.AnimationUtils;  
  19. import android.view.animation.LinearInterpolator;  
  20. import android.view.animation.RotateAnimation;  
  21. import android.widget.AbsListView;  
  22. import android.widget.AdapterView;  
  23. import android.widget.AdapterView.OnItemClickListener;  
  24. import android.widget.AdapterView.OnItemLongClickListener;  
  25. import android.widget.BaseExpandableListAdapter;  
  26. import android.widget.ExpandableListView;  
  27. import android.widget.ExpandableListView.OnChildClickListener;  
  28. import android.widget.ExpandableListView.OnGroupClickListener;  
  29. import android.widget.ListView;  
  30. import android.widget.TextView;  
  31. import android.widget.Toast;  
  32.   
  33. /** 
  34.  * 除了下拉刷新,在contenview为ListView的情况下我给ListView增加了FooterView,实现点击加载更多 
  35.  *  
  36.  * @author 陈靖 
  37.  *  
  38.  */  
  39. public class MainActivity extends Activity implements OnRefreshListener, OnClickListener  
  40. {  
  41.     private AbsListView alv;  
  42.     private PullToRefreshLayout refreshLayout;  
  43.     private View loading;  
  44.     private RotateAnimation loadingAnimation;  
  45.     private TextView loadTextView;  
  46.     private MyAdapter adapter;  
  47.     private boolean isLoading = false;  
  48.   
  49.     @Override  
  50.     protected void onCreate(Bundle savedInstanceState)  
  51.     {  
  52.         super.onCreate(savedInstanceState);  
  53.         setContentView(R.layout.activity_main);  
  54.         init();  
  55.     }  
  56.   
  57.     private void init()  
  58.     {  
  59.         alv = (AbsListView) findViewById(R.id.content_view);  
  60.         refreshLayout = (PullToRefreshLayout) findViewById(R.id.refresh_view);  
  61.         refreshLayout.setOnRefreshListener(this);  
  62.         initListView();  
  63.   
  64.         loadingAnimation = (RotateAnimation) AnimationUtils.loadAnimation(this, R.anim.rotating);  
  65.         // 添加匀速转动动画  
  66.         LinearInterpolator lir = new LinearInterpolator();  
  67.         loadingAnimation.setInterpolator(lir);  
  68.     }  
  69.   
  70.     /** 
  71.      * ListView初始化方法 
  72.      */  
  73.     private void initListView()  
  74.     {  
  75.         List<String> items = new ArrayList<String>();  
  76.         for (int i = 0; i < 30; i++)  
  77.         {  
  78.             items.add("这里是item " + i);  
  79.         }  
  80.         // 添加head  
  81.         View headView = getLayoutInflater().inflate(R.layout.listview_head, null);  
  82.         ((ListView) alv).addHeaderView(headView, nullfalse);  
  83.         // 添加footer  
  84.         View footerView = getLayoutInflater().inflate(R.layout.load_more, null);  
  85.         loading = footerView.findViewById(R.id.loading_icon);  
  86.         loadTextView = (TextView) footerView.findViewById(R.id.loadmore_tv);  
  87.         ((ListView) alv).addFooterView(footerView, nullfalse);  
  88.         footerView.setOnClickListener(this);  
  89.         adapter = new MyAdapter(this, items);  
  90.         alv.setAdapter(adapter);  
  91.         alv.setOnItemLongClickListener(new OnItemLongClickListener()  
  92.         {  
  93.   
  94.             @Override  
  95.             public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)  
  96.             {  
  97.                 Toast.makeText(MainActivity.this"LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show();  
  98.                 return true;  
  99.             }  
  100.         });  
  101.         alv.setOnItemClickListener(new OnItemClickListener()  
  102.         {  
  103.   
  104.             @Override  
  105.             public void onItemClick(AdapterView<?> parent, View view, int position, long id)  
  106.             {  
  107.                 Toast.makeText(MainActivity.this" Click on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show();  
  108.             }  
  109.         });  
  110.     }  
  111.   
  112.     /** 
  113.      * GridView初始化方法 
  114.      */  
  115.     private void initGridView()  
  116.     {  
  117.         List<String> items = new ArrayList<String>();  
  118.         for (int i = 0; i < 30; i++)  
  119.         {  
  120.             items.add("这里是item " + i);  
  121.         }  
  122.         adapter = new MyAdapter(this, items);  
  123.         alv.setAdapter(adapter);  
  124.         alv.setOnItemLongClickListener(new OnItemLongClickListener()  
  125.         {  
  126.   
  127.             @Override  
  128.             public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)  
  129.             {  
  130.                 Toast.makeText(MainActivity.this"LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show();  
  131.                 return true;  
  132.             }  
  133.         });  
  134.         alv.setOnItemClickListener(new OnItemClickListener()  
  135.         {  
  136.   
  137.             @Override  
  138.             public void onItemClick(AdapterView<?> parent, View view, int position, long id)  
  139.             {  
  140.                 Toast.makeText(MainActivity.this" Click on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show();  
  141.             }  
  142.         });  
  143.     }  
  144.   
  145.     /** 
  146.      * ExpandableListView初始化方法 
  147.      */  
  148.     private void initExpandableListView()  
  149.     {  
  150.         ((ExpandableListView) alv).setAdapter(new ExpandableListAdapter(this));  
  151.         ((ExpandableListView) alv).setOnChildClickListener(new OnChildClickListener()  
  152.         {  
  153.   
  154.             @Override  
  155.             public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id)  
  156.             {  
  157.                 Toast.makeText(MainActivity.this" Click on group " + groupPosition + " item " + childPosition, Toast.LENGTH_SHORT).show();  
  158.                 return true;  
  159.             }  
  160.         });  
  161.         ((ExpandableListView) alv).setOnItemLongClickListener(new OnItemLongClickListener()  
  162.         {  
  163.   
  164.             @Override  
  165.             public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)  
  166.             {  
  167.                 Toast.makeText(MainActivity.this"LongClick on " + parent.getAdapter().getItemId(position), Toast.LENGTH_SHORT).show();  
  168.                 return true;  
  169.             }  
  170.         });  
  171.         ((ExpandableListView) alv).setOnGroupClickListener(new OnGroupClickListener()  
  172.         {  
  173.   
  174.             @Override  
  175.             public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id)  
  176.             {  
  177.                 if (parent.isGroupExpanded(groupPosition))  
  178.                 {  
  179.                     // 如果展开则关闭  
  180.                     parent.collapseGroup(groupPosition);  
  181.                 } else  
  182.                 {  
  183.                     // 如果关闭则打开,注意这里是手动打开不要默认滚动否则会有bug  
  184.                     parent.expandGroup(groupPosition);  
  185.                 }  
  186.                 return true;  
  187.             }  
  188.         });  
  189.     }  
  190.   
  191.     @Override  
  192.     public void onRefresh()  
  193.     {  
  194.         // 下拉刷新操作  
  195.         new Handler()  
  196.         {  
  197.             @Override  
  198.             public void handleMessage(Message msg)  
  199.             {  
  200.                 refreshLayout.refreshFinish(PullToRefreshLayout.REFRESH_SUCCEED);  
  201.             }  
  202.         }.sendEmptyMessageDelayed(05000);  
  203.     }  
  204.   
  205.     @Override  
  206.     public void onClick(View v)  
  207.     {  
  208.         switch (v.getId())  
  209.         {  
  210.         case R.id.loadmore_layout:  
  211.             if (!isLoading)  
  212.             {  
  213.                 loading.setVisibility(View.VISIBLE);  
  214.                 loading.startAnimation(loadingAnimation);  
  215.                 loadTextView.setText(R.string.loading);  
  216.                 isLoading = true;  
  217.             }  
  218.             break;  
  219.         default:  
  220.             break;  
  221.         }  
  222.   
  223.     }  
  224.   
  225.     class ExpandableListAdapter extends BaseExpandableListAdapter  
  226.     {  
  227.         private String[] groupsStrings;// = new String[] { "这里是group 0",  
  228.                                         // "这里是group 1", "这里是group 2" };  
  229.         private String[][] groupItems;  
  230.         private Context context;  
  231.   
  232.         public ExpandableListAdapter(Context context)  
  233.         {  
  234.             this.context = context;  
  235.             groupsStrings = new String[8];  
  236.             for (int i = 0; i < groupsStrings.length; i++)  
  237.             {  
  238.                 groupsStrings[i] = new String("这里是group " + i);  
  239.             }  
  240.             groupItems = new String[8][8];  
  241.             for (int i = 0; i < groupItems.length; i++)  
  242.                 for (int j = 0; j < groupItems[i].length; j++)  
  243.                 {  
  244.                     groupItems[i][j] = new String("这里是group " + i + "里的item " + j);  
  245.                 }  
  246.         }  
  247.   
  248.         @Override  
  249.         public int getGroupCount()  
  250.         {  
  251.             return groupsStrings.length;  
  252.         }  
  253.   
  254.         @Override  
  255.         public int getChildrenCount(int groupPosition)  
  256.         {  
  257.             return groupItems[groupPosition].length;  
  258.         }  
  259.   
  260.         @Override  
  261.         public Object getGroup(int groupPosition)  
  262.         {  
  263.             return groupsStrings[groupPosition];  
  264.         }  
  265.   
  266.         @Override  
  267.         public Object getChild(int groupPosition, int childPosition)  
  268.         {  
  269.             return groupItems[groupPosition][childPosition];  
  270.         }  
  271.   
  272.         @Override  
  273.         public long getGroupId(int groupPosition)  
  274.         {  
  275.             return groupPosition;  
  276.         }  
  277.   
  278.         @Override  
  279.         public long getChildId(int groupPosition, int childPosition)  
  280.         {  
  281.             return childPosition;  
  282.         }  
  283.   
  284.         @Override  
  285.         public boolean hasStableIds()  
  286.         {  
  287.             return true;  
  288.         }  
  289.   
  290.         @Override  
  291.         public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent)  
  292.         {  
  293.             View view = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null);  
  294.             TextView tv = (TextView) view.findViewById(R.id.name_tv);  
  295.             tv.setText(groupsStrings[groupPosition]);  
  296.             return view;  
  297.         }  
  298.   
  299.         @Override  
  300.         public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent)  
  301.         {  
  302.             View view = LayoutInflater.from(context).inflate(R.layout.list_item_layout, null);  
  303.             TextView tv = (TextView) view.findViewById(R.id.name_tv);  
  304.             tv.setText(groupItems[groupPosition][childPosition]);  
  305.             return view;  
  306.         }  
  307.   
  308.         @Override  
  309.         public boolean isChildSelectable(int groupPosition, int childPosition)  
  310.         {  
  311.             return true;  
  312.         }  
  313.   
  314.     }  
  315.   
  316. }  



在MainActivity中判断contentView是ListView的话给ListView添加了FooterView实现点击加载更多的功能。这只是一个演示PullToRefreshLayout使用的demo,可以参照一下修改。我已经在里面写了ListView,GridView和ExpandableListView的初始化方法,根据自己使用的是哪个来调用吧。那么这是ListView的下拉刷新和加载更多。如果我要GridView也有下拉刷新功能呢?那就把MainActivity的布局换成这样:

[html]  view plain copy
  1. <com.jingchen.pulltorefresh.PullToRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:id="@+id/refresh_view"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <include layout="@layout/refresh_head" />  
  7.     <!-- 支持AbsListView的所有子类 -->  
  8.     <GridView  
  9.         android:id="@+id/content_view"  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="match_parent"  
  12.         android:background="@color/white"  
  13.         android:columnWidth="90dp"  
  14.         android:gravity="center"  
  15.         android:horizontalSpacing="10dp"  
  16.         android:numColumns="auto_fit"  
  17.         android:stretchMode="columnWidth"  
  18.         android:verticalSpacing="15dp" >  
  19.     </GridView>  
  20.   
  21. </com.jingchen.pulltorefresh.PullToRefreshLayout>  

如果是ExpandableListView则把布局改成这样:

[html]  view plain copy
  1. <com.jingchen.pulltorefresh.PullToRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:id="@+id/refresh_view"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent" >  
  5.   
  6.     <include layout="@layout/refresh_head" />  
  7.     <!-- 支持AbsListView的所有子类 -->  
  8.     <ExpandableListView  
  9.         android:id="@+id/content_view"  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="match_parent"  
  12.         android:background="@color/white" >  
  13.     </ExpandableListView>  
  14.   
  15. </com.jingchen.pulltorefresh.PullToRefreshLayout>  

怎么样?很简单吧?简单易用,不用再去继承修改了~

点击下载源码
相关文章
|
16天前
|
搜索推荐 Android开发 开发者
安卓应用开发中的自定义控件实践
在安卓应用开发的广阔天地中,自定义控件如同璀璨的星辰,点亮了用户界面设计的夜空。它们不仅丰富了交互体验,更赋予了应用独特的个性。本文将带你领略自定义控件的魅力,从基础概念到实际应用,一步步揭示其背后的原理与技术细节。我们将通过一个简单的例子——打造一个具有独特动画效果的按钮,来展现自定义控件的强大功能和灵活性。无论你是初学者还是资深开发者,这篇文章都将为你打开一扇通往更高阶UI设计的大门。
|
2月前
|
缓存 前端开发 Android开发
Android实战之如何截取Activity或者Fragment的内容?
本文首发于公众号“AntDream”,介绍了如何在Android中截取Activity或Fragment的屏幕内容并保存为图片。包括截取整个Activity、特定控件或区域的方法,以及处理包含RecyclerView的复杂情况。
27 3
|
2月前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件基础与进阶
【10月更文挑战第5天】在Android应用开发中,自定义控件是提升用户体验和界面个性化的重要手段。本文将通过浅显易懂的语言和实例,引导你了解自定义控件的基本概念、创建流程以及高级应用技巧,帮助你在开发过程中更好地掌握自定义控件的使用和优化。
48 10
|
1月前
|
前端开发 Android开发 UED
安卓应用开发中的自定义控件实践
【10月更文挑战第35天】在移动应用开发中,自定义控件是提升用户体验、增强界面表现力的重要手段。本文将通过一个安卓自定义控件的创建过程,展示如何从零开始构建一个具有交互功能的自定义视图。我们将探索关键概念和步骤,包括继承View类、处理测量与布局、绘制以及事件处理。最终,我们将实现一个简单的圆形进度条,并分析其性能优化。
|
2月前
|
Android开发
Android实战之如何快速实现自动轮播图
本文介绍了在 Android 中使用 `ViewPager2` 和自定义适配器实现轮播图的方法,包括添加依赖、布局配置、创建适配器及实现自动轮播等步骤。
87 0
|
2月前
|
Android开发
Android开发显示头部Bar的需求解决方案--Android应用实战
Android开发显示头部Bar的需求解决方案--Android应用实战
24 0
|
28天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
15天前
|
搜索推荐 前端开发 API
探索安卓开发中的自定义视图:打造个性化用户界面
在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
40 19
|
28天前
|
IDE Java 开发工具
移动应用与系统:探索Android开发之旅
在这篇文章中,我们将深入探讨Android开发的各个方面,从基础知识到高级技术。我们将通过代码示例和案例分析,帮助读者更好地理解和掌握Android开发。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。让我们一起开启Android开发的旅程吧!
|
15天前
|
JSON Java API
探索安卓开发:打造你的首个天气应用
在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
41 14