ListView 实现下拉刷新

作者: 随时学丫 | 来源:发表于2018-07-14 20:06 被阅读32次

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

    demo.gif

    我们先分析一下如何实现 ListView 下拉刷新。

    1. 当我们下拉的时候,会出现一个提示界面,即 ListView 的 Header 布局。
    2. ListView 要实现滚动,所以要监听 ListView 滚动事件,即 OnScrollListener() 事件。
    3. 当我们开始滚动时,Header 布局才慢慢显示出来,所以需要监听 ListView 的 onTouch() 事件。

    实现思路

    1. 首先判断 ListView 刷新时机,当 ListView 的 firstVisibleItem 为 0 时表示当前处于ListView 最顶端,此时允许下拉。
    2. 自定义一个 HeaderView,将 HeaderView 添加到 ListView 头部,在下拉时候的显示和完成时候的隐藏。
    3. 定义一个刷新接口,当下拉动作完成时候回调,用于标记状态并刷新最新数据进行展示。

    1、定义 Header

    第一次打开

    Header 要实现的效果:

    1. 第一次下拉时,Header 逐渐显示,文字显示为下拉可以刷新,箭头向下,进度条隐藏
    2. 当松开刷新的时候,箭头隐藏,进度条展示,文字改为正在刷新,并记录当前刷新时间


    第二次打开

    image-20180714185659186.png

    Header 要实现的效果:

    1. 再一次下拉时,Header 逐渐显示,文字显示为下拉可以刷新,箭头向下,显示上一次刷新时间,进度条隐藏
    2. 当松开刷新的时候,箭头隐藏,进度条展示,文字改为正在刷新,并再一次记录当前刷新时间

    定义一个如上图所示的 Header 的 XML 文件 header_layout.xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingBottom="10dp"
        android:paddingTop="10dp">
    
        <LinearLayout
            android:id="@+id/layout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:gravity="center"
            android:orientation="vertical">
    
            <TextView
                android:id="@+id/tv_tip"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="下拉可以刷新"
                android:textSize="12sp" />
    
            <TextView
                android:id="@+id/tv_last_update_time"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="12sp"
                tools:text="上次刷新时间" />
        </LinearLayout>
    
        <ImageView
            android:id="@+id/img_arrow"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="10dp"
            android:layout_toLeftOf="@+id/layout"
            android:src="@drawable/pull_to_refresh_arrow" />
    
        <ProgressBar
            android:id="@+id/progress"
            style="@style/progressBar_custom_drawable"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginRight="10dp"
            android:layout_toLeftOf="@+id/img_arrow"
            android:visibility="gone"
            tools:visibility="visible" />
    </RelativeLayout>
    

    2、初始化布局

    定义一个 RefreshListView 类继承 ListView,重写构造函数,并将 Header 添加到 ListView 中。

    public class RefreshListView extends ListView {
        View header;
        public RefreshListView(Context context) {
            super(context);
            initView(context);
        }
    
        public RefreshListView(Context context, AttributeSet attrs) {
            super(context, attrs);
            initView(context);
        }
    
        public RefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            initView(context);
        }
    
    
        private void initView(Context context) {
            header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
            this.addHeaderView(header);
            this.setOnScrollListener(this);
        }
        
        /**
         * 设置 Header 布局的上边距
         * 以隐藏 Header
         * @param topPadding
         */
        private void topPadding(int topPadding) {
            header.setPadding(header.getPaddingLeft(), topPadding,
                    header.getPaddingRight(),
                    header.getPaddingBottom());
            header.invalidate();
        }
    }
    

    运行效果


    添加Header

    当我们打开界面的时候,最开始 Header 应该为隐藏,因为在下拉过程中 Header 是逐渐显示的,而不是一下子就出来,所以这里我们不能直接使用 GONE,而是要用 padding 来设置显示和隐藏。

    private void initView(Context context) {
            header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
            headerHeight = header.getMeasuredHeight();//获取 Header 的高度
            Log.i("initView","headerHeight:"+headerHeight);
            topPadding(-headerHeight);//初始状态隐藏 Header
            this.addHeaderView(header);
        }
    

    再次运行


    添加Header

    发现根本没变化,为什么?我们可以打印一下 headerHeight 的值。

    07-14 19:44:11.705 8658-8658/com.dali.refreshandloadmorelistview I/initView: headerHeight:0
    

    headerHeight 的值是 0,说明我们没有取到 headerHeight 的值,在这里我们需要测量 headerHeight 的高度。

    /**
     * 通知父布局 header 占用的宽高
     * @param view
     */
    private void measureView(View view) {
        ViewGroup.LayoutParams lp = view.getLayoutParams();
        if (lp == null) {
            lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int width = ViewGroup.getChildMeasureSpec(0, 0, lp.width);
        int height;
        int tempHeight = lp.height;
        if (tempHeight > 0) {
            height = MeasureSpec.makeMeasureSpec(tempHeight, MeasureSpec.EXACTLY);
        } else {
            height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        view.measure(width, height);
    }
    
    private void initView(Context context) {
            header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
            measureView(header);
            //这里获取高度的时候需要先通知父布局header占用的空间
            headerHeight = header.getMeasuredHeight();
            Log.i("initView", "headerHeight:" + headerHeight);
            topPadding(-headerHeight);
            this.addHeaderView(header);
        }
    

    再次运行,headerHeight 被准确测量,再看运行效果

    07-14 19:44:11.705 8658-8658/com.dali.refreshandloadmorelistview I/initView: headerHeight:186
    
    隐藏 Header

    3、实现下拉刷新

    给 ListView 设置监听

    public class RefreshListView extends ListView implements AbsListView.OnScrollListener {
        private int firstVisibleItem;//当前第一个 Item 可见位置
        private int scrollState;//当前滚动状态
        private void initView(Context context) {
            header = LayoutInflater.from(context).inflate(R.layout.header_layout, null);
            measureView(header);
            //这里获取高度的时候需要先通知父布局header占用的空间
            headerHeight = header.getMeasuredHeight();
            topPadding(-headerHeight);
            this.addHeaderView(header);
            this.setOnScrollListener(this);
        }
        
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            this.scrollState = scrollState;
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            this.firstVisibleItem = firstVisibleItem;
        }
    }
    

    刷新的时机是判断 firstVisibleItem 是否为 0,而下拉事件我们需要重写 onTouchEvent() 事件,首先定义几个状态。

    private boolean isMark;//标记按下时当前在ListView最顶端
    private float startY;//按下时开始的Y值
    
    private static int state;//当前状态
    private final static int NONE = 0;//正常状态
    private final static int PULL = 1;//下拉状态
    private final static int RELEASE = 2;//释放状态
    private final static int REFRESHING = 3;//正在刷新状态
    

    在 onTouchEvent 中,在 ACTION_DOWN 时,记录最开始的 Y 值,然后在 ACTION_MOVE 事件中实时记录移动距离 space,不断刷新 HeaderView 的 topPadding,让它跟随滑动距离进行显示,继续滑动,当 space 大于了 headerHeight 时,状态给为 RELEASE,表示可以释放进行刷新操作。

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //最顶部
                if (firstVisibleItem == 0) {
                    isMark = true;
                    startY = ev.getY();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                onMove(ev);
                break;
            case MotionEvent.ACTION_UP:
                if (state == RELEASE) {//如果已经释放,则可以提示刷新数据
                    state = REFRESHING;
                } else if (state == PULL) {//如果是在下拉状态,不刷新数据
                    state = NONE;
                    isMark = false;
                }
                refreshViewByState();
                break;
        }
        return super.onTouchEvent(ev);
    }
    
    /**
         * 判断移动过程中的操作
         * @param ev
         */
        private void onMove(MotionEvent ev) {
            if (!isMark) {
                return;
            }
            int tempY = (int) ev.getY();//移动过程中的Y值
            int space = (int) (tempY - startY);//移动的距离
            int topPadding = space - headerHeight;//在移动过程中不断设置 topPadding
            switch (state) {
                case NONE:
                    //移动距离大于0
                    if (space > 0) {
                        state = PULL; //状态变成下拉状态
                        refreshViewByState();
                    }
                    break;
                case PULL:
                    topPadding(topPadding);
                    //移动距离大于headerHeight并且正在滚动
                    if (space > (headerHeight + 30)
                            && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
                        state = RELEASE;//提示释放
                        refreshViewByState();
                    }
                    break;
                case RELEASE:
                    topPadding(topPadding);
                    //移动距离小于headerHeight并且正在滚动
                    if (space < headerHeight + 30) {
                        state = PULL;//提示下拉
                    } else if (space <= 0) {
                        state = NONE;
                        isMark = false;
                    }
                    refreshViewByState();
                    break;
            }
        }
    

    刷新数据的时候,要根据状态不断改变 HeaderView 的显示,箭头定义一个旋转动画让其跟随滑动距离实现旋转,进度条也设置了逐帧动画实现自定义进度条。

    private void refreshViewByState() {
        TextView tip = header.findViewById(R.id.tv_tip);
        ImageView arrow = header.findViewById(R.id.img_arrow);
        ProgressBar progressBar = header.findViewById(R.id.progress);
        progressBar.setBackgroundResource(R.drawable.custom_progress_bar);
        AnimationDrawable animationDrawable = (AnimationDrawable) progressBar.getBackground();
        //给箭头设置动画
        RotateAnimation anim = new RotateAnimation(0, 180,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        RotateAnimation anim1 = new RotateAnimation(180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(200);
        anim.setFillAfter(true);
        anim1.setDuration(200);
        anim1.setFillAfter(true);
        switch (state) {
            case NONE://正常,Header不显示
                topPadding(-headerHeight);
                arrow.clearAnimation();
                break;
            case PULL://下拉状态
                arrow.setVisibility(VISIBLE);//箭头显示,进度条隐藏
                progressBar.setVisibility(GONE);
                if (animationDrawable.isRunning()) {
                    //停止动画播放
                    animationDrawable.stop();
                }
                tip.setText("下拉可以刷新");
                arrow.clearAnimation();
                arrow.setAnimation(anim1);//箭头向上
                break;
            case RELEASE://释放状态
                arrow.setVisibility(VISIBLE);//箭头显示,进度条隐藏
                progressBar.setVisibility(GONE);
                if (animationDrawable.isRunning()) {
                    //停止动画播放
                    animationDrawable.stop();
                }
                tip.setText("松开可以刷新");
                arrow.clearAnimation();
                arrow.setAnimation(anim);//箭头向下
                break;
            case REFRESHING://刷新状态
                topPadding(50);
                arrow.setVisibility(GONE);//箭头显示,进度条隐藏
                progressBar.setVisibility(VISIBLE);
                animationDrawable.start();
                tip.setText("正在刷新...");
                arrow.clearAnimation();
                break;
        }
    }
    

    4、下拉刷新完成回调

    当下拉刷新完成时,我们需要实现数据的刷新,并且要通知 Adapter 刷新数据,这里我们定义一个监听接口实现回调即可。回调在 ACTION_UP 的 RELEASE 状态下进行注册。

    private IRefreshListener iRefreshListener;
    
    public void setIRefreshListener(IRefreshListener iRefreshListener) {
        this.iRefreshListener = iRefreshListener;
    }
    
    public interface IRefreshListener {
        void onRefresh();
    }
    
    @Override
        public boolean onTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    //最顶部
                    if (firstVisibleItem == 0) {
                        isMark = true;
                        startY = ev.getY();
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    onMove(ev);
                    break;
                case MotionEvent.ACTION_UP:
                    if (state == RELEASE) {//如果已经释放,则可以提示刷新数据
                        state = REFRESHING;
                        //刷新数据,通知 Activity 进行刷新
                        if (iRefreshListener != null) {
                            iRefreshListener.onRefresh();
                        }
                    } else if (state == PULL) {//如果是在下拉状态,不刷新数据
                        state = NONE;
                        isMark = false;
                    }
                    refreshViewByState();
                    break;
            }
            return super.onTouchEvent(ev);
        }
    

    5、测试

    public class MainActivity extends Activity implements RefreshListView.IRefreshListener {
    
        private ArrayList<ApkEntity> apk_list;
        private ListAdapter adapter;
        private RefreshListView listView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            setData();
            showList(apk_list);
        }
    
        private void showList(ArrayList<ApkEntity> apk_list) {
            if (adapter == null) {
                listView = (RefreshListView) findViewById(R.id.listview);
                listView.setIRefreshListener(this);
                adapter = new ListAdapter(this, apk_list);
                listView.setAdapter(adapter);
            } else {
                adapter.onDateChange(apk_list);
            }
        }
    
        private void setData() {
            apk_list = new ArrayList<ApkEntity>();
            for (int i = 0; i < 10; i++) {
                ApkEntity entity = new ApkEntity();
                entity.setName("默认数据 " + i);
                entity.setDes("这是一个神奇的应用");
                entity.setInfo("50w用户");
                apk_list.add(entity);
            }
        }
    
        private void setRefreshData() {
            for (int i = 0; i < 2; i++) {
                ApkEntity entity = new ApkEntity();
                entity.setName("默认数据 + 刷新 " + i);
                entity.setDes("这是一个神奇的应用");
                entity.setInfo("50w用户");
                apk_list.add(0, entity);
            }
        }
    
        @Override
        public void onRefresh() {
            //添加刷新动画效果
            Handler handler = new Handler();
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    //获取最新数据
                    setRefreshData();
                    //通知界面显示数据
                    showList(apk_list);
                    //通知 ListView 刷新完成
                    listView.refreshComplete();
                }
            }, 2000);
        }
    }
    

    GitHub 源码

    相关文章

      网友评论

        本文标题:ListView 实现下拉刷新

        本文链接:https://www.haomeiwen.com/subject/evaypftx.html