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 中判斷出當前是要滑出頭提示還是尾提示,然后再根據手勢滑動的垂直距離來實時改變頭尾控件的高度,再調用容器中的更新子布局方法即可。

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);
    }

}
一種具備上下拉刷新功能的ListView

 


 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM