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 中判斷出當前是要滑出頭提示還是尾提示,然后再根據手勢滑動的垂直距離來實時改變頭尾控件的高度,再調用容器中的更新子布局方法即可。
head.setLayoutHeight((int) distanceVertical); requestLayout();
整個控件的核心就這些東西。整體代碼量不多,能實現上面效果圖中的功能,但同樣也存在一些問題。具體問題就是在列表中數量超過一屏幕容量時,上、下滑動未及端點即開始響應滑出提示語的現象。這個現象的原因筆者在上面已經分析過了。
以下貼出完整源碼:

/** * 一個具備上拉刷新與下拉加載功能的ListView * */ public class PullingListView extends LinearLayout implements View.OnTouchListener, AdapterView.OnItemClickListener, AbsListView.OnScrollListener { private static final String TAG = "PullingListView"; private static final int LISTVIEW_SCROLL_STATUS_IN_HEAD = 0; private static final int LISTVIEW_SCROLL_STATUS_IN_MIDDLE = 1; private static final int LISTVIEW_SCROLL_STATUS_IN_TAIL = 2; private float y0; private float lastDisHeight; //上次垂直移動的高度。 private int listViewPos; private ListView listview; private Header head; private Header foot; private OnPullingListViewListener listener; private ListAdapter adapter; public PullingListView(Context context){ super(context); init(); } private void init(){ Logger.v(TAG, "init()"); setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); setOrientation(VERTICAL); listview = new ListView(getContext()); LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); llp.weight = 1; listview.setLayoutParams(llp); head = new Header(true); foot = new Header(false); listview.setOnTouchListener(this); listview.setOnItemClickListener(this); listview.setOnScrollListener(this); addView(head.getView()); addView(listview); addView(foot.getView()); } @Override public boolean onTouch(View v, MotionEvent event){ // Logger.v(TAG, "onTouch,action:" + event.getAction() + ",listViewPos:" + listViewPos); switch(event.getAction()){ case MotionEvent.ACTION_DOWN:{ y0 = event.getY(); lastDisHeight = 0; listview.scrollTo(0, 0); head.setLayoutHeight(0); foot.setLayoutHeight(0); requestLayout(); }break; case MotionEvent.ACTION_MOVE:{ float distanceVertical = (event.getY() - y0) / 2.0f; //為了避免響應過於靈敏,垂直滑動距離應延緩5倍。 switch(listViewPos){ case LISTVIEW_SCROLL_STATUS_IN_MIDDLE:{ Logger.d(TAG, "MIDDLE"); return false; } case LISTVIEW_SCROLL_STATUS_IN_HEAD:{ Logger.d(TAG, "HEAD"); if(distanceVertical > 0){ //往下滑動。 head.setLayoutHeight((int) distanceVertical); requestLayout(); }else{ //往上滑動,要看有沒有填滿。 if(adapter.getCount() > 0){ int shownHeight = listview.getChildCount() * (listview.getChildAt(0).getHeight() + listview.getDividerHeight()); if(shownHeight <= listview.getHeight()){ // Logger.d(TAG, "None fill out."); //沒填滿. foot.setLayoutHeight((int) distanceVertical); requestLayout(); if(foot.getView().getLayoutParams().height >= foot.HEAD_LAYOUT_HEIGHT_MAX){ lastDisHeight = distanceVertical; listview.scrollTo(0, foot.getView().getLayoutParams().height); }else{ listview.scrollBy(0, (int) (distanceVertical - lastDisHeight) * -1); } lastDisHeight = distanceVertical; }else{ // Logger.d(TAG, "filled out."); //填滿了,要滑動item。 return false; } }else{ // Logger.d(TAG, "No records"); //沒有數據,則忽略掉滑動事件。 return true; } } }break; case LISTVIEW_SCROLL_STATUS_IN_TAIL:{ Logger.d(TAG, "TAIL"); if(distanceVertical < 0){ //往上滑動,加載。 foot.setLayoutHeight((int) distanceVertical); requestLayout(); listview.scrollTo(0, 0); }else{ //往下滑動 return false; } }break; } }break; case MotionEvent.ACTION_UP:{ if(head.canLoad()){ head.load(); if(listener != null) { listener.onRefresh(); } }else if(foot.canLoad()){ foot.load(); if(listener != null) { listener.onLoad(); } }else{ foot.setLayoutHeight(0); head.setLayoutHeight(0); requestLayout(); listview.scrollTo(0, 0); } }break; }//switch(event.getAction()) -- end return false; }//onTouch() -- end @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { if(listener != null) { listener.onItemClick(parent, view, position, id); } } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // do nothing. Logger.d(TAG, "onScrollStateChanged,scrollState:" + scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { Logger.d(TAG, "onScroll,firstVisibleItem:" + firstVisibleItem + ",visibleItemCount:" + visibleItemCount + ",totalItemCount:" + totalItemCount); if (firstVisibleItem == 0) { listViewPos = LISTVIEW_SCROLL_STATUS_IN_HEAD; }else if (visibleItemCount + firstVisibleItem == totalItemCount) { listViewPos = LISTVIEW_SCROLL_STATUS_IN_TAIL; }else{ listViewPos = LISTVIEW_SCROLL_STATUS_IN_MIDDLE; } } public void setDivider(Drawable divider){ listview.setDivider(divider); } public void setDividerHeight(int height){ listview.setDividerHeight(height); } public void setAdapter(ListAdapter adapter){ this.adapter = adapter; listview.setAdapter(adapter); } public void setOnPullingListViewListener(OnPullingListViewListener listener){ this.listener = listener; } public void refreshed(){ Logger.v(TAG, "refreshed"); listview.post(new Runnable() { @Override public void run() { foot.loadFinished(); head.loadFinished(); requestLayout(); listview.scrollTo(0, 0); } }); } public void setSelction(int selection){ Logger.v(TAG, "setSelction:" + selection); listview.setSelection(selection); } /** * 上下兩個頁眉的布局管理。 * */ private class Header extends BaseLayoutManager { private final int HEAD_LAYOUT_HEIGHT_MAX = UnitManager.px2dp(60); private final int STATUS_NORMAL = 0; private final int STATUS_TIP = 1; private final int STATUS_LOADING = 2; private int status; private boolean isTop; private TextView tv; private Header(boolean isTop){ super(null); this.isTop = isTop; LinearLayout linearLayout = new LinearLayout(PullingListView.this.getContext()); LinearLayout.LayoutParams llp = new LinearLayout.LayoutParams(-1, 0); linearLayout.setLayoutParams(llp); linearLayout.setBackgroundColor(ResourcesManager.getColor(R.color.activity_base_bg)); linearLayout.setOrientation(LinearLayout.HORIZONTAL); linearLayout.setGravity(Gravity.CENTER); view = linearLayout; ProgressBar pb = new ProgressBar(getContext()); llp = new LinearLayout.LayoutParams(UnitManager.px2dp(35), UnitManager.px2dp(35)); pb.setLayoutParams(llp); tv = new TextView(getContext()); if(isTop) { tv.setText("刷新列表"); }else { tv.setText("加載更多"); } tv.setGravity(Gravity.CENTER_VERTICAL); llp = new LinearLayout.LayoutParams(-2, -1); llp.leftMargin = UnitManager.px2dp(15); tv.setLayoutParams(llp); linearLayout.addView(pb); linearLayout.addView(tv); } private void setLayoutHeight(int height){ height = Math.abs(height); if(height < HEAD_LAYOUT_HEIGHT_MAX){ view.getLayoutParams().height = height; if(height > (HEAD_LAYOUT_HEIGHT_MAX * 0.7)){ if(status != STATUS_TIP){ status = STATUS_TIP; if(isTop) { tv.setText("松開以刷新"); }else{ tv.setText("松開以加載"); } } }else { if(status != STATUS_NORMAL){ status = STATUS_NORMAL; if(isTop) { tv.setText("刷新列表"); }else{ tv.setText("加載更多"); } } } }else{ view.getLayoutParams().height = HEAD_LAYOUT_HEIGHT_MAX; } } private boolean canLoad(){ return status == STATUS_TIP; } private void load(){ status = STATUS_LOADING; if(isTop) { tv.setText("刷新中,請稍候"); }else{ tv.setText("加載中,請稍候"); } } private void loadFinished(){ status = STATUS_NORMAL; view.getLayoutParams().height = 0; if(isTop) { tv.setText("刷新列表"); }else { tv.setText("加載更多"); } } }// class Header -- end public interface OnPullingListViewListener { void onRefresh(); void onLoad(); void onItemClick(AdapterView<?> parent, View view, int position, long id); } }