View系列(1)--自定义一个ListView的下拉刷新

原创 2017年06月22日 11:12:38

下拉刷新是常见的Android效果之一,下面我们来看看它是怎么实现的。

下拉刷新会在头部出现一个头部,然后我们控制这个头部显示,隐藏,来实现下拉刷新的效果。在listview里面的item是从0开始数的,这个头部item就是在第0个item前面一个,我们现在要做的是控制这个头部的绘画和动作事件。

/**
     * 初始化界面,添加顶部布局文件到 listview
     *
     * @param context
     */
    private void initView(Context context) {
        LayoutInflater inflater = LayoutInflater.from(context);
        header = inflater.inflate(R.layout.header_layout, null);
        measureView(header);
        headerHeight = header.getMeasuredHeight();
        Log.i("tag", "headerHeight = " + headerHeight);
        topPadding(-headerHeight);
        this.addHeaderView(header);
        this.setOnScrollListener(this);
    }

header是设置头部布局,这个布局在header_layout.xml里面,然后测量高度。addHeaderView是listview为我们提供的头部,setOnScrollListener是监听滑动事件。
我的头部布局如下,这里我就不贴xml里面的代码了。
header_layout布局
header_layout

measureView是测量子空间的宽高

 /**
     * 通知父布局,占用的宽,高;
     *
     * @param view
     */
    private void measureView(View view) {
        ViewGroup.LayoutParams p = view.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
        //spec,padding均为0,这是子布局为实际大小。
        //   spec 父窗口传递给子视图的大小和模式
        //  padding 父窗口的边距,也就是xml中的android:padding
        //   childDimension 子视图想要绘制的准确大小,但最终不一定绘制此值        
        int height;
        int tempHeight = p.height;
        if (tempHeight > 0) {
            height = MeasureSpec.makeMeasureSpec(tempHeight,
                    MeasureSpec.EXACTLY);
        } else {
            height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        view.measure(width, height);
    }

topPadding设置上边距,控制头部里上边距的多少,换句话说就是控制head的显示和隐藏。

/**
     * 设置header 布局 上边距;
     *
     * @param topPadding
     */
    private void topPadding(int topPadding) {
        header.setPadding(header.getPaddingLeft(), topPadding,
                header.getPaddingRight(), header.getPaddingBottom());
        header.invalidate();
    }

初始化搞定之后,这时候的头部是隐藏了的。下面将设置点击事件,并和绘图结合。随事件发生而更改画面
在此,先设置一些标志。

    View header;// 顶部布局文件;
    int headerHeight;// 顶部布局文件的高度;
    int firstVisibleItem;// 当前第一个可见的item的位置;
    int scrollState;// listview 当前滚动状态;
    boolean isRemark;// 标记,当前是在listview最顶端按下的;
    int startY;// 按下时的Y值;

    int state;// 当前的状态;
    final int NONE = 0;// 正常状态;
    final int PULL = 1;// 提示下拉状态;
    final int RELESE = 2;// 提示释放状态;
    final int REFLASHING = 3;// 刷新状态;
    IReflashListener iReflashListener;//刷新数据的接口

这里我设置了下拉刷新的四个状态。

点击事件

   @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (firstVisibleItem == 0) {//表示listview已经在最顶端了,显示第0个item
                    isRemark = true;
                    startY = (int) ev.getY();
                }
                break;

            case MotionEvent.ACTION_MOVE:
                onMove(ev);
                break;
            case MotionEvent.ACTION_UP:
                if (state == RELESE) {
                    state = REFLASHING;
                    // 加载最新数据;
                    reflashViewByState();
                    iReflashListener.onReflash();
                } else if (state == PULL) {
                    state = NONE;
                    isRemark = false;
                    reflashViewByState();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }

手指按下时候记录一下当前的坐标,并标志isRemark为true表示按下了。
移动过程中调用onMove(ev)来操作各种移动操作。
手指离开时,判断一下当前的状态,如果是下拉完成了就加载数据,回调加载完成操作。
如果是下拉没完成,就恢复状态。
四个下拉状态都在onTouchEvent里完成标志,当然在移动过程中也会设置状态标志。

下拉的四个状态区分好之后,下面就是根据这些状态设置不同的动作了。
reflashViewByState这个方法设置了这四个状态不同动作。这里我设置了箭头翻转动作、文字变化和加载动作。

/**
     * 根据当前状态,改变界面显示;
     */
    private void reflashViewByState() {
        TextView tip = (TextView) header.findViewById(R.id.tip);
        ImageView arrow = (ImageView) header.findViewById(R.id.arrow);
        ProgressBar progress = (ProgressBar) header.findViewById(R.id.progress);
        //翻转180度
        RotateAnimation anim = new RotateAnimation(0, 180,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(500);//设置延时
        anim.setFillAfter(true);
        //再翻转180度,翻转回来
        RotateAnimation anim1 = new RotateAnimation(180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        anim1.setDuration(500);
        anim1.setFillAfter(true);
        switch (state) {
            case NONE:
                arrow.clearAnimation();
                topPadding(-headerHeight);
                break;

            case PULL:
                arrow.setVisibility(View.VISIBLE);
                progress.setVisibility(View.GONE);
                tip.setText("下拉可以刷新!");
                arrow.clearAnimation();
                arrow.setAnimation(anim1);
                break;
            case RELESE:
                arrow.setVisibility(View.VISIBLE);
                progress.setVisibility(View.GONE);
                tip.setText("松开可以刷新!");
                arrow.clearAnimation();
                arrow.setAnimation(anim);
                break;
            case REFLASHING:
                topPadding(50);
                arrow.setVisibility(View.GONE);
                progress.setVisibility(View.VISIBLE);
                tip.setText("正在刷新...");
                arrow.clearAnimation();
                break;
        }
    }

在移动的过程中,我们将根据移动的距离来设置状态标志,并且刷新画面。

/**
     * 判断移动过程操作;
     *
     * @param ev
     */
    private void onMove(MotionEvent ev) {
        if (!isRemark) {
            return;
        }
        int tempY = (int) ev.getY();//当前坐标
        int space = tempY - startY;//移动间距
        int topPadding = space - headerHeight;//移动距离和头部高度的差距
        switch (state) {
            case NONE:
                if (space > 0) {
                    state = PULL;
                    reflashViewByState();
                }
                break;
            case PULL:
                topPadding(topPadding);
                if (space > headerHeight + 30
                        && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
                    state = RELESE;
                    reflashViewByState();
                }
                break;
            case RELESE:
                topPadding(topPadding);
                if (space < headerHeight + 30) {//退回了
                    state = PULL;
                    reflashViewByState();
                } else if (space <= 0) {
                    state = NONE;
                    isRemark = false;
                    reflashViewByState();
                }//其他情况还是RELESE状态,不变。
                break;
        }
    }

在移动过程中状态是在NONE -> PULL -> RELESE 一步步加深,往下转移的。

上面还有些参数需要获取的,比如scrollState 和 firstVisibleItem 。它是在AbsListView.OnScrollListener 的回调中获取到的。

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

好了,现在下拉刷新操作基本完成,剩下就设置给外面调用的接口和方法。

回调接口

public void setInterface(IReflashListener iReflashListener){
        this.iReflashListener = iReflashListener;
    }
    /**
     * 刷新数据接口
     * @author Administrator
     */
    public interface IReflashListener{
        public void onReflash();
    }

reflashComplete,外面调用的方法。记录刷新完成后获取数据的时间。

/**
     * 获取完数据;
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    public void reflashComplete() {
        state = NONE;
        isRemark = false;
        reflashViewByState();
        TextView lastupdatetime = (TextView) header
                .findViewById(R.id.lastupdate_time);
        SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
        Date date = new Date(System.currentTimeMillis());
        String time = format.format(date);
        lastupdatetime.setText(time);
    }

再来个总体代码

package com.example.uidemo.listviewfrash.view;

import android.content.Context;
import android.icu.text.SimpleDateFormat;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.example.uidemo.R;

import java.util.Date;

/**
 * Created by Administrator on 2017/6/21.
 */

public class ReFlashListView extends ListView implements AbsListView.OnScrollListener {

    View header;// 顶部布局文件;
    int headerHeight;// 顶部布局文件的高度;
    int firstVisibleItem;// 当前第一个可见的item的位置;
    int scrollState;// listview 当前滚动状态;
    boolean isRemark;// 标记,当前是在listview最顶端摁下的;
    int startY;// 摁下时的Y值;

    int state;// 当前的状态;
    final int NONE = 0;// 正常状态;
    final int PULL = 1;// 提示下拉状态;
    final int RELESE = 2;// 提示释放状态;
    final int REFLASHING = 3;// 刷新状态;
    IReflashListener iReflashListener;//刷新数据的接口


    public ReFlashListView(Context context) {
        super(context);
        initView(context);
    }

    public ReFlashListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public ReFlashListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public ReFlashListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initView(context);
    }

    /**
     * 初始化界面,添加顶部布局文件到 listview
     *
     * @param context
     */
    private void initView(Context context) {
        LayoutInflater inflater = LayoutInflater.from(context);
        header = inflater.inflate(R.layout.header_layout, null);
        measureView(header);
        headerHeight = header.getMeasuredHeight();
        Log.i("tag", "headerHeight = " + headerHeight);
        topPadding(-headerHeight);
        this.addHeaderView(header);
        this.setOnScrollListener(this);
    }

    /**
     * 通知父布局,占用的宽,高;
     *
     * @param view
     */
    private void measureView(View view) {
        ViewGroup.LayoutParams p = view.getLayoutParams();
        if (p == null) {
            p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT);
        }
        int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
        int height;
        int tempHeight = p.height;
        if (tempHeight > 0) {
            height = MeasureSpec.makeMeasureSpec(tempHeight,
                    MeasureSpec.EXACTLY);
        } else {
            height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        }
        view.measure(width, height);
    }


    /**
     * 设置header 布局 上边距;
     *
     * @param topPadding
     */
    private void topPadding(int topPadding) {
        header.setPadding(header.getPaddingLeft(), topPadding,
                header.getPaddingRight(), header.getPaddingBottom());
        header.invalidate();
    }


    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (firstVisibleItem == 0) {
                    isRemark = true;
                    startY = (int) ev.getY();
                }
                break;

            case MotionEvent.ACTION_MOVE:
                onMove(ev);
                break;
            case MotionEvent.ACTION_UP:
                if (state == RELESE) {
                    state = REFLASHING;
                    // 加载最新数据;
                    reflashViewByState();
                    iReflashListener.onReflash();
                } else if (state == PULL) {
                    state = NONE;
                    isRemark = false;
                    reflashViewByState();
                }
                break;
        }
        return super.onTouchEvent(ev);
    }


    /**
     * 判断移动过程操作;
     *
     * @param ev
     */
    private void onMove(MotionEvent ev) {
        if (!isRemark) {
            return;
        }
        int tempY = (int) ev.getY();
        int space = tempY - startY;
        int topPadding = space - headerHeight;
        switch (state) {
            case NONE:
                if (space > 0) {
                    state = PULL;
                    reflashViewByState();
                }
                break;
            case PULL:
                topPadding(topPadding);
                if (space > headerHeight + 30
                        && scrollState == SCROLL_STATE_TOUCH_SCROLL) {
                    state = RELESE;
                    reflashViewByState();
                }
                break;
            case RELESE:
                topPadding(topPadding);
                if (space < headerHeight + 30) {
                    state = PULL;
                    reflashViewByState();
                } else if (space <= 0) {
                    state = NONE;
                    isRemark = false;
                    reflashViewByState();
                }
                break;
        }
    }

    /**
     * 根据当前状态,改变界面显示;
     */
    private void reflashViewByState() {
        TextView tip = (TextView) header.findViewById(R.id.tip);
        ImageView arrow = (ImageView) header.findViewById(R.id.arrow);
        ProgressBar progress = (ProgressBar) header.findViewById(R.id.progress);
        //翻转
        RotateAnimation anim = new RotateAnimation(0, 180,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        anim.setDuration(500);
        anim.setFillAfter(true);
        RotateAnimation anim1 = new RotateAnimation(180, 0,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f,
                RotateAnimation.RELATIVE_TO_SELF, 0.5f);
        anim1.setDuration(500);
        anim1.setFillAfter(true);
        switch (state) {
            case NONE:
                arrow.clearAnimation();
                topPadding(-headerHeight);
                break;

            case PULL:
                arrow.setVisibility(View.VISIBLE);
                progress.setVisibility(View.GONE);
                tip.setText("下拉可以刷新!");
                arrow.clearAnimation();
                arrow.setAnimation(anim1);
                break;
            case RELESE:
                arrow.setVisibility(View.VISIBLE);
                progress.setVisibility(View.GONE);
                tip.setText("松开可以刷新!");
                arrow.clearAnimation();
                arrow.setAnimation(anim);
                break;
            case REFLASHING:
                topPadding(50);
                arrow.setVisibility(View.GONE);
                progress.setVisibility(View.VISIBLE);
                tip.setText("正在刷新...");
                arrow.clearAnimation();
                break;
        }
    }

    /**
     * 获取完数据;
     */
    @RequiresApi(api = Build.VERSION_CODES.N)
    public void reflashComplete() {
        state = NONE;
        isRemark = false;
        reflashViewByState();
        TextView lastupdatetime = (TextView) header
                .findViewById(R.id.lastupdate_time);
        SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss");
        Date date = new Date(System.currentTimeMillis());
        String time = format.format(date);
        lastupdatetime.setText(time);
    }

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

    public void setInterface(IReflashListener iReflashListener){
        this.iReflashListener = iReflashListener;
    }
    /**
     * 刷新数据接口
     * @author Administrator
     */
    public interface IReflashListener{
        public void onReflash();
    }
}

效果图

下拉刷新

下拉刷新中

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/loongago/article/details/73179185

Android自定义控件下拉刷新上拉加载,所有View通用.(直接拿来用)

  • 2015年12月23日 10:07
  • 7.61MB
  • 下载

SimpleRefreshListView

  • 2017年10月15日 15:22
  • 8.89MB
  • 下载

再也不用担心下拉刷新,上拉加载啦!-自定义ListView对上拉刷新,上拉加载的详解

前言:       看过许多下拉刷新的例子,好多大牛们的代码写的很完美,让人羡慕嫉妒恨~~~,可是,对于下拉刷新时的手势操作却没有给出详细的解释,当一堆堆逻辑代码出来的时候,对于我们这些菜鸟来说,理解...
  • yyh448522331
  • yyh448522331
  • 2015-10-12 21:20:54
  • 2479

Android自定义ListView,轻松实现上下拉刷新,一看就懂,一学就会,超简单。

之前用别人的ListView,总是不能满足项目需求,故此特意研究一下自定义listview,和大家分享一下 ,简单易懂。...
  • u013790519
  • u013790519
  • 2015-09-24 17:20:23
  • 3344

自定义ListView下拉刷新上拉加载功能(面试)

ListView下拉刷新上拉加载的原理: 添加一个头和一个脚,然后对头和脚进行一些具体的处理,还要提供回调方法在外界进行具体的逻辑实现想要的功能。 对头进行的处理,即执行下拉加载: 在onTou...
  • smile0528
  • smile0528
  • 2016-08-26 21:04:15
  • 529

Android自定义View之(下拉刷新+侧滑删除)

以前项目中用到了一个放qq的侧滑删除的效果,结果github上一搜就copy了一个,不得不说大神们写的真心牛逼,那个时候呢看到一个东西能用就可以了,也不管怎么实现的,现在反过来一看,原来自定义还可以这...
  • vv_bug
  • vv_bug
  • 2016-08-22 22:09:37
  • 1428

自定义布局实现listview上拉加载下拉刷新

listview布局有时加载数据偏多,需要上拉加载第二页等更多数据,下拉刷新数据等功能,也有开源的框架XRefreshView ,可以参考http://www.w2bc.com/Article/442...
  • u013408979
  • u013408979
  • 2016-08-26 17:01:35
  • 834

ListView 下拉刷新的原理解析

1. 流程分析     下拉刷新最主要的流程是:     (1). 下拉,显示提示头部界面(HeaderView),这个过程提示用户"下拉刷新"     (2). 下拉到一定程度,超出了刷新最基...
  • chenqiuping_ls
  • chenqiuping_ls
  • 2017-02-09 11:52:45
  • 736

Android自定义控件——ListView的下拉刷新与上拉加载

无疑,在Android开发中,ListView是使用非常频繁的控件之一,ListView提供一个列表的容易,允许我们以列表的形式将数据展示到界面上,但是Google给我们提供的原生ListView的控...
  • lee_tianya
  • lee_tianya
  • 2014-10-10 20:14:21
  • 17482

基于RecyclerView通用适配自定义下拉刷新

天气热,人也变的懒散了,晚上如汗蒸,睡眠很不好,精神状态也不好。一、前言自从使用了RecyclerView,就爱上了它,灵活性非常的强大,效果绚丽,如果想进一步了解,请关注:RecyclerView ...
  • u012551350
  • u012551350
  • 2016-08-26 15:44:00
  • 4005
收藏助手
不良信息举报
您举报文章:View系列(1)--自定义一个ListView的下拉刷新
举报原因:
原因补充:

(最多只允许输入30个字)