Android 高仿QQ的下拉刷新 ListView

学习笔记,转自http://blog.csdn.net/yutou58nian/article/details/17116073

最近工程需要使用下拉刷新,但是使用网上流传的各种版本均有或多或少的bug,或者效果不完美的地方。在使用QQ的时候,在消息列表界面的下拉刷新,个人感觉效果比较棒,就做了一个高仿版,效果与QQ的基本保持一致,有不足之处,欢迎指正。

 

源码下载地址:

http://download.csdn.net/detail/yutou58nian/6708851

又重构了一下代码,目前接近完美版!

仅有的一个小问题是,滑开头部,在下拉刷新和松手立即刷新状态来回切换时,滑动的弹性效果会变小。原因是在一直滑动的过程中,根据手指滑动的距离,一直setPadding时,会导致头部的paddingTop值跟实际显示在界面上的效果不一致,暂时还不知道怎么解决。

效果图如下:

        主要实现的特殊效果如下:

1.  下拉时缩减手指滑动距离,实现越拉越难的效果

 

2.  加载状态时,上推界面遮挡部分头部,头部自动收回

 

3.  加载状态时,界面依然可以下拉,松手自动收回,只显示头部

 

4.  加载完成后,有加载完成的状态,停留1秒之后自动收回

 

5.  ListView中数据长度没有充满屏幕时,可以下拉刷新

 

6.  ListView中没有数据时,可以下拉刷新

7.  所有的下拉,回弹均有动画效果

 

具体的实现思路跟网上的是一样的,就是给ListView添加HeadView,默认隐藏,通过监听OnTouch、OnScroll事件实现滑动时的各种效果。

只说说在实现时遇到的几个问题是怎么解决的:

1. 实现越拉越难的效果

在实现拉动越来越难的效果时,通过监听MotionEvent.ACTION_MOVE事件,取手指滑动距离的1/3,代码如下:

 1             if (currentHeaderState != REFRESH_BACED) {
 2                 mHeaderLinearLayout.setPadding(
 3                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
 4                                 + (int) ((mMoveY - mDownY) / 3),
 5                         mHeaderLinearLayout.getPaddingRight(),
 6                         mHeaderLinearLayout.getPaddingBottom());
 7             } else if (currentHeaderState == REFRESH_BACED
 8                     && headVisible == true) {
 9                 mHeaderLinearLayout.setPadding(
10                         mHeaderLinearLayout.getPaddingLeft(),
11                         (int) ((mMoveY - mDownY) / 3),
12                         mHeaderLinearLayout.getPaddingRight(),
13                         mHeaderLinearLayout.getPaddingBottom());
14             } else if (currentHeaderState == REFRESH_BACED
15                     && headVisible == false) {
16                 mHeaderLinearLayout.setPadding(
17                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
18                                 + (int) ((mMoveY - mDownY) / 3),
19                         mHeaderLinearLayout.getPaddingRight(),
20                         mHeaderLinearLayout.getPaddingBottom());
21 
22             }

下拉时一共有三种状态:A. 正常状态、B. 加载状态且头部隐藏、C.加载状态且头部显示。

 

其中A状态和B状态处理方式一样,直接从手指滑动开始计算,拉开滑动距离1/3的效果

C状态时,滑动时位置减去头部的高度,再开始滑动。

直接通过setPadding来实现滑开的效果,且滑动距离缩减1/3,如果手指不松开,来回滑动的话,会导致距离计算不正确,所以在设置回弹效果的时候,要做处理,不然会导致界面收回之后,List中的部分条目也被遮挡。

 

2. 实现回弹动画

这个因为ListView也是在主界面的线程中,所以可以使用Handler.postDelayed()来实现,每次缩减剩余高度的1/4,5毫秒刷新一次即可。

这里主要实现了两个动画,一个是头部隐藏动画,用于未达到刷新状态,和遮挡部分加载中的头部时的动画

另外一个是头部收回动画,用于下拉高度超出头部高度时,头部的松手回弹动画,代码如下:

 1 Runnable headHideAnimation = new Runnable() {
 2         public void run() {
 3             if (mHeaderLinearLayout.getBottom() > 0) {
 4                 int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout
 5                         .getPaddingTop() * 0.75f) - 1;
 6                 if (paddingTop < -mHeaderHeight) {
 7                     paddingTop = -mHeaderHeight;
 8                 }
 9                 mHeaderLinearLayout.setPadding(
10                         mHeaderLinearLayout.getPaddingLeft(), paddingTop,
11                         mHeaderLinearLayout.getPaddingRight(),
12                         mHeaderLinearLayout.getPaddingBottom());
13                 handler.postDelayed(headHideAnimation, 5);
14             } else {
15                 handler.removeCallbacks(headHideAnimation);
16                 mHeaderLinearLayout.setPadding(
17                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight,
18                         mHeaderLinearLayout.getPaddingRight(),
19                         mHeaderLinearLayout.getPaddingBottom());
20                 setSelection(1);
21                 headVisible = false;
22             }
23         }
24     };
25 
26     Runnable headBackAnimation = new Runnable() {
27         public void run() {
28             if (mHeaderLinearLayout.getPaddingTop() > 1) {
29                 mHeaderLinearLayout.setPadding(
30                         mHeaderLinearLayout.getPaddingLeft(),
31                         (int) (mHeaderLinearLayout.getPaddingTop() * 0.75f),
32                         mHeaderLinearLayout.getPaddingRight(),
33                         mHeaderLinearLayout.getPaddingBottom());
34                 handler.postDelayed(headBackAnimation, 5);
35             } else {
36                 headVisible = true;
37                 handler.removeCallbacks(headBackAnimation);
38             }
39         }
40     };
3. 实现ListView中数据没有充满屏幕时的下拉

 

当ListView中的数据没有充满屏幕的时候,滑动ListView没有内容的部分,监听不到onScrollStateChanged()事件,只能监听到onTouchEvent()、onScroll()这两个事件,如果不做特殊处理的话,会导致下拉之后状态不改变。

所以在onTouchEvent()中的Move事件中将界面的状态由静止改为滑动,即可解决问题。

全部的代码实现如下:

  1 public class RefreshListView extends ListView implements OnScrollListener {
  2 
  3     private float mDownY;
  4     private float mMoveY;
  5 
  6     private int mHeaderHeight;
  7 
  8     private int mCurrentScrollState;
  9 
 10     private final static int NONE_PULL_REFRESH = 0; // 正常状态
 11     private final static int ENTER_PULL_REFRESH = 1; // 进入下拉刷新状态
 12     private final static int OVER_PULL_REFRESH = 2; // 进入松手立即刷新状态
 13     // 加载状态下拉
 14     private final static int PUSH_REFRESHING = 3; // 加载状态中,隐藏部分正在加载
 15     private final static int OVER_PULL_REFRESHING = 4; // 加载状态中,滑开超出titlebar高度
 16     private int mPullRefreshState = 0; // 记录当前滑动状态
 17     // 松手后,界面状态
 18     private final static int REFRESH_BACED = 1; // 反弹结束,刷新中
 19     private final static int REFRESH_RETURN = 2; // 没有达到刷新界限,返回
 20     private final static int REFRESH_DONE = 3; // 加载数据结束
 21     private final static int REFRESH_ORIGINAL = 4; // 最初的状态
 22     public int currentHeaderState = -1; // 记录当前数据加载状态
 23 
 24     private boolean headVisible = false;
 25 
 26     private LinearLayout mHeaderLinearLayout = null;
 27     private TextView mHeaderTextView = null;
 28     private ImageView mHeaderPullDownImageView = null;
 29     private ImageView mHeaderProgressImage = null;
 30     private ImageView mHeaderRefreshOkImage = null;
 31     private RefreshListener mRefreshListener = null;
 32 
 33     private RotateAnimation animation;
 34     private RotateAnimation reverseAnimation;
 35     private boolean isBack = false;
 36     private Handler handler = new Handler();
 37 
 38     public void setOnRefreshListener(RefreshListener refreshListener) {
 39         this.mRefreshListener = refreshListener;
 40     }
 41 
 42     public RefreshListView(Context context) {
 43         this(context, null);
 44     }
 45 
 46     public RefreshListView(Context context, AttributeSet attrs) {
 47         super(context, attrs);
 48         init(context);
 49     }
 50 
 51     public void init(final Context context) {
 52         mHeaderLinearLayout = (LinearLayout) LayoutInflater.from(context)
 53                 .inflate(R.layout.refresh_list_header, null);
 54         addHeaderView(mHeaderLinearLayout);
 55         mHeaderTextView = (TextView) findViewById(R.id.refresh_list_header_text);
 56         mHeaderPullDownImageView = (ImageView) findViewById(R.id.refresh_list_header_pull_down);
 57         mHeaderProgressImage = (ImageView) findViewById(R.id.refresh_list_header_loading);
 58         mHeaderRefreshOkImage = (ImageView) findViewById(R.id.refresh_list_header_success);
 59 
 60         setSelection(1);
 61         setOnScrollListener(this);
 62         measureView(mHeaderLinearLayout);
 63         mHeaderHeight = mHeaderLinearLayout.getMeasuredHeight();
 64 
 65         mHeaderLinearLayout.setPadding(mHeaderLinearLayout.getPaddingLeft(),
 66                 -mHeaderHeight, mHeaderLinearLayout.getPaddingRight(),
 67                 mHeaderLinearLayout.getPaddingBottom());
 68 
 69         animation = new RotateAnimation(0, 180,
 70                 RotateAnimation.RELATIVE_TO_SELF, 0.5f,
 71                 RotateAnimation.RELATIVE_TO_SELF, 0.5f);
 72         animation.setInterpolator(new LinearInterpolator());
 73         animation.setDuration(150);
 74         animation.setFillAfter(true);// 箭头翻转动画
 75 
 76         reverseAnimation = new RotateAnimation(180, 0,
 77                 RotateAnimation.RELATIVE_TO_SELF, 0.5f,
 78                 RotateAnimation.RELATIVE_TO_SELF, 0.5f);
 79         reverseAnimation.setInterpolator(new LinearInterpolator());
 80         reverseAnimation.setDuration(150);
 81         reverseAnimation.setFillAfter(true);// 箭头反翻转动画
 82     }
 83 
 84     @Override
 85     public boolean onTouchEvent(MotionEvent ev) {
 86         switch (ev.getAction()) {
 87         case MotionEvent.ACTION_DOWN:
 88             mDownY = ev.getY();
 89             handler.removeCallbacks(headHideAnimation);
 90             handler.removeCallbacks(headBackAnimation);
 91             break;
 92         case MotionEvent.ACTION_MOVE:
 93             mMoveY = ev.getY();
 94             if (mCurrentScrollState == SCROLL_STATE_IDLE) {
 95                 mCurrentScrollState = SCROLL_STATE_TOUCH_SCROLL;
 96             }
 97             if (currentHeaderState != REFRESH_BACED) {
 98                 mHeaderLinearLayout.setPadding(
 99                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
100                                 + (int) ((mMoveY - mDownY) / 3),
101                         mHeaderLinearLayout.getPaddingRight(),
102                         mHeaderLinearLayout.getPaddingBottom());
103             } else if (currentHeaderState == REFRESH_BACED
104                     && headVisible == true) {
105                 mHeaderLinearLayout.setPadding(
106                         mHeaderLinearLayout.getPaddingLeft(),
107                         (int) ((mMoveY - mDownY) / 3),
108                         mHeaderLinearLayout.getPaddingRight(),
109                         mHeaderLinearLayout.getPaddingBottom());
110             } else if (currentHeaderState == REFRESH_BACED
111                     && headVisible == false) {
112                 mHeaderLinearLayout.setPadding(
113                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight
114                                 + (int) ((mMoveY - mDownY) / 3),
115                         mHeaderLinearLayout.getPaddingRight(),
116                         mHeaderLinearLayout.getPaddingBottom());
117 
118             }
119 
120             break;
121         case MotionEvent.ACTION_UP:
122             if (mPullRefreshState == OVER_PULL_REFRESH) {
123                 currentHeaderState = REFRESH_BACED;
124                 handler.postDelayed(headBackAnimation, 5);
125                 refreshViewByState();
126             } else if (mPullRefreshState == ENTER_PULL_REFRESH) {
127                 currentHeaderState = REFRESH_RETURN;
128                 handler.postDelayed(headHideAnimation, 5);
129                 refreshViewByState();
130             } else if (mPullRefreshState == PUSH_REFRESHING) {
131                 handler.postDelayed(headHideAnimation, 5);
132             } else if (mPullRefreshState == OVER_PULL_REFRESHING) {
133                 handler.postDelayed(headBackAnimation, 5);
134             }
135 
136             break;
137         }
138         return super.onTouchEvent(ev);
139     }
140 
141     @Override
142     public void onScroll(AbsListView view, int firstVisibleItem,
143             int visibleItemCount, int totalItemCount) {
144         if (currentHeaderState != REFRESH_BACED) {
145             if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
146                     && firstVisibleItem == 0
147                     && (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
148                             .getBottom() < mHeaderHeight)) {
149 
150                 mPullRefreshState = ENTER_PULL_REFRESH;
151                 mHeaderTextView.setText(R.string.app_list_header_refresh_down);
152                 mHeaderPullDownImageView.setVisibility(View.VISIBLE);
153                 mHeaderRefreshOkImage.setVisibility(View.GONE);
154 
155                 if (isBack) {
156                     isBack = false;
157                     mHeaderPullDownImageView.clearAnimation();
158                     mHeaderPullDownImageView.startAnimation(reverseAnimation);
159                 }
160             } else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
161                     && firstVisibleItem == 0
162                     && (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
163                 isBack = true;
164 
165                 if (mPullRefreshState == ENTER_PULL_REFRESH
166                         || mPullRefreshState == NONE_PULL_REFRESH) {
167                     mPullRefreshState = OVER_PULL_REFRESH;
168                     mHeaderTextView.setText(R.string.app_list_header_refresh);
169                     mHeaderPullDownImageView.clearAnimation();
170                     mHeaderPullDownImageView.startAnimation(animation);
171                 }
172             } else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
173                     && firstVisibleItem != 0) {
174                 if (mPullRefreshState == ENTER_PULL_REFRESH) {
175                     mPullRefreshState = NONE_PULL_REFRESH;
176                 }
177             }
178         } else {
179             if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
180                     && firstVisibleItem == 0
181                     && (mHeaderLinearLayout.getBottom() >= 0 && mHeaderLinearLayout
182                             .getBottom() < mHeaderHeight)) {
183                 mPullRefreshState = PUSH_REFRESHING;
184             } else if (mCurrentScrollState == SCROLL_STATE_TOUCH_SCROLL
185                     && firstVisibleItem == 0
186                     && (mHeaderLinearLayout.getBottom() >= mHeaderHeight)) {
187                 mPullRefreshState = OVER_PULL_REFRESHING;
188             }
189         }
190 
191         if (mCurrentScrollState == SCROLL_STATE_FLING && firstVisibleItem == 0) {
192             setSelection(1);
193         }
194     }
195 
196     @Override
197     public void onScrollStateChanged(AbsListView view, int scrollState) {
198         mCurrentScrollState = scrollState;
199     }
200 
201     @Override
202     public void setAdapter(ListAdapter adapter) {
203         super.setAdapter(adapter);
204         setSelection(1);
205     }
206 
207     private void measureView(View child) {
208         ViewGroup.LayoutParams p = child.getLayoutParams();
209         if (p == null) {
210             p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
211                     ViewGroup.LayoutParams.WRAP_CONTENT);
212         }
213 
214         int childWidthSpec = ViewGroup.getChildMeasureSpec(0, 0 + 0, p.width);
215         int lpHeight = p.height;
216         int childHeightSpec;
217         if (lpHeight > 0) {
218             childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight,
219                     MeasureSpec.EXACTLY);
220         } else {
221             childHeightSpec = MeasureSpec.makeMeasureSpec(0,
222                     MeasureSpec.UNSPECIFIED);
223         }
224         child.measure(childWidthSpec, childHeightSpec);
225     }
226 
227     public void refreshViewByState() {
228         switch (currentHeaderState) {
229         case REFRESH_BACED:
230             mHeaderTextView.setText(R.string.app_list_loading);
231             mHeaderProgressImage.setVisibility(View.VISIBLE);
232             mHeaderPullDownImageView.clearAnimation();
233             mHeaderPullDownImageView.setVisibility(View.GONE);
234             mPullRefreshState = NONE_PULL_REFRESH;
235             isBack = false;
236             if (mRefreshListener != null) {
237                 mRefreshListener.refreshing();
238             }
239             break;
240         case REFRESH_RETURN:
241             mPullRefreshState = NONE_PULL_REFRESH;
242             currentHeaderState = REFRESH_ORIGINAL;
243             break;
244         case REFRESH_DONE:
245             mHeaderTextView.setText(R.string.app_list_refresh_done);
246             mHeaderProgressImage.setVisibility(View.INVISIBLE);
247             mHeaderRefreshOkImage.setVisibility(View.VISIBLE);
248             mPullRefreshState = NONE_PULL_REFRESH;
249             currentHeaderState = REFRESH_ORIGINAL;
250             mCurrentScrollState = SCROLL_STATE_IDLE;
251             handler.postDelayed(headHideAnimation, 700);
252             break;
253         default:
254             break;
255         }
256     }
257 
258     Runnable headHideAnimation = new Runnable() {
259         public void run() {
260             if (mHeaderLinearLayout.getBottom() > 0) {
261                 int paddingTop = (int) (-mHeaderHeight * 0.25f + mHeaderLinearLayout
262                         .getPaddingTop() * 0.75f) - 1;
263                 if (paddingTop < -mHeaderHeight) {
264                     paddingTop = -mHeaderHeight;
265                 }
266                 mHeaderLinearLayout.setPadding(
267                         mHeaderLinearLayout.getPaddingLeft(), paddingTop,
268                         mHeaderLinearLayout.getPaddingRight(),
269                         mHeaderLinearLayout.getPaddingBottom());
270                 handler.postDelayed(headHideAnimation, 5);
271             } else {
272                 handler.removeCallbacks(headHideAnimation);
273                 mHeaderLinearLayout.setPadding(
274                         mHeaderLinearLayout.getPaddingLeft(), -mHeaderHeight,
275                         mHeaderLinearLayout.getPaddingRight(),
276                         mHeaderLinearLayout.getPaddingBottom());
277                 setSelection(1);
278                 headVisible = false;
279             }
280         }
281     };
282 
283     Runnable headBackAnimation = new Runnable() {
284         public void run() {
285             if (mHeaderLinearLayout.getPaddingTop() > 1) {
286                 mHeaderLinearLayout.setPadding(
287                         mHeaderLinearLayout.getPaddingLeft(),
288                         (int) (mHeaderLinearLayout.getPaddingTop() * 0.75f),
289                         mHeaderLinearLayout.getPaddingRight(),
290                         mHeaderLinearLayout.getPaddingBottom());
291                 handler.postDelayed(headBackAnimation, 5);
292             } else {
293                 headVisible = true;
294                 handler.removeCallbacks(headBackAnimation);
295             }
296         }
297     };
298 
299     public interface RefreshListener {
300         // 正在下拉刷新
301         public void refreshing();
302     }
303 }

头部的布局文件如下:

 

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 3     android:layout_width="fill_parent"
 4     android:layout_height="wrap_content"
 5     android:background="@android:color/black"
 6     android:gravity="center"
 7     android:orientation="horizontal" >
 8 
 9     <RelativeLayout
10         android:layout_width="fill_parent"
11         android:layout_height="50dp" >
12 
13         <ImageView
14             android:id="@+id/refresh_list_header_loading"
15             android:layout_width="wrap_content"
16             android:layout_height="50dp"
17             android:layout_marginLeft="@dimen/refresh_title_margin_left"
18             android:contentDescription="@string/app_image_helper"
19             android:src="@drawable/refresh_loading"
20             android:visibility="gone" >
21         </ImageView>
22 
23         <ImageView
24             android:id="@+id/refresh_list_header_pull_down"
25             android:layout_width="wrap_content"
26             android:layout_height="50dp"
27             android:layout_marginLeft="@dimen/refresh_title_margin_left"
28             android:contentDescription="@string/app_image_helper"
29             android:src="@drawable/refresh_arrow" />
30 
31         <RelativeLayout
32             android:layout_width="wrap_content"
33             android:layout_height="wrap_content"
34             android:layout_centerInParent="true" >
35 
36             <ImageView
37                 android:id="@+id/refresh_list_header_success"
38                 android:layout_width="15dp"
39                 android:layout_height="15dp"
40                 android:layout_centerVertical="true"
41                 android:contentDescription="@string/app_image_helper"
42                 android:src="@drawable/header_refresh_success"
43                 android:visibility="gone" >
44             </ImageView>
45 
46             <TextView
47                 android:id="@+id/refresh_list_header_text"
48                 android:layout_width="wrap_content"
49                 android:layout_height="wrap_content"
50                 android:layout_centerVertical="true"
51                 android:layout_marginLeft="4dp"
52                 android:layout_toRightOf="@id/refresh_list_header_success"
53                 android:text="@string/app_list_header_refresh_down"
54                 android:textColor="@android:color/white"
55                 android:textSize="15sp" />
56         </RelativeLayout>
57     </RelativeLayout>
58 
59 </LinearLayout>

 

代码中注释不多,敬请谅解。

 

posted @ 2013-12-17 13:14  will_N  阅读(1036)  评论(0)    收藏  举报