1.简介

       无疑,在Android开发中,ListView是使用非常频繁的控件之一,ListView提供一个列表的容易,允许我们以列表的形式将数据展示到界面上,但是Google给我们提供的原生ListView的控件,虽然在功能上很强大,但是在用户体验和动态效果上,还是比较差劲的。为了改善用户体验,市面上纷纷出现了各种各样的自定义的ListView,他们功能强大,界面美观,使我们该需要学习的地方。其中,使用最频繁的功能无疑就是ListView的下拉刷新和上拉加载数据了,几乎在没一款内容型的App中都可以找到这种控件的身影,尤其是需要联网获取数据的模块,使用的就更为频繁了,so,我们很有必要了解下这种效果是怎么实现的。

2.开源组件PullToRefreshList介绍      


      既然Android和Java都是开源的,一些常见的功能或者效果就不难被找到。PullToRefresh就是一个典型的例子,PullToRefresh是老外写的一个开源ListView组件,这个项目在ListView的基础上扩展了ListView的功能,增强了Listview的用户体验,功能十分强大,而且很容易被集成到当前的项目中,你只需要调用简单的API,即可省去很多不必要的麻烦,非常棒。以上是项目在Github的链接,有兴趣的可以戳进去down下来,使用一下。这里不是我们的重点,不想废话了。


PullToRefresh的Github项目地址: ​https://github.com/chrisbanes/Android-PullToRefresh


3.自定义ListView——下拉刷新&上拉加载


     本博客的重点讲述一下自定义LisView,实现下拉刷新和上拉加载的功能,实现类似于开源项目PullToRefresh的效果。好,既然如此,先看看我实现后的效果图,再分析:



Android ListView 实现下拉刷新上拉加载_android

  


Android ListView 实现下拉刷新上拉加载_下拉刷新_02

   


Android ListView 实现下拉刷新上拉加载_android_03



        好,效果图如上所示,下面逐步讲解下实现的过程。首先,来观察一下,ListView上方的布局,我这里称其为“头布局”,这个所谓的头布局,大致功能是这样的,一个ImageView显示上下拉动方向的状态的,ImageView相同的位置隐藏了一个ProgressBar,用来在数据刷新时给个提示作用的。还有两个TextView,上面用来显示下拉刷新时提醒用户是如何操作的,例如“下拉刷新”“松开刷新”“正在刷新”,另一个是用来显示本次刷新的时间的。比较简单的布局,下面是XML代码:



1. <?xml version="1.0" encoding="utf-8"?>
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3. android:layout_width="match_parent"
4. android:layout_height="wrap_content"
5. android:orientation="horizontal" >
6.
7. <FrameLayout
8. android:layout_width="wrap_content"
9. android:layout_height="wrap_content"
10. android:layout_margin="10dip" >
11.
12. <ImageView
13. android:id="@+id/iv_listview_header_arrow"
14. android:layout_width="wrap_content"
15. android:layout_height="wrap_content"
16. android:layout_gravity="center"
17. android:minWidth="30dip"
18. android:src="@drawable/common_listview_headview_red_arrow" />
19.
20. <ProgressBar
21. android:id="@+id/pb_listview_header"
22. android:layout_width="wrap_content"
23. android:layout_height="wrap_content"
24. android:layout_gravity="center"
25. android:indeterminateDrawable="@drawable/common_progressbar"
26. android:visibility="gone" />
27. </FrameLayout>
28.
29. <LinearLayout
30. android:layout_width="fill_parent"
31. android:layout_height="wrap_content"
32. android:layout_gravity="center_vertical"
33. android:gravity="center_horizontal"
34. android:orientation="vertical" >
35.
36. <TextView
37. android:id="@+id/tv_listview_header_state"
38. android:layout_width="wrap_content"
39. android:layout_height="wrap_content"
40. android:text="下拉刷新"
41. android:textColor="#FF0000"
42. android:textSize="18sp" />
43.
44. <TextView
45. android:id="@+id/tv_listview_header_last_update_time"
46. android:layout_width="wrap_content"
47. android:layout_height="wrap_content"
48. android:layout_marginTop="5dip"
49. android:text="最后刷新时间: 2014-10-10 12:56:12"
50. android:textColor="@android:color/white"
51. android:textSize="14sp" />
52. </LinearLayout>
53.
54. </LinearLayout>


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >

<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dip" >

<ImageView
android:id="@+id/iv_listview_header_arrow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="30dip"
android:src="@drawable/common_listview_headview_red_arrow" />

<ProgressBar
android:id="@+id/pb_listview_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/common_progressbar"
android:visibility="gone" />
</FrameLayout>

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_horizontal"
android:orientation="vertical" >

<TextView
android:id="@+id/tv_listview_header_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下拉刷新"
android:textColor="#FF0000"
android:textSize="18sp" />

<TextView
android:id="@+id/tv_listview_header_last_update_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dip"
android:text="最后刷新时间: 2014-10-10 12:56:12"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>

</LinearLayout>



1. <?xml version="1.0" encoding="utf-8"?>
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3. android:layout_width="match_parent"
4. android:layout_height="wrap_content"
5. android:orientation="vertical" >
6.
7. <LinearLayout
8. android:layout_width="wrap_content"
9. android:layout_height="wrap_content"
10. android:layout_gravity="center_horizontal"
11. android:layout_margin="10dip"
12. android:gravity="center_vertical"
13. android:orientation="horizontal" >
14.
15. <ProgressBar
16. android:layout_width="wrap_content"
17. android:layout_height="wrap_content"
18. android:layout_gravity="center"
19. android:indeterminateDrawable="@drawable/common_progressbar" />
20.
21. <TextView
22. android:layout_width="wrap_content"
23. android:layout_height="wrap_content"
24. android:layout_marginLeft="10dip"
25. android:text="加载更多..."
26. android:textColor="#FF0000"
27. android:textSize="18sp" />
28. </LinearLayout>
29.
30. </LinearLayout>


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="10dip"
android:gravity="center_vertical"
android:orientation="horizontal" >

<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminateDrawable="@drawable/common_progressbar" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dip"
android:text="加载更多..."
android:textColor="#FF0000"
android:textSize="18sp" />
</LinearLayout>

</LinearLayout>

此外,两个布局都用到一个ProgressBar的背景,其XML如下:


1. <?xml version="1.0" encoding="utf-8"?>
2. <rotate xmlns:android="http://schemas.android.com/apk/res/android"
3. android:fromDegrees="0"
4. android:pivotX="50%"
5. android:pivotY="50%"
6. android:toDegrees="360" >
7.
8. <shape
9. android:innerRadiusRatio="3"
10. android:shape="ring"
11. android:useLevel="false" >
12. <gradient
13. android:centerColor="#FF6666"
14. android:endColor="#FF0000"
15. android:startColor="#FFFFFF"
16. android:type="sweep" />
17. </shape>
18.
19. </rotate>

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" >

<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:useLevel="false" >
<gradient
android:centerColor="#FF6666"
android:endColor="#FF0000"
android:startColor="#FFFFFF"
android:type="sweep" />
</shape>

</rotate>
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" >

<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:useLevel="false" >
<gradient
android:centerColor="#FF6666"
android:endColor="#FF0000"
android:startColor="#FFFFFF"
android:type="sweep" />
</shape>

</rotate>

        ListView的头布局和脚布局已经做好了,那么接下来该怎么集成到ListView上去呢?首先我们来看看ListView的部分源代码,很轻松能找到这两个方法: addHeaderView(View v)addFooterView(View v),通过字面的意思就可以理解,这两个方法分别是向ListView顶部添加一个View和向ListView的底部添加View的,有了这两个方法,那么上面的头布局和脚布局就很容易被添加到ListView上了,并且成为ListView的一体。


       其实,再看会发现,简单的使用这两个方法分别往ListView上添加头布局和脚布局是不合理,添加上去的头布局和脚布局会被显示出来,并没有被隐藏掉,为了实现下拉和上拉时能够将头布局和脚布局都“拉出来”并且还可以松开时,再次隐藏起来,我们可以使用View下的一个方法 setPadding(int left, int top, int right, int bottom),这个方法设置View的Padding属性,这里,我们不必管left、right、bottom,只要设置top的值为头布局或者脚布局的高度即可“隐藏”这两个布局,而且还可以在手指滑动屏幕的时候,动态的设置这个top的值,来实现头布局和脚布局的显示-隐藏-显示。


       还有一个非常重要的话题,就是这个top的值还设定为多少合适?上面说了,我们来头布局来说明一下,隐藏这个头布局需要将top值设置成top=- 头布局高度,那么这个头布局的高度怎么求得呢?很显然,使用 getHeight()是得不到头布局高度的,因为 getHeight()方法是先控件在屏幕上展示完毕后得到的高度,显然在这里,这个ListView还在构建中,并没有展示到屏幕上。所以注意了,我们先调用View下的 measure(int widthMeasureSpec, int heightMeasureSpec)方法,将widthMeasureSpec和heightMeasureSpec分别设置为0,这里的widthMeasureSpec和heightMeasureSpec并不是一个准备的值,而且指定一个规格或者标准让系统帮我们测量View的宽高,当我们指定widthMeasureSpec和heightMeasureSpec分别为0的时候,系统将不采用这个规格去测量,而是根据实际情况去测量。之后,我们可以调用View下的 getMeasuredHeight()方法获取真实的头布局的高度,然后设置top = - 头布局实际高度,实现隐藏头布局。


1. public class RefreshListView extends ListView implements
2.
3. private static final String TAG = "RefreshListView";
4. private int firstVisibleItemPosition; // 屏幕显示在第一个的item的索引
5. private int downY; // 按下时y轴的偏移量
6. private int headerViewHeight; // 头布局的高度
7. private View headerView; // 头布局的对象
8.
9. private final int DOWN_PULL_REFRESH = 0; // 下拉刷新状态
10. private final int RELEASE_REFRESH = 1; // 松开刷新
11. private final int REFRESHING = 2; // 正在刷新中
12. private int currentState = DOWN_PULL_REFRESH; // 头布局的状态: 默认为下拉刷新状态
13.
14. private Animation upAnimation; // 向上旋转的动画
15. private Animation downAnimation; // 向下旋转的动画
16.
17. private ImageView ivArrow; // 头布局的剪头
18. private ProgressBar mProgressBar; // 头布局的进度条
19. private TextView tvState; // 头布局的状态
20. private TextView tvLastUpdateTime; // 头布局的最后更新时间
21.
22. private
23. private boolean isScrollToBottom; // 是否滑动到底部
24. private View footerView; // 脚布局的对象
25. private int footerViewHeight; // 脚布局的高度
26. private boolean isLoadingMore = false; // 是否正在加载更多中
27.
28. public
29. super(context, attrs);
30. initHeaderView();
31. initFooterView();
32. this.setOnScrollListener(this);
33. }
34.
35. /**
36. * 初始化脚布局
37. */
38. private void
39. null);
40. 0, 0);
41. footerViewHeight = footerView.getMeasuredHeight();
42. 0, -footerViewHeight, 0, 0);
43. this.addFooterView(footerView);
44. }
45.
46. /**
47. * 初始化头布局
48. */
49. private void
50. null);
51. ivArrow = (ImageView) headerView
52. .findViewById(R.id.iv_listview_header_arrow);
53. mProgressBar = (ProgressBar) headerView
54. .findViewById(R.id.pb_listview_header);
55. tvState = (TextView) headerView
56. .findViewById(R.id.tv_listview_header_state);
57. tvLastUpdateTime = (TextView) headerView
58. .findViewById(R.id.tv_listview_header_last_update_time);
59.
60. // 设置最后刷新时间
61. "最后刷新时间: "
62.
63. 0, 0); // 系统会帮我们测量出headerView的高度
64. headerViewHeight = headerView.getMeasuredHeight();
65. 0, -headerViewHeight, 0, 0);
66. this.addHeaderView(headerView); // 向ListView的顶部添加一个view对象
67. initAnimation();
68. }
69.
70. /**
71. * 获得系统的最新时间
72. *
73. * @return
74. */
75. private
76. new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
77. return
78. }
79.
80. /**
81. * 初始化动画
82. */
83. private void
84. new
85. 0.5f, Animation.RELATIVE_TO_SELF,
86. 0.5f);
87. 500);
88. true); // 动画结束后, 停留在结束的位置上
89.
90. new
91. 0.5f, Animation.RELATIVE_TO_SELF,
92. 0.5f);
93. 500);
94. true); // 动画结束后, 停留在结束的位置上
95. }
96.
97. @Override
98. public boolean
99. switch
100. case
101. int) ev.getY();
102. break;
103. case
104. int moveY = (int) ev.getY();
105. // 移动中的y - 按下的y = 间距.
106. int diff = (moveY - downY) / 2;
107. // -头布局的高度 + 间距 = paddingTop
108. int
109. // 如果: -头布局的高度 > paddingTop的值 执行super.onTouchEvent(ev);
110. if (firstVisibleItemPosition == 0
111. && -headerViewHeight < paddingTop) {
112. if (paddingTop > 0 && currentState == DOWN_PULL_REFRESH) { // 完全显示了.
113. "松开刷新");
114. currentState = RELEASE_REFRESH;
115. refreshHeaderView();
116. else if (paddingTop < 0
117. // 没有显示完全
118. "下拉刷新");
119. currentState = DOWN_PULL_REFRESH;
120. refreshHeaderView();
121. }
122. // 下拉头布局
123. 0, paddingTop, 0, 0);
124. return true;
125. }
126. break;
127. case
128. // 判断当前的状态是松开刷新还是下拉刷新
129. if
130. "刷新数据.");
131. // 把头布局设置为完全显示状态
132. 0, 0, 0, 0);
133. // 进入到正在刷新中状态
134. currentState = REFRESHING;
135. refreshHeaderView();
136.
137. if (mOnRefershListener != null) {
138. // 调用使用者的监听方法
139. }
140. else if
141. // 隐藏头布局
142. 0, -headerViewHeight, 0, 0);
143. }
144. break;
145. default
146. break;
147. }
148. return super.onTouchEvent(ev);
149. }
150.
151. /**
152. * 根据currentState刷新头布局的状态
153. */
154. private void
155. switch
156. case DOWN_PULL_REFRESH : // 下拉刷新状态
157. "下拉刷新");
158. // 执行向下旋转
159. break;
160. case RELEASE_REFRESH : // 松开刷新状态
161. "松开刷新");
162. // 执行向上旋转
163. break;
164. case REFRESHING : // 正在刷新中状态
165. ivArrow.clearAnimation();
166. ivArrow.setVisibility(View.GONE);
167. mProgressBar.setVisibility(View.VISIBLE);
168. "正在刷新中...");
169. break;
170. default
171. break;
172. }
173. }
174.
175. /**
176. * 当滚动状态改变时回调
177. */
178. @Override
179. public void onScrollStateChanged(AbsListView view, int
180.
181. if
182. || scrollState == SCROLL_STATE_FLING) {
183. // 判断当前是否已经到了底部
184. if
185. true;
186. // 当前到底部
187. "加载更多数据");
188. 0, 0, 0, 0);
189. this.setSelection(this.getCount());
190.
191. if (mOnRefershListener != null) {
192. mOnRefershListener.onLoadingMore();
193. }
194. }
195. }
196. }
197.
198. /**
199. * 当滚动时调用
200. *
201. * @param firstVisibleItem
202. * 当前屏幕显示在顶部的item的position
203. * @param visibleItemCount
204. * 当前屏幕显示了多少个条目的总数
205. * @param totalItemCount
206. * ListView的总条目的总数
207. */
208. @Override
209. public void onScroll(AbsListView view, int
210. int visibleItemCount, int
211. firstVisibleItemPosition = firstVisibleItem;
212.
213. if (getLastVisiblePosition() == (totalItemCount - 1)) {
214. true;
215. else
216. false;
217. }
218. }
219.
220. /**
221. * 设置刷新监听事件
222. *
223. * @param listener
224. */
225. public void
226. mOnRefershListener = listener;
227. }
228.
229. /**
230. * 隐藏头布局
231. */
232. public void
233. 0, -headerViewHeight, 0, 0);
234. ivArrow.setVisibility(View.VISIBLE);
235. mProgressBar.setVisibility(View.GONE);
236. "下拉刷新");
237. "最后刷新时间: "
238. currentState = DOWN_PULL_REFRESH;
239. }
240.
241. /**
242. * 隐藏脚布局
243. */
244. public void
245. 0, -footerViewHeight, 0, 0);
246. false;
247. }
248. }

public class RefreshListView extends ListView implements OnScrollListener {

private static final String TAG = "RefreshListView";
private int firstVisibleItemPosition; // 屏幕显示在第一个的item的索引
private int downY; // 按下时y轴的偏移量
private int headerViewHeight; // 头布局的高度
private View headerView; // 头布局的对象

private final int DOWN_PULL_REFRESH = 0; // 下拉刷新状态
private final int RELEASE_REFRESH = 1; // 松开刷新
private final int REFRESHING = 2; // 正在刷新中
private int currentState = DOWN_PULL_REFRESH; // 头布局的状态: 默认为下拉刷新状态

private Animation upAnimation; // 向上旋转的动画
private Animation downAnimation; // 向下旋转的动画

private ImageView ivArrow; // 头布局的剪头
private ProgressBar mProgressBar; // 头布局的进度条
private TextView tvState; // 头布局的状态
private TextView tvLastUpdateTime; // 头布局的最后更新时间

private OnRefreshListener mOnRefershListener;
private boolean isScrollToBottom; // 是否滑动到底部
private View footerView; // 脚布局的对象
private int footerViewHeight; // 脚布局的高度
private boolean isLoadingMore = false; // 是否正在加载更多中

public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
initHeaderView();
initFooterView();
this.setOnScrollListener(this);
}

/**
* 初始化脚布局
*/
private void initFooterView() {
footerView = View.inflate(getContext(), R.layout.listview_footer, null);
footerView.measure(0, 0);
footerViewHeight = footerView.getMeasuredHeight();
footerView.setPadding(0, -footerViewHeight, 0, 0);
this.addFooterView(footerView);
}

/**
* 初始化头布局
*/
private void initHeaderView() {
headerView = View.inflate(getContext(), R.layout.listview_header, null);
ivArrow = (ImageView) headerView
.findViewById(R.id.iv_listview_header_arrow);
mProgressBar = (ProgressBar) headerView
.findViewById(R.id.pb_listview_header);
tvState = (TextView) headerView
.findViewById(R.id.tv_listview_header_state);
tvLastUpdateTime = (TextView) headerView
.findViewById(R.id.tv_listview_header_last_update_time);

// 设置最后刷新时间
tvLastUpdateTime.setText("最后刷新时间: " + getLastUpdateTime());

headerView.measure(0, 0); // 系统会帮我们测量出headerView的高度
headerViewHeight = headerView.getMeasuredHeight();
headerView.setPadding(0, -headerViewHeight, 0, 0);
this.addHeaderView(headerView); // 向ListView的顶部添加一个view对象
initAnimation();
}

/**
* 获得系统的最新时间
*
* @return
*/
private String getLastUpdateTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(System.currentTimeMillis());
}

/**
* 初始化动画
*/
private void initAnimation() {
upAnimation = new RotateAnimation(0f, -180f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
upAnimation.setDuration(500);
upAnimation.setFillAfter(true); // 动画结束后, 停留在结束的位置上

downAnimation = new RotateAnimation(-180f, -360f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
downAnimation.setDuration(500);
downAnimation.setFillAfter(true); // 动画结束后, 停留在结束的位置上
}

@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN :
downY = (int) ev.getY();
break;
case MotionEvent.ACTION_MOVE :
int moveY = (int) ev.getY();
// 移动中的y - 按下的y = 间距.
int diff = (moveY - downY) / 2;
// -头布局的高度 + 间距 = paddingTop
int paddingTop = -headerViewHeight + diff;
// 如果: -头布局的高度 > paddingTop的值 执行super.onTouchEvent(ev);
if (firstVisibleItemPosition == 0
&& -headerViewHeight < paddingTop) {
if (paddingTop > 0 && currentState == DOWN_PULL_REFRESH) { // 完全显示了.
Log.i(TAG, "松开刷新");
currentState = RELEASE_REFRESH;
refreshHeaderView();
} else if (paddingTop < 0
&& currentState == RELEASE_REFRESH) { // 没有显示完全
Log.i(TAG, "下拉刷新");
currentState = DOWN_PULL_REFRESH;
refreshHeaderView();
}
// 下拉头布局
headerView.setPadding(0, paddingTop, 0, 0);
return true;
}
break;
case MotionEvent.ACTION_UP :
// 判断当前的状态是松开刷新还是下拉刷新
if (currentState == RELEASE_REFRESH) {
Log.i(TAG, "刷新数据.");
// 把头布局设置为完全显示状态
headerView.setPadding(0, 0, 0, 0);
// 进入到正在刷新中状态
currentState = REFRESHING;
refreshHeaderView();

if (mOnRefershListener != null) {
mOnRefershListener.onDownPullRefresh(); // 调用使用者的监听方法
}
} else if (currentState == DOWN_PULL_REFRESH) {
// 隐藏头布局
headerView.setPadding(0, -headerViewHeight, 0, 0);
}
break;
default :
break;
}
return super.onTouchEvent(ev);
}

/**
* 根据currentState刷新头布局的状态
*/
private void refreshHeaderView() {
switch (currentState) {
case DOWN_PULL_REFRESH : // 下拉刷新状态
tvState.setText("下拉刷新");
ivArrow.startAnimation(downAnimation); // 执行向下旋转
break;
case RELEASE_REFRESH : // 松开刷新状态
tvState.setText("松开刷新");
ivArrow.startAnimation(upAnimation); // 执行向上旋转
break;
case REFRESHING : // 正在刷新中状态
ivArrow.clearAnimation();
ivArrow.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
tvState.setText("正在刷新中...");
break;
default :
break;
}
}

/**
* 当滚动状态改变时回调
*/
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {

if (scrollState == SCROLL_STATE_IDLE
|| scrollState == SCROLL_STATE_FLING) {
// 判断当前是否已经到了底部
if (isScrollToBottom && !isLoadingMore) {
isLoadingMore = true;
// 当前到底部
Log.i(TAG, "加载更多数据");
footerView.setPadding(0, 0, 0, 0);
this.setSelection(this.getCount());

if (mOnRefershListener != null) {
mOnRefershListener.onLoadingMore();
}
}
}
}

/**
* 当滚动时调用
*
* @param firstVisibleItem
* 当前屏幕显示在顶部的item的position
* @param visibleItemCount
* 当前屏幕显示了多少个条目的总数
* @param totalItemCount
* ListView的总条目的总数
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
firstVisibleItemPosition = firstVisibleItem;

if (getLastVisiblePosition() == (totalItemCount - 1)) {
isScrollToBottom = true;
} else {
isScrollToBottom = false;
}
}

/**
* 设置刷新监听事件
*
* @param listener
*/
public void setOnRefreshListener(OnRefreshListener listener) {
mOnRefershListener = listener;
}

/**
* 隐藏头布局
*/
public void hideHeaderView() {
headerView.setPadding(0, -headerViewHeight, 0, 0);
ivArrow.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
tvState.setText("下拉刷新");
tvLastUpdateTime.setText("最后刷新时间: " + getLastUpdateTime());
currentState = DOWN_PULL_REFRESH;
}

/**
* 隐藏脚布局
*/
public void hideFooterView() {
footerView.setPadding(0, -footerViewHeight, 0, 0);
isLoadingMore = false;
}
}


1,计算手指在屏幕中滑动的间距。


      移动中的Y轴坐标 - 按下的Y轴坐标 = 间距


2,计算头布局距离顶部的距离


      -头布局的高度 + 间距 = paddingTop


3,如果: -头布局的高度 > paddingTop的值 执行super.onTouchEvent(ev);手指继续滑动,头布局状态为“松开刷新”;


      如果:-头布局的高度 < paddingTop的值,返回true,头布局状态为“下拉刷新”


4,手指抬起的时候,刷新


       问题2是如何知道,脚布局滑动到了ListView的底部,即最后一个可见的item。我们可以借助 android.widget.AbsListView.OnScrollListener接口下的两个方法 onScrollStateChanged(AbsListView view, int scrollState)onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount),现在ListView滚动时被调用的onScroll()方法中,判断当前的Item是不是最后一个Item,如果是的话就标记一个boolean的状态值,然后在监听ListView滑动状态onScrollStateChanged方法中,判断状态 scrollState == SCROLL_STATE_IDLE|| scrollState == SCROLL_STATE_FLING;若是,就说明ListView滑动到了底部了,这是需要上拉加载更多的数据。详细请参考上面贴出的代码,带上注释去看,也很好理解。


4.为ListView添加回调函数


       上面的效果是实现了,主要就讲解了UI的实现,接下来,我们了解一下逻辑方法的东西。显然,这个ListView还不能动态的“下来刷新”和“上拉加载”,为什么呢?很简单啊,暂时还没有向外界暴露出一个设置数据的方法。为了实现这个能够动态实施数据更新的功能,我们需要写一个回调提供给其它的类使用,首先看一下这个回调的接口:


1. public interface
2.
3. /**
4. * 下拉刷新
5. */
6. void
7.
8. /**
9. * 上拉加载更多
10. */
11. void
12. }


public interface OnRefreshListener {

/**
* 下拉刷新
*/
void onDownPullRefresh();

/**
* 上拉加载更多
*/
void onLoadingMore();
}

这个回调的接口定义了两个方法,分别是“下拉刷新”和“上拉加载”,然后还必须在ListView中暴露一个接口与外面的类链接,最好的方法就暴露公共方法,例如:


1. public void
2. mOnRefershListener = listener;
3. }

public void setOnRefreshListener(OnRefreshListener listener) {
mOnRefershListener = listener;
}

这样这个接口的对象就在ListView中建立了,我们只要拿着这个对象,就可以在相应的位置上调用该对象的“下拉刷新”“上拉加载”的方法了,不必在乎方法体是什么,因为具体实现的方式,具体的数据结构都是其他类中定义的,我们只要提供实现的方式即可。


5.使用这个自定义的ListView


       使用这个自定义的ListView特别简单的,这里不多说了,看代码:


1. public class MainActivity extends Activity implements
2.
3. private
4. private
5. private
6.
7. @Override
8. protected void
9. super.onCreate(savedInstanceState);
10. setContentView(R.layout.activity_main);
11.
12. rListView = (RefreshListView) findViewById(R.id.refreshlistview);
13. new
14. for (int i = 0; i < 25; i++) {
15. "这是一条ListView的数据"
16. }
17. new
18. rListView.setAdapter(adapter);
19. this);
20. }
21.
22. private class MyAdapter extends
23.
24. @Override
25. public int
26. // TODO Auto-generated method stub
27. return
28. }
29.
30. @Override
31. public Object getItem(int
32. // TODO Auto-generated method stub
33. return
34. }
35.
36. @Override
37. public long getItemId(int
38. // TODO Auto-generated method stub
39. return
40. }
41.
42. @Override
43. public View getView(int
44. // TODO Auto-generated method stub
45. new TextView(MainActivity.this);
46. textView.setText(textList.get(position));
47. textView.setTextColor(Color.WHITE);
48. 18.0f);
49. return
50. }
51.
52. }
53.
54. @Override
55. public void
56. new
57.
58. @Override
59. protected
60. 2000);
61. for (int i = 0; i < 2; i++) {
62. 0, "这是下拉刷新出来的数据"
63. }
64. return null;
65. }
66.
67. @Override
68. protected void
69. adapter.notifyDataSetChanged();
70. rListView.hideHeaderView();
71. }
72. new
73. }
74.
75. @Override
76. public void
77. new
78.
79. @Override
80. protected
81. 5000);
82.
83. "这是加载更多出来的数据1");
84. "这是加载更多出来的数据2");
85. "这是加载更多出来的数据3");
86. return null;
87. }
88.
89. @Override
90. protected void
91. adapter.notifyDataSetChanged();
92.
93. // 控制脚布局隐藏
94. rListView.hideFooterView();
95. }
96. new
97. }
98.
99. }

public class MainActivity extends Activity implements OnRefreshListener {

private List<String> textList;
private MyAdapter adapter;
private RefreshListView rListView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

rListView = (RefreshListView) findViewById(R.id.refreshlistview);
textList = new ArrayList<String>();
for (int i = 0; i < 25; i++) {
textList.add("这是一条ListView的数据" + i);
}
adapter = new MyAdapter();
rListView.setAdapter(adapter);
rListView.setOnRefreshListener(this);
}

private class MyAdapter extends BaseAdapter {

@Override
public int getCount() {
// TODO Auto-generated method stub
return textList.size();
}

@Override
public Object getItem(int position) {
// TODO Auto-generated method stub
return textList.get(position);
}

@Override
public long getItemId(int position) {
// TODO Auto-generated method stub
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
// TODO Auto-generated method stub
TextView textView = new TextView(MainActivity.this);
textView.setText(textList.get(position));
textView.setTextColor(Color.WHITE);
textView.setTextSize(18.0f);
return textView;
}

}

@Override
public void onDownPullRefresh() {
new AsyncTask<Void, Void, Void>() {

@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(2000);
for (int i = 0; i < 2; i++) {
textList.add(0, "这是下拉刷新出来的数据" + i);
}
return null;
}

@Override
protected void onPostExecute(Void result) {
adapter.notifyDataSetChanged();
rListView.hideHeaderView();
}
}.execute(new Void[]{});
}

@Override
public void onLoadingMore() {
new AsyncTask<Void, Void, Void>() {

@Override
protected Void doInBackground(Void... params) {
SystemClock.sleep(5000);

textList.add("这是加载更多出来的数据1");
textList.add("这是加载更多出来的数据2");
textList.add("这是加载更多出来的数据3");
return null;
}

@Override
protected void onPostExecute(Void result) {
adapter.notifyDataSetChanged();

// 控制脚布局隐藏
rListView.hideFooterView();
}
}.execute(new Void[]{});
}

}

我们模拟的是联网更新数据,所以必须要开启新的线程去获取数据,联网获取数据的方式有很多种,我这里使用的Android为我们提供好的AsyncTask轻量型的框架,关于这个框架,在下面有一些简单的介绍。


6.AsyncTask简单介绍


先来看看AsyncTask的定义:


1. public abstract class


public abstract class AsyncTask<Params, Progress, Result> {

三种泛型类型分别代表

Params :“启动任务执行的输入参数”,Progress:“后台任务执行的进度”,Result:“后台计算结果的类型”。在特定场合下,并不是所有类型都被使用,如果没有被使用,可以用java.lang.Void类型代替。

一个异步任务的执行一般包括以下几个步骤:

1.execute(Params... params),执行一个异步任务,需要我们在代码中调用此方法,触发异步任务的执行。

2.onPreExecute(),在execute(Params... params)被调用后立即执行,一般用来在执行后台任务前对UI做一些标记。

3.doInBackground(Params... params),在onPreExecute()完成后立即执行,用于执行较为费时的操作,此方法将接收输入参数和返回计算结果。在执行过程中可以调用publishProgress(Progress... values)来更新进度信息。

4.onProgressUpdate(Progress... values),在调用publishProgress(Progress... values)时,此方法被执行,直接将进度信息更新到UI组件上。

5.onPostExecute(Result result),当后台操作结束时,此方法将会被调用,计算结果将做为参数传递到此方法中,直接将结果显示到UI组件上。


下面是一个图解:



Android ListView 实现下拉刷新上拉加载_android_04





通过这个草图,我们可以分析出AsyncTask三个泛型参数的用处,1,第一个泛型参数Params就是execute()方法中的参数,这二者要保持一致,因为这个参数Params会直接传递给doInBackground(Params...params)方法中,作为这个方法的参数存在。2,第二个参数Progress代表任务执行的进度,通常设定为Void类型,不使用它。3,第三个参数Result代表的是doInBackground()方法的返回值类型,这个返回值类型决定该方法在子线程中获取的是什么类型的数据,并且获取到的数据将被传递给onPostExecute()方法中作为参数,就是程序执行的结果,在该方法中拿到这个结果在UI上实现数据更新。


       以上是对AsyncTask的简单介绍,没有深入,想要深入了解AsyncTask,请移步到以下这两篇博客中。


​​详解Android中AsyncTask的使用


​​Android AsyncTask完全解析,带你从源码的角度彻底理解




​​源码请在这里下载​​


 


前段时间项目中用到了下拉刷新功能,之前在网上也找到过类似的demo,但这些demo的质量参差不齐,用户体验也不好,接口设计也不行。最张没办法,终于忍不了了,自己就写了一个下拉刷新的框架,这个框架是一个通用的框架,效果和设计感觉都还不错,现在分享给各位看官。

致谢:

1. 感谢lk6233160同学提出的问题,旋转View时调用setRotation方法只能是在API Level11(3.0)以上才能用,这个问题的解决办法是给ImageView设置一个Matrix,把Matrix上面作用一个旋转矩阵,但是如果不是ImageView的话,可能实现起来比较麻烦,再次谢谢lk6233160同学。

2. 谢谢​​如毛毛风​​提出的问题,向下滑动后,再向上滑动到头,只能再松手后才能再次下拉。这个问题的回复请参考评论。



1. 关于下拉刷新


下拉刷新这种用户交互最早由twitter创始人洛伦•布里切特(Loren Brichter)发明, 有理论认为,下拉刷新是一种适用于按照从新到旧的时间顺序排列feeds的应用,在这种应用场景中看完旧的内容时,用户会很自然地下拉查找更新的内容,因此下拉刷新就显得非常合理。大家可以参考这篇文章: ​​有趣的下拉刷新​​,下面我贴出一个有趣的下拉刷新的案例。



Android ListView 实现下拉刷新上拉加载_下拉刷新_05



图一、有趣的下拉刷新案例(一)




Android ListView 实现下拉刷新上拉加载_下拉刷新_06



图一、有趣的下拉刷新案例(二)




2. 实现原理


上面这些例子,外观做得再好看,他的本质上都一样,那就是一个下拉刷新控件通常由以下几部分组成:


【1】Header


Header通常有下拉箭头,文字,进度条等元素,根据下拉的距离来改变它的状态,从而显示不同的样式


【2】Content


这部分是内容区域,网上有很多例子都是直接在ListView里面添加Header,但这就有局限性,因为好多情况下并不一定是用ListView来显示数据。我们把要显示内容的View放置在我们的一个容器中,如果你想实现一个用ListView显示数据的下拉刷新,你需要创建一个ListView旋转到我的容器中。我们处理这个容器的事件(down, move, up),如果向下拉,则把整个布局向下滑动,从而把header显示出来。


【3】Footer


Footer可以用来显示向上拉的箭头,自动加载更多的进度条等。



以上三部分总结的说来,就是如下图所示的这种布局结构:



Android ListView 实现下拉刷新上拉加载_下拉刷新_07


图三,下拉刷新的布局结构



关于上图,需要说明几点:


1、这个布局扩展于 LinearLayout,垂直排列


2、从上到下的顺序是:Header, Content, Footer


3、Content填充满父控件,通过设置top, bottom的padding来使Header和Footer不可见,也就是让它超出屏幕外


4、下拉时,调用scrollTo方法来将整个布局向下滑动,从而把Header显示出来,上拉正好与下拉相反。


5、派生类需要实现的是:将Content View填充到父容器中,比如,如果你要使用的话,那么你需要把ListView, ScrollView, WebView等添加到容器中。


6、上图中的红色区域就是屏的大小(严格来说,这里说屏幕大小并不准确,应该说成内容区域更加准确)



3. 具体实现


明白了实现原理与过程,我们尝试来具体实现,首先,为了以后更好地扩展,设计更加合理,我们把下拉刷新的功能抽象成一个接口:


1、IPullToRefresh<T extends View>


它具体的定义方法如下:


1. public interface IPullToRefresh<T extends
2. public void setPullRefreshEnabled(boolean
3. public void setPullLoadEnabled(boolean
4. public void setScrollLoadEnabled(boolean
5. public boolean
6. public boolean
7. public boolean
8. public void
9. public void
10. public void
11. public
12. public
13. public
14. public void
15. }

public interface IPullToRefresh<T extends View> {
public void setPullRefreshEnabled(boolean pullRefreshEnabled);
public void setPullLoadEnabled(boolean pullLoadEnabled);
public void setScrollLoadEnabled(boolean scrollLoadEnabled);
public boolean isPullRefreshEnabled();
public boolean isPullLoadEnabled();
public boolean isScrollLoadEnabled();
public void setOnRefreshListener(OnRefreshListener<T> refreshListener);
public void onPullDownRefreshComplete();
public void onPullUpRefreshComplete();
public T getRefreshableView();
public LoadingLayout getHeaderLoadingLayout();
public LoadingLayout getFooterLoadingLayout();
public void setLastUpdatedLabel(CharSequence label);
}
public interface IPullToRefresh<T extends View> {
public void setPullRefreshEnabled(boolean pullRefreshEnabled);
public void setPullLoadEnabled(boolean pullLoadEnabled);
public void setScrollLoadEnabled(boolean scrollLoadEnabled);
public boolean isPullRefreshEnabled();
public boolean isPullLoadEnabled();
public boolean isScrollLoadEnabled();
public void setOnRefreshListener(OnRefreshListener<T> refreshListener);
public void onPullDownRefreshComplete();
public void onPullUpRefreshComplete();
public T getRefreshableView();
public LoadingLayout getHeaderLoadingLayout();
public LoadingLayout getFooterLoadingLayout();
public void setLastUpdatedLabel(CharSequence label);
}

这个接口是一个泛型的,它接受View的派生类, 因为要放到我们的容器中的不就是一个View吗?


2、PullToRefreshBase<T extends View>


这个类实现了IPullToRefresh接口,它是从LinearLayout继承过来,作为下拉刷新的一个 抽象基类,如果你想实现ListView的下拉刷新,只需要扩展这个类,实现一些必要的方法就可以了。这个类的职责主要有以下几点:


  • 处理onInterceptTouchEvent()和onTouchEvent()中的事件:当内容的View(比如ListView)正如处于最顶部,此时再向下拉,我们必须截断事件,然后move事件就会把后续的事件传递到onTouchEvent()方法中,然后再在这个方法中,我们根据move的距离再进行scroll整个View。
  • 负责创建Header、Footer和Content View:在构造方法中调用方法去创建这三个部分的View,派生类可以重写这些方法,以提供不同式样的Header和Footer,它会调用createHeaderLoadingLayout和createFooterLoadingLayout方法来创建Header和Footer创建Content View的方法是一个抽象方法,必须让派生类来实现,返回一个非null的View,然后容器再把这个View添加到自己里面。
  • 设置各种状态:这里面有很多状态,如下拉、上拉、刷新、加载中、释放等,它会根据用户拉动的距离来更改状态,状态的改变,它也会把Header和Footer的状态改变,然后Header和Footer会根据状态去显示相应的界面式样。

3、PullToRefreshBase<T extends View>继承关系


这里我实现了三个下拉刷新的派生类,分别是ListView、ScrollView、WebView三个,它们的继承关系如下:



Android ListView 实现下拉刷新上拉加载_xml_08



图四、PullToRefreshBase类的继承关系



关于PullToRefreshBase类及其派和类,有几点需要说明:


  • 对于ListView,ScrollView,WebView这三种情况,他们是否滑动到最顶部或是最底部的实现是不一样的,所以,在PullToRefreshBase类中需要调用两个抽象方法来判断当前的位置是否在顶部或底部,而其派生类必须要实现这两个方法。比如对于ListView,它滑动到最顶部的条件就是第一个child完全可见并且first postion是0。这两个抽象方法是:


    1. /**
    2. * 判断刷新的View是否滑动到顶部
    3. *
    4. * @return true表示已经滑动到顶部,否则false
    5. */
    6. protected abstract boolean
    7.
    8. /**
    9. * 判断刷新的View是否滑动到底
    10. *
    11. * @return true表示已经滑动到底部,否则false
    12. */
    13. protected abstract boolean


    /**
    * 判断刷新的View是否滑动到顶部
    *
    * @return true表示已经滑动到顶部,否则false
    */
    protected abstract boolean isReadyForPullDown();

    /**
    * 判断刷新的View是否滑动到底
    *
    * @return true表示已经滑动到底部,否则false
    */
    protected abstract boolean isReadyForPullUp();
    • 创建可下拉刷新的View(也就是content view)的抽象方法是

    1. /**
    2. * 创建可以刷新的View
    3. *
    4. * @param context context
    5. * @param attrs 属性
    6. * @return View
    7. */
    8. protected abstract


    /**
    * 创建可以刷新的View
    *
    * @param context context
    * @param attrs 属性
    * @return View
    */
    protected abstract T createRefreshableView(Context context, AttributeSet attrs);

    4、LoadingLayout


    LoadingLayout是刷新Layout的一个抽象,它是一个抽象基类。Header和Footer都扩展于这个类。这类抽象类,提供了两个抽象方法:


    • getContentSize


    这个方法返回当前这个刷新Layout的大小,通常返回的是布局的高度,为了以后可以扩展为水平拉动,所以方法名字没有取成getLayoutHeight()之类的,这个返回值,将会作为松手后是否可以刷新的临界值,如果下拉的偏移值大于这个值,就认为可以刷新,否则不刷新,这个方法必须由派生类来实现。

    • setState


    这个方法用来设置当前刷新Layout的状态,PullToRefreshBase类会调用这个方法,当进入下拉,松手等动作时,都会调用这个方法,派生类里面只需要根据这些状态实现不同的界面显示,如下拉状态时,就显示出箭头,刷新状态时,就显示loading的图标。

    可能的状态值有: RESET, PULL_TO_REFRESH, RELEASE_TO_REFRESH, REFRESHING, NO_MORE_DATA



    LoadingLayout及其派生类的继承关系如下图所示:



    Android ListView 实现下拉刷新上拉加载_xml_09



    图五、LoadingLayout及其派生类的类图



    我们可以随意地制定自己的Header和Footer,我们也可以实现如图一和图二中显示的各种下拉刷新案例中的Header和Footer,只要重写上述两个方法getContentSize()和setState()就行了。HeaderLoadingLayout,它默认是显示箭头式样的布局,而RotateLoadingLayout则是显示一个旋转图标的式样。



    5、事件处理


    我们必须重写PullToRefreshBase类的两个事件相关的方法 onInterceptTouchEvent()和onTouchEvent()方法。由于ListView,ScrollView,WebView它们是放到PullToRefreshBase内部的,所在事件先是传递到PullToRefreshBase#onInterceptTouchEvent()方法中,所以我们应该在这个方法中去处理ACTION_MOVE事件,判断如果当前ListView,ScrollView,WebView是否在最顶部或最底部,如果是,则开始截断事件,一旦事件被截断,后续的事件就会传递到PullToRefreshBase#onInterceptTouchEvent()方法中,我们再在ACTION_MOVE事件中去移动整个布局,从而实现下拉或上拉动作。



    6、滚动布局(scrollTo)


    如图三的布局结构可知,默认情况下Header和Footer是放置在Content View的最上面和最下面,通过设置padding来让他跑到屏幕外面去了,如果我们将整个布局向下滚动(scrollTo)一定距离,那么Header就会被显示出来,基于这种情况,所以在我的实现中,最终我是调用 scrollTo来实现下拉动作的。



    总的说来,实现的重要的点就这些,具体的一些细节在实现在会碰到很多,可以参考代码。



    4. 如何使用


    使用下拉刷新的代码如下



      1. @Override
      2. public void
      3. super.onCreate(savedInstanceState);
      4.
      5. new PullToRefreshListView(this);
      6. setContentView(mPullListView);
      7.
      8. // 上拉加载不可用
      9. false);
      10. // 滚动到底自动加载可用
      11. true);
      12.
      13. mCurIndex = mLoadDataCount;
      14. new
      15. 0, mCurIndex));
      16. new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);
      17.
      18. // 得到实际的ListView
      19. mListView = mPullListView.getRefreshableView();
      20. // 绑定数据
      21. mListView.setAdapter(mAdapter);
      22. // 设置下拉刷新的listener
      23. new
      24. @Override
      25. public void
      26. true;
      27. new
      28. }
      29.
      30. @Override
      31. public void
      32. false;
      33. new
      34. }
      35. });
      36. setLastUpdateTime();
      37.
      38. // 自动刷新
      39. true, 500);
      40. }


      @Override
      public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      mPullListView = new PullToRefreshListView(this);
      setContentView(mPullListView);

      // 上拉加载不可用
      mPullListView.setPullLoadEnabled(false);
      // 滚动到底自动加载可用
      mPullListView.setScrollLoadEnabled(true);

      mCurIndex = mLoadDataCount;
      mListItems = new LinkedList<String>();
      mListItems.addAll(Arrays.asList(mStrings).subList(0, mCurIndex));
      mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mListItems);

      // 得到实际的ListView
      mListView = mPullListView.getRefreshableView();
      // 绑定数据
      mListView.setAdapter(mAdapter);
      // 设置下拉刷新的listener
      mPullListView.setOnRefreshListener(new OnRefreshListener<ListView>() {
      @Override
      public void onPullDownToRefresh(PullToRefreshBase<ListView> refreshView) {
      mIsStart = true;
      new GetDataTask().execute();
      }

      @Override
      public void onPullUpToRefresh(PullToRefreshBase<ListView> refreshView) {
      mIsStart = false;
      new GetDataTask().execute();
      }
      });
      setLastUpdateTime();

      // 自动刷新
      mPullListView.doPullRefreshing(true, 500);
      }

      这是初始化一个下拉刷新的布局,并且调用setContentView来设置到Activity中。

      在下拉刷新完成后,我们可以调用 onPullDownRefreshComplete()和onPullUpRefreshComplete()方法来停止刷新和加载



      5. 运行效果


      这里列出了demo的运行效果图。



      Android ListView 实现下拉刷新上拉加载_下拉刷新_10



      图六、ListView下拉刷新,注意Header和Footer的样式




      Android ListView 实现下拉刷新上拉加载_xml_11



      图七、WebView和ScrollView的下拉刷新效果图




      6. 源码下载


      实现这个下拉刷新的框架,并不是我的原创,我也是参考了很多开源的,把我认为比较好的东西借鉴过来,从而形成我的东西,我主要是参考了下面这个demo:


      ​https://github.com/chrisbanes/Android-PullToRefresh​​ 这个demo写得不错,不过他这个太复杂了,我们都知道,一旦复杂了,万一我们要添加一些需要,自然也要费劲一些,我其实就是把他的简化再简化,以满足我们自己的需要。


      谢谢!!!




      7. Bug修复




      已知bug修复情况如下,发现了代码bug的看官也可以给我反馈,谢谢~~~



      1,对于ListView的下拉刷新,当启用滚动到底自动加载时,如果footer由隐藏变为显示时,出现显示异常的情况


      这个问题已经修复了,修正的代码如下:


      • PullToRefreshListView#setScrollLoadEnabled方法,修正后的代码如下:


        1. @Override
        2. public void setScrollLoadEnabled(boolean
        3. if
        4. return;
        5. }
        6.
        7. super.setScrollLoadEnabled(scrollLoadEnabled);
        8.
        9. if
        10. // 设置Footer
        11. if (null
        12. new
        13. null, false);
        14. }
        15.
        16. true);
        17. else
        18. if (null
        19. false);
        20. }
        21. }
        22. }


        @Override
        public void setScrollLoadEnabled(boolean scrollLoadEnabled) {
        if (isScrollLoadEnabled() == scrollLoadEnabled) {
        return;
        }

        super.setScrollLoadEnabled(scrollLoadEnabled);

        if (scrollLoadEnabled) {
        // 设置Footer
        if (null == mLoadMoreFooterLayout) {
        mLoadMoreFooterLayout = new FooterLoadingLayout(getContext());
        mListView.addFooterView(mLoadMoreFooterLayout, null, false);
        }

        mLoadMoreFooterLayout.show(true);
        } else {
        if (null != mLoadMoreFooterLayout) {
        mLoadMoreFooterLayout.show(false);
        }
        }
        }
        • LoadingLayout#show方法,修正后的代码如下:



          1. /**
          2. * 显示或隐藏这个布局
          3. *
          4. * @param show flag
          5. */
          6. public void show(boolean
          7. // If is showing, do nothing.
          8. if
          9. return;
          10. }
          11.
          12. ViewGroup.LayoutParams params = mContainer.getLayoutParams();
          13. if (null
          14. if
          15. params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
          16. else
          17. 0;
          18. }
          19.
          20. requestLayout();
          21. setVisibility(show ? View.VISIBLE : View.INVISIBLE);
          22. }
          23. }


          /**
          * 显示或隐藏这个布局
          *
          * @param show flag
          */
          public void show(boolean show) {
          // If is showing, do nothing.
          if (show == (View.VISIBLE == getVisibility())) {
          return;
          }

          ViewGroup.LayoutParams params = mContainer.getLayoutParams();
          if (null != params) {
          if (show) {
          params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
          } else {
          params.height = 0;
          }

          requestLayout();
          setVisibility(show ? View.VISIBLE : View.INVISIBLE);
          }
          }

          在更改LayoutParameter后,调用requestLayout()方法。

          • 图片旋转兼容2.x系统

          我之前想的是这个只需要兼容3.x以上的系统,但发现有很多网友在使用过程中遇到过兼容性问题,这次抽空将这个兼容性一并实现了。


                 onPull的修改如下:


          1. @Override
          2. public void onPull(float
          3. if (null
          4. new
          5. }
          6.
          7. float angle = scale * 180f; // SUPPRESS CHECKSTYLE
          8. mRotationHelper.setRotation(angle);
          9. }

          @Override
          public void onPull(float scale) {
          if (null == mRotationHelper) {
          mRotationHelper = new ImageViewRotationHelper(mArrowImageView);
          }

          float angle = scale * 180f; // SUPPRESS CHECKSTYLE
          mRotationHelper.setRotation(angle);
          }



          ImageViewRotationHelper主要的作用就是实现了ImageView的旋转功能,内部作了版本的区分,实现代码如下:



          1. /**
          2. * The image view rotation helper
          3. *
          4. * @author lihong06
          5. * @since 2014-5-2
          6. */
          7. static class
          8. /** The imageview */
          9. private final
          10. /** The matrix */
          11. private
          12. /** Pivot X */
          13. private float
          14. /** Pivot Y */
          15. private float
          16.
          17. /**
          18. * The constructor method.
          19. *
          20. * @param imageView the image view
          21. */
          22. public
          23. mImageView = imageView;
          24. }
          25.
          26. /**
          27. * Sets the degrees that the view is rotated around the pivot point. Increasing values
          28. * result in clockwise rotation.
          29. *
          30. * @param rotation The degrees of rotation.
          31. *
          32. * @see #getRotation()
          33. * @see #getPivotX()
          34. * @see #getPivotY()
          35. * @see #setRotationX(float)
          36. * @see #setRotationY(float)
          37. *
          38. * @attr ref android.R.styleable#View_rotation
          39. */
          40. public void setRotation(float
          41. if
          42. mImageView.setRotation(rotation);
          43. else
          44. if (null
          45. new
          46.
          47. // 计算旋转的中心点
          48. Drawable imageDrawable = mImageView.getDrawable();
          49. if (null
          50. mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f);
          51. mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f);
          52. }
          53. }
          54.
          55. mMatrix.setRotate(rotation, mRotationPivotX, mRotationPivotY);
          56. mImageView.setImageMatrix(mMatrix);
          57. }
          58. }
          59. }

          /**
          * The image view rotation helper
          *
          * @author lihong06
          * @since 2014-5-2
          */
          static class ImageViewRotationHelper {
          /** The imageview */
          private final ImageView mImageView;
          /** The matrix */
          private Matrix mMatrix;
          /** Pivot X */
          private float mRotationPivotX;
          /** Pivot Y */
          private float mRotationPivotY;

          /**
          * The constructor method.
          *
          * @param imageView the image view
          */
          public ImageViewRotationHelper(ImageView imageView) {
          mImageView = imageView;
          }

          /**
          * Sets the degrees that the view is rotated around the pivot point. Increasing values
          * result in clockwise rotation.
          *
          * @param rotation The degrees of rotation.
          *
          * @see #getRotation()
          * @see #getPivotX()
          * @see #getPivotY()
          * @see #setRotationX(float)
          * @see #setRotationY(float)
          *
          * @attr ref android.R.styleable#View_rotation
          */
          public void setRotation(float rotation) {
          if (APIUtils.hasHoneycomb()) {
          mImageView.setRotation(rotation);
          } else {
          if (null == mMatrix) {
          mMatrix = new Matrix();

          // 计算旋转的中心点
          Drawable imageDrawable = mImageView.getDrawable();
          if (null != imageDrawable) {
          mRotationPivotX = Math.round(imageDrawable.getIntrinsicWidth() / 2f);
          mRotationPivotY = Math.round(imageDrawable.getIntrinsicHeight() / 2f);
          }
          }

          mMatrix.setRotate(rotation, mRotationPivotX, mRotationPivotY);
          mImageView.setImageMatrix(mMatrix);
          }
          }
          }




          • PullToRefreshBase构造方法兼容2.x


          在三个参数的构造方法声明如下标注:


              @SuppressLint("NewApi")
              @TargetApi(Build.VERSION_CODES.HONEYCOMB)


           


           



          最近项目中需要用到ListView下拉刷新的功能,一开始想图省事,在网上直接找一个现成的,可是尝试了网上多个版本的下拉刷新之后发现效果都不怎么理想。有些是因为功能不完整或有Bug,有些是因为使用起来太复杂,十全十美的还真没找到。因此我也是放弃了在网上找现成代码的想法,自己花功夫编写了一种非常简单的下拉刷新实现方案,现在拿出来和大家分享一下。相信在阅读完本篇文章之后,大家都可以在自己的项目中一分钟引入下拉刷新功能。

          首先讲一下实现原理。这里我们将采取的方案是使用组合View的方式,先自定义一个布局继承自LinearLayout,然后在这个布局中加入下拉头和ListView这两个子元素,并让这两个子元素纵向排列。初始化的时候,让下拉头向上偏移出屏幕,这样我们看到的就只有ListView了。然后对ListView的touch事件进行监听,如果当前ListView已经滚动到顶部并且手指还在向下拉的话,那就将下拉头显示出来,松手后进行刷新操作,并将下拉头隐藏。原理示意图如下:

                                      

          Android ListView 实现下拉刷新上拉加载_下拉刷新_12

          那我们现在就来动手实现一下,新建一个项目起名叫PullToRefreshTest,先在项目中定义一个下拉头的布局文件pull_to_refresh.xml,代码如下所示:

          1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          2. xmlns:tools="http://schemas.android.com/tools"
          3. android:id="@+id/pull_to_refresh_head"
          4. android:layout_width="fill_parent"
          5. android:layout_height="60dip" >
          6.
          7. <LinearLayout
          8. android:layout_width="200dip"
          9. android:layout_height="60dip"
          10. android:layout_centerInParent="true"
          11. android:orientation="horizontal" >
          12.
          13. <RelativeLayout
          14. android:layout_width="0dip"
          15. android:layout_height="60dip"
          16. android:layout_weight="3"
          17. >
          18. <ImageView
          19. android:id="@+id/arrow"
          20. android:layout_width="wrap_content"
          21. android:layout_height="wrap_content"
          22. android:layout_centerInParent="true"
          23. android:src="@drawable/arrow"
          24. />
          25. <ProgressBar
          26. android:id="@+id/progress_bar"
          27. android:layout_width="30dip"
          28. android:layout_height="30dip"
          29. android:layout_centerInParent="true"
          30. android:visibility="gone"
          31. />
          32. </RelativeLayout>
          33.
          34. <LinearLayout
          35. android:layout_width="0dip"
          36. android:layout_height="60dip"
          37. android:layout_weight="12"
          38. android:orientation="vertical" >
          39.
          40. <TextView
          41. android:id="@+id/description"
          42. android:layout_width="fill_parent"
          43. android:layout_height="0dip"
          44. android:layout_weight="1"
          45. android:gravity="center_horizontal|bottom"
          46. android:text="@string/pull_to_refresh" />
          47.
          48. <TextView
          49. android:id="@+id/updated_at"
          50. android:layout_width="fill_parent"
          51. android:layout_height="0dip"
          52. android:layout_weight="1"
          53. android:gravity="center_horizontal|top"
          54. android:text="@string/updated_at" />
          55. </LinearLayout>
          56. </LinearLayout>
          57.
          58. </RelativeLayout>

          <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          android:id="@+id/pull_to_refresh_head"
          android:layout_width="fill_parent"
          android:layout_height="60dip" >

          <LinearLayout
          android:layout_width="200dip"
          android:layout_height="60dip"
          android:layout_centerInParent="true"
          android:orientation="horizontal" >

          <RelativeLayout
          android:layout_width="0dip"
          android:layout_height="60dip"
          android:layout_weight="3"
          >
          <ImageView
          android:id="@+id/arrow"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_centerInParent="true"
          android:src="@drawable/arrow"
          />
          <ProgressBar
          android:id="@+id/progress_bar"
          android:layout_width="30dip"
          android:layout_height="30dip"
          android:layout_centerInParent="true"
          android:visibility="gone"
          />
          </RelativeLayout>

          <LinearLayout
          android:layout_width="0dip"
          android:layout_height="60dip"
          android:layout_weight="12"
          android:orientation="vertical" >

          <TextView
          android:id="@+id/description"
          android:layout_width="fill_parent"
          android:layout_height="0dip"
          android:layout_weight="1"
          android:gravity="center_horizontal|bottom"
          android:text="@string/pull_to_refresh" />

          <TextView
          android:id="@+id/updated_at"
          android:layout_width="fill_parent"
          android:layout_height="0dip"
          android:layout_weight="1"
          android:gravity="center_horizontal|top"
          android:text="@string/updated_at" />
          </LinearLayout>
          </LinearLayout>

          </RelativeLayout>

          在这个布局中,我们包含了一个下拉指示箭头,一个下拉状态文字提示,和一个上次更新的时间。当然,还有一个隐藏的旋转进度条,只有正在刷新的时候我们才会将它显示出来。

          布局中所有引用的字符串我们都放在strings.xml中,如下所示:

          1. <?xml version="1.0" encoding="utf-8"?>
          2. <resources>
          3.
          4. <string name="app_name">PullToRefreshTest</string>
          5. <string name="pull_to_refresh">下拉可以刷新</string>
          6. <string name="release_to_refresh">释放立即刷新</string>
          7. <string name="refreshing">正在刷新…</string>
          8. <string name="not_updated_yet">暂未更新过</string>
          9. <string name="updated_at">上次更新于%1$s前</string>
          10. <string name="updated_just_now">刚刚更新</string>
          11. <string name="time_error">时间有问题</string>
          12.
          13. </resources>


          <?xml version="1.0" encoding="utf-8"?>
          <resources>

          <string name="app_name">PullToRefreshTest</string>
          <string name="pull_to_refresh">下拉可以刷新</string>
          <string name="release_to_refresh">释放立即刷新</string>
          <string name="refreshing">正在刷新…</string>
          <string name="not_updated_yet">暂未更新过</string>
          <string name="updated_at">上次更新于%1$s前</string>
          <string name="updated_just_now">刚刚更新</string>
          <string name="time_error">时间有问题</string>

          </resources>

          然后新建一个RefreshableView继承自LinearLayout,代码如下所示:

          ​​


          1. public class RefreshableView extends LinearLayout implements
          2.
          3. /**
          4. * 下拉状态
          5. */
          6. public static final int STATUS_PULL_TO_REFRESH = 0;
          7.
          8. /**
          9. * 释放立即刷新状态
          10. */
          11. public static final int STATUS_RELEASE_TO_REFRESH = 1;
          12.
          13. /**
          14. * 正在刷新状态
          15. */
          16. public static final int STATUS_REFRESHING = 2;
          17.
          18. /**
          19. * 刷新完成或未刷新状态
          20. */
          21. public static final int STATUS_REFRESH_FINISHED = 3;
          22.
          23. /**
          24. * 下拉头部回滚的速度
          25. */
          26. public static final int SCROLL_SPEED = -20;
          27.
          28. /**
          29. * 一分钟的毫秒值,用于判断上次的更新时间
          30. */
          31. public static final long ONE_MINUTE = 60 * 1000;
          32.
          33. /**
          34. * 一小时的毫秒值,用于判断上次的更新时间
          35. */
          36. public static final long ONE_HOUR = 60
          37.
          38. /**
          39. * 一天的毫秒值,用于判断上次的更新时间
          40. */
          41. public static final long ONE_DAY = 24
          42.
          43. /**
          44. * 一月的毫秒值,用于判断上次的更新时间
          45. */
          46. public static final long ONE_MONTH = 30
          47.
          48. /**
          49. * 一年的毫秒值,用于判断上次的更新时间
          50. */
          51. public static final long ONE_YEAR = 12
          52.
          53. /**
          54. * 上次更新时间的字符串常量,用于作为SharedPreferences的键值
          55. */
          56. private static final String UPDATED_AT = "updated_at";
          57.
          58. /**
          59. * 下拉刷新的回调接口
          60. */
          61. private
          62.
          63. /**
          64. * 用于存储上次更新时间
          65. */
          66. private
          67.
          68. /**
          69. * 下拉头的View
          70. */
          71. private
          72.
          73. /**
          74. * 需要去下拉刷新的ListView
          75. */
          76. private
          77.
          78. /**
          79. * 刷新时显示的进度条
          80. */
          81. private
          82.
          83. /**
          84. * 指示下拉和释放的箭头
          85. */
          86. private
          87.
          88. /**
          89. * 指示下拉和释放的文字描述
          90. */
          91. private
          92.
          93. /**
          94. * 上次更新时间的文字描述
          95. */
          96. private
          97.
          98. /**
          99. * 下拉头的布局参数
          100. */
          101. private
          102.
          103. /**
          104. * 上次更新时间的毫秒值
          105. */
          106. private long
          107.
          108. /**
          109. * 为了防止不同界面的下拉刷新在上次更新时间上互相有冲突,使用id来做区分
          110. */
          111. private int mId = -1;
          112.
          113. /**
          114. * 下拉头的高度
          115. */
          116. private int
          117.
          118. /**
          119. * 当前处理什么状态,可选值有STATUS_PULL_TO_REFRESH, STATUS_RELEASE_TO_REFRESH,
          120. * STATUS_REFRESHING 和 STATUS_REFRESH_FINISHED
          121. */
          122. private int
          123.
          124. /**
          125. * 记录上一次的状态是什么,避免进行重复操作
          126. */
          127. private int
          128.
          129. /**
          130. * 手指按下时的屏幕纵坐标
          131. */
          132. private float
          133.
          134. /**
          135. * 在被判定为滚动之前用户手指可以移动的最大值。
          136. */
          137. private int
          138.
          139. /**
          140. * 是否已加载过一次layout,这里onLayout中的初始化只需加载一次
          141. */
          142. private boolean
          143.
          144. /**
          145. * 当前是否可以下拉,只有ListView滚动到头的时候才允许下拉
          146. */
          147. private boolean
          148.
          149. /**
          150. * 下拉刷新控件的构造函数,会在运行时动态添加一个下拉头的布局。
          151. *
          152. * @param context
          153. * @param attrs
          154. */
          155. public
          156. super(context, attrs);
          157. preferences = PreferenceManager.getDefaultSharedPreferences(context);
          158. null, true);
          159. progressBar = (ProgressBar) header.findViewById(R.id.progress_bar);
          160. arrow = (ImageView) header.findViewById(R.id.arrow);
          161. description = (TextView) header.findViewById(R.id.description);
          162. updateAt = (TextView) header.findViewById(R.id.updated_at);
          163. touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
          164. refreshUpdatedAtValue();
          165. setOrientation(VERTICAL);
          166. 0);
          167. }
          168.
          169. /**
          170. * 进行一些关键性的初始化操作,比如:将下拉头向上偏移进行隐藏,给ListView注册touch事件。
          171. */
          172. @Override
          173. protected void onLayout(boolean changed, int l, int t, int r, int
          174. super.onLayout(changed, l, t, r, b);
          175. if
          176. hideHeaderHeight = -header.getHeight();
          177. headerLayoutParams = (MarginLayoutParams) header.getLayoutParams();
          178. headerLayoutParams.topMargin = hideHeaderHeight;
          179. 1);
          180. this);
          181. true;
          182. }
          183. }
          184.
          185. /**
          186. * 当ListView被触摸时调用,其中处理了各种下拉刷新的具体逻辑。
          187. */
          188. @Override
          189. public boolean
          190. setIsAbleToPull(event);
          191. if
          192. switch
          193. case
          194. yDown = event.getRawY();
          195. break;
          196. case
          197. float
          198. int distance = (int) (yMove - yDown);
          199. // 如果手指是下滑状态,并且下拉头是完全隐藏的,就屏蔽下拉事件
          200. if (distance <= 0
          201. return false;
          202. }
          203. if
          204. return false;
          205. }
          206. if
          207. if (headerLayoutParams.topMargin > 0) {
          208. currentStatus = STATUS_RELEASE_TO_REFRESH;
          209. else
          210. currentStatus = STATUS_PULL_TO_REFRESH;
          211. }
          212. // 通过偏移下拉头的topMargin值,来实现下拉效果
          213. 2) + hideHeaderHeight;
          214. header.setLayoutParams(headerLayoutParams);
          215. }
          216. break;
          217. case
          218. default:
          219. if
          220. // 松手时如果是释放立即刷新状态,就去调用正在刷新的任务
          221. new
          222. else if
          223. // 松手时如果是下拉状态,就去调用隐藏下拉头的任务
          224. new
          225. }
          226. break;
          227. }
          228. // 时刻记得更新下拉头中的信息
          229. if
          230. || currentStatus == STATUS_RELEASE_TO_REFRESH) {
          231. updateHeaderView();
          232. // 当前正处于下拉或释放状态,要让ListView失去焦点,否则被点击的那一项会一直处于选中状态
          233. false);
          234. false); %N�;#F����vӡFʿ�