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>
代码中注释不多,敬请谅解。

浙公网安备 33010602011771号