Android如何实现一个上拉刷新下拉加载的ListView

2019-12-20

关键字:自定义上下拉ListView


在 APK 开发中,一个具备在列表顶部下拉刷新、在列表尾部上拉加载功能的 ListView 的需求还是比较多的。

具备这种功能的优秀开源代码同样也有很多。

但今天,笔者就非要自己实现一个这样的控件不可。

以下是成品效果图:

这个控件的结构很简单:

1、一个LinearLayout容器打底;

2、一个ListView置于中间;

3、一个用于标识头部“下拉刷新”标语的控件;

4、一个用于标识尾部“上拉加载”标语的控件;

仅此而已。

所以,笔者这个上下拉列表控件其实是需要自定义一个LinearLayout容器控件。然后在这个容器控件里根据规则来处理触摸事件、点击事件并通知上下拉事件等。

public class PullingListView extends LinearLayout

这里有几个难点:

1、如何监听列表滚动到头部还是尾部亦或正处于中间?

2、上在列表上的上、下滑事件应如何响应成滑出对应的提示标语?

3、首尾提示标语应如何随手势滑出来?

关于第 1 点,直接通过监听 ListView 的 onScrollListener 即可勉强达到目的。

listview.setOnScrollListener(this);

为什么说是勉强呢?因为这个监听会在ListView滚动时回调,虽然它会告诉我们当前ListView中第 1 个可见Item的标号与最后一个可见Item的标号以及总Item数量。但它会在Item刚一加载时就通知,而不是在Item真正展示出来或者真正展示完全以后才通知。这就会存在一个“超前通知”的问题。就是实际上我们还没有看到第 1 个Item,但你却在回调方法中告诉我它已经展示出来了。这会让我们误判。关于这个问题,笔者目前还没有找到解决办法。

而关于第 2 点,则是通过监听ListView的触摸事件,并根据前面 onScrollListener 中得到的当前列表位置,再根据手势方向来决定是该滑出提示语还是让其滚动ListView。

listview.setOnTouchListener(this);

第 3 点其实也不难,只需要在 onTouch 中判断出当前是要滑出头提示还是尾提示,然后再根据手势滑动的垂直距离来实时改变头尾控件的高度,再调用容器中的更新子布局方法即可。

1head.setLayoutHeight((int) distanceVertical); 2requestLayout();

整个控件的核心就这些东西。整体代码量不多,能实现上面效果图中的功能,但同样也存在一些问题。具体问题就是在列表中数量超过一屏幕容量时,上、下滑动未及端点即开始响应滑出提示语的现象。这个现象的原因笔者在上面已经分析过了。

以下贴出完整源码:

1/** 2 * 一个具备上拉刷新与下拉加载功能的ListView 3 * */ 4public class PullingListView extends LinearLayout implements View.OnTouchListener, AdapterView.OnItemClickListener, AbsListView.OnScrollListener { 5 6 private static final String TAG = "PullingListView"; 7 8 private static final int LISTVIEW_SCROLL_STATUS_IN_HEAD = 0; 9 private static final int LISTVIEW_SCROLL_STATUS_IN_MIDDLE = 1; 10 private static final int LISTVIEW_SCROLL_STATUS_IN_TAIL = 2; 11 12 private float y0; 13 private float lastDisHeight; //上次垂直移动的高度。 14 15 private int listViewPos; 16 17 private ListView listview; 18 19 private Header head; 20 private Header foot; 21 22 private OnPullingListViewListener listener; 23 private ListAdapter adapter; 24 25 public PullingListView(Context context){ 26 super(context); 27 init(); 28 } 29 30 private void init(){ 31 Logger.v(TAG, "init()"); 32 setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 33 setOrientation(VERTICAL); 34 35 listview = new ListView(getContext()); 36 LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 37 llp.weight = 1; 38 listview.setLayoutParams(llp); 39 40 head = new Header(true); 41 foot = new Header(false); 42 43 listview.setOnTouchListener(this); 44 listview.setOnItemClickListener(this); 45 listview.setOnScrollListener(this); 46 47 addView(head.getView()); 48 addView(listview); 49 addView(foot.getView()); 50 } 51 52 @Override 53 public boolean onTouch(View v, MotionEvent event){ 54// Logger.v(TAG, "onTouch,action:" + event.getAction() + ",listViewPos:" + listViewPos); 55 switch(event.getAction()){ 56 case MotionEvent.ACTION_DOWN:{ 57 y0 = event.getY(); 58 lastDisHeight = 0; 59 listview.scrollTo(0, 0); 60 head.setLayoutHeight(0); 61 foot.setLayoutHeight(0); 62 requestLayout(); 63 }break; 64 case MotionEvent.ACTION_MOVE:{ 65 float distanceVertical = (event.getY() - y0) / 2.0f; //为了避免响应过于灵敏,垂直滑动距离应延缓5倍。 66 switch(listViewPos){ 67 case LISTVIEW_SCROLL_STATUS_IN_MIDDLE:{ 68 Logger.d(TAG, "MIDDLE"); 69 return false; 70 } 71 case LISTVIEW_SCROLL_STATUS_IN_HEAD:{ 72 Logger.d(TAG, "HEAD"); 73 if(distanceVertical > 0){ 74 //往下滑动。 75 head.setLayoutHeight((int) distanceVertical); 76 requestLayout(); 77 }else{ 78 //往上滑动,要看有没有填满。 79 if(adapter.getCount() > 0){ 80 int shownHeight = listview.getChildCount() * (listview.getChildAt(0).getHeight() + listview.getDividerHeight()); 81 if(shownHeight <= listview.getHeight()){ 82// Logger.d(TAG, "None fill out."); 83 //没填满. 84 foot.setLayoutHeight((int) distanceVertical); 85 requestLayout(); 86 87 if(foot.getView().getLayoutParams().height >= foot.HEAD_LAYOUT_HEIGHT_MAX){ 88 lastDisHeight = distanceVertical; 89 listview.scrollTo(0, foot.getView().getLayoutParams().height); 90 }else{ 91 listview.scrollBy(0, (int) (distanceVertical - lastDisHeight) * -1); 92 } 93 94 lastDisHeight = distanceVertical; 95 }else{ 96// Logger.d(TAG, "filled out."); 97 //填满了,要滑动item。 98 return false; 99 } 100 }else{ 101// Logger.d(TAG, "No records"); 102 //没有数据,则忽略掉滑动事件。 103 return true; 104 } 105 } 106 }break; 107 case LISTVIEW_SCROLL_STATUS_IN_TAIL:{ 108 Logger.d(TAG, "TAIL"); 109 if(distanceVertical < 0){ 110 //往上滑动,加载。 111 foot.setLayoutHeight((int) distanceVertical); 112 requestLayout(); 113 listview.scrollTo(0, 0); 114 }else{ 115 //往下滑动 116 return false; 117 } 118 }break; 119 } 120 }break; 121 case MotionEvent.ACTION_UP:{ 122 if(head.canLoad()){ 123 head.load(); 124 if(listener != null) { 125 listener.onRefresh(); 126 } 127 }else if(foot.canLoad()){ 128 foot.load(); 129 if(listener != null) { 130 listener.onLoad(); 131 } 132 }else{ 133 foot.setLayoutHeight(0); 134 head.setLayoutHeight(0); 135 requestLayout(); 136 listview.scrollTo(0, 0); 137 } 138 139 }break; 140 }//switch(event.getAction()) -- end 141 142 return false; 143 }//onTouch() -- end 144 145 @Override 146 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 147 if(listener != null) { 148 listener.onItemClick(parent, view, position, id); 149 } 150 } 151 152 @Override 153 public void onScrollStateChanged(AbsListView view, int scrollState) { 154 // do nothing. 155 Logger.d(TAG, "onScrollStateChanged,scrollState:" + scrollState); 156 } 157 158 @Override 159 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { 160 Logger.d(TAG, "onScroll,firstVisibleItem:" + firstVisibleItem + ",visibleItemCount:" + visibleItemCount + ",totalItemCount:" + totalItemCount); 161 if (firstVisibleItem == 0) { 162 listViewPos = LISTVIEW_SCROLL_STATUS_IN_HEAD; 163 }else if (visibleItemCount + firstVisibleItem == totalItemCount) { 164 listViewPos = LISTVIEW_SCROLL_STATUS_IN_TAIL; 165 }else{ 166 listViewPos = LISTVIEW_SCROLL_STATUS_IN_MIDDLE; 167 } 168 } 169 170 public void setDivider(Drawable divider){ 171 listview.setDivider(divider); 172 } 173 174 public void setDividerHeight(int height){ 175 listview.setDividerHeight(height); 176 } 177 178 public void setAdapter(ListAdapter adapter){ 179 this.adapter = adapter; 180 listview.setAdapter(adapter); 181 } 182 183 public void setOnPullingListViewListener(OnPullingListViewListener listener){ 184 this.listener = listener; 185 } 186 187 public void refreshed(){ 188 Logger.v(TAG, "refreshed"); 189 listview.post(new Runnable() { 190 @Override 191 public void run() { 192 foot.loadFinished(); 193 head.loadFinished(); 194 requestLayout(); 195 listview.scrollTo(0, 0); 196 } 197 }); 198 } 199 200 public void setSelction(int selection){ 201 Logger.v(TAG, "setSelction:" + selection); 202 listview.setSelection(selection); 203 } 204 205 /** 206 * 上下两个页眉的布局管理。 207 * */ 208 private class Header extends BaseLayoutManager { 209 210 private final int HEAD_LAYOUT_HEIGHT_MAX = UnitManager.px2dp(60); 211 212 private final int STATUS_NORMAL = 0; 213 private final int STATUS_TIP = 1; 214 private final int STATUS_LOADING = 2; 215 216 private int status; 217 218 private boolean isTop; 219 220 private TextView tv; 221 222 private Header(boolean isTop){ 223 super(null); 224 this.isTop = isTop; 225 LinearLayout linearLayout = new LinearLayout(PullingListView.this.getContext()); 226 LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(-1, 0); 227 linearLayout.setLayoutParams(llp); 228 linearLayout.setBackgroundColor(ResourcesManager.getColor(R.color.activity_base_bg)); 229 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 230 linearLayout.setGravity(Gravity.CENTER); 231 232 view = linearLayout; 233 234 ProgressBar pb = new ProgressBar(getContext()); 235 llp = new LinearLayout.LayoutParams(UnitManager.px2dp(35), UnitManager.px2dp(35)); 236 pb.setLayoutParams(llp); 237 238 239 tv = new TextView(getContext()); 240 if(isTop) { 241 tv.setText("刷新列表"); 242 }else { 243 tv.setText("加载更多"); 244 } 245 tv.setGravity(Gravity.CENTER_VERTICAL); 246 llp = new LinearLayout.LayoutParams(-2, -1); 247 llp.leftMargin = UnitManager.px2dp(15); 248 tv.setLayoutParams(llp); 249 250 linearLayout.addView(pb); 251 linearLayout.addView(tv); 252 } 253 254 private void setLayoutHeight(int height){ 255 height = Math.abs(height); 256 if(height < HEAD_LAYOUT_HEIGHT_MAX){ 257 view.getLayoutParams().height = height; 258 if(height > (HEAD_LAYOUT_HEIGHT_MAX * 0.7)){ 259 if(status != STATUS_TIP){ 260 status = STATUS_TIP; 261 if(isTop) { 262 tv.setText("松开以刷新"); 263 }else{ 264 tv.setText("松开以加载"); 265 } 266 } 267 }else { 268 if(status != STATUS_NORMAL){ 269 status = STATUS_NORMAL; 270 if(isTop) { 271 tv.setText("刷新列表"); 272 }else{ 273 tv.setText("加载更多"); 274 } 275 } 276 } 277 }else{ 278 view.getLayoutParams().height = HEAD_LAYOUT_HEIGHT_MAX; 279 } 280 } 281 282 private boolean canLoad(){ 283 return status == STATUS_TIP; 284 } 285 286 private void load(){ 287 status = STATUS_LOADING; 288 if(isTop) { 289 tv.setText("刷新中,请稍候"); 290 }else{ 291 tv.setText("加载中,请稍候"); 292 } 293 } 294 295 private void loadFinished(){ 296 status = STATUS_NORMAL; 297 view.getLayoutParams().height = 0; 298 if(isTop) { 299 tv.setText("刷新列表"); 300 }else { 301 tv.setText("加载更多"); 302 } 303 } 304 305 }// class Header -- end 306 307 public interface OnPullingListViewListener { 308 void onRefresh(); 309 void onLoad(); 310 void onItemClick(AdapterView<?> parent, View view, int position, long id); 311 } 312 313}

一种具备上下拉刷新功能的ListView


点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Android So动态加载 优雅实现与原理分析

背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我

SwipeRefreshLayout下拉刷新冲突解决

使用SwipeRefreshLayout,网上资料copy了一个OnScrollListener给ListView,结果当第一个item长度超过一屏,明明还没有到达列表顶部,Scroll事件就被拦截,列表无法滚动,同时启动了刷新。修正代码后,自定义的OnScrollListener如下:/ 由于Listview与下拉刷新的Scroll

Android如何实现一个上拉刷新下拉加载的ListView - HelloWorld