zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    解决Android SDK Manager无法更新下载
    使用Anaconda3配置多版本Python虚拟开发环境
    Python·Jupyter Notebook各种使用方法
    学习 python 编写规范 pep8 的问题笔记
    ajax工作原理及其优缺点
    json和jsonp
    cookie、session、localStorage、sessionStorage区别
    浅谈前端性能优化(PC版)
    浅谈前端性能优化(移动端)
    前端优化 -- Combo Handler
  • 原文地址:https://www.cnblogs.com/chorm590/p/11832349.html
Copyright © 2011-2022 走看看