Android高效率实现自定义下拉刷新,仿QQ效果

转载请注明出处:http://blog.csdn.net/jakeyangchina/article/details/53224478

不知不觉来北京工作快到一年了,从来也没养成写博客的习惯,平时自己总结完知识点,间隔几天之后忘记的也差不多了,从新翻看总结的文档,找起来麻烦,于是乎找到了最合理的方案,那就是写博客记录下来,我要坚持总结干货,定期写博客


初步打算分四大类:

  1. Android基础篇
  2. 自定义模块
  3. 功能模块
  4. 问题总结

希望能和大家共同学习,一起进步,第一篇博客就从项目中经常用到的小模块下拉刷新功能开始,记得当时为了图省事直接从网上找现成的,本以为很顺利,导致测试妹子过来找我聊聊,一咬牙决定自己写个模块,于是乎有了今天的内容


打开QQ界面反复下拉刷新,发现偶尔会出现Bug,决定仿QQ效果写个Demo,只是用于讲解演示用,没有优化,写Demo时遇到了很多坑,修复了各种bug,废话不多说了,开始动起来:


原理图:
原理图


主要步骤如下:

  • 创建下拉布局文件header_item.xml
  • 自定义类PullAndUpToRefreshListView继承ListView
  • 初始化布局,将下拉布局文件添加到ListView头中
  • 覆写onTouchEvent
  • 定义回调接口RefreshItemClickListener监听条目点击事件
  • 定义回调接口OnRefreshCompleteListener监听下拉刷新状态

遇到的主要问题:

  1. 当手指按下不松开,快速反复上下滑动,ListView内部条目也会随之滑动,头布局遮挡条目
  2. 当手指向下刷新不松开,同时再向上滑动,将头文件滑动到不可见,同时再继续向上滑动,松开手,再次点击向下滑动到头文件可见继续上下反复滑动这时会出现跳动现象,产生的原因:再次点击时无法获取down事件,解决办法:添加判断,过滤掉不符合条件,首次保存motionEvent.getY()坐标,详细看代码注解
  3. 当点击下拉时,同时条目会执行点击事件,产生的原因:当点击条目下拉时默认为条目点击事件,解决办法:自定义条目监听事件,通过判断禁用掉系统点击事件

效果展示:
效果展示


代码展示:

资源文件:strings.xml
<resources>
    <string name="app_name">RefreshView</string>
    <string name="pull_to_refresh">下拉刷新</string>
    <string name="release_to_refresh">松手刷新</string>
    <string name="refresh_to_refresh">正在刷新...</string>
</resources>

资源文件中定义状态文字

自定义类文件:PullAndUpToRefreshListView.java
类中字段声明:
    /**
     * 显示加载文字
     */
    private TextView textView;

    /**
     * 显示箭头
     */
    private ImageView imageView_arrow;

    /**
     * 显示进度条
     */
    private ProgressBar progressBar;

    /**
     * 下拉状态
     */
    private final int pull_state = 0;

    /**
     * 释放状态
     */
    private final int release_state = 1;

    /**
     * 正在刷新状态
     */
    private final int refresh_state = 2;

    /**
     * 当前状态
     */
    private int current_state = pull_state;//初始化当前状态

    /**
     * 让加载布局时执行一次,标识
     */
    private boolean isFirst = false;

    /**
     * 下拉头的高度
     */
    private int hideHeaderHeight;

    /**
     * 手指按下时的屏幕纵坐标
     */
    private float yDown;

    /**
     * 手指移动时的屏幕纵坐标
     */
    private float yMove;

    /**
     * 每次下拉加载完后再允许执行下次请求
     */
    private boolean isFlag = false;

    /**
     * 防止首次点击条目下拉时,条目执行点击事件
     */
    private boolean isItemClick = false;

    /**
     *  防止当向下拉动同时在将手指向上滑动,使firstChild.getTop()<0后松开,再次触屏下滑时没有down坐标点
     */
    private boolean  isDownY = false;

    /**
     * 记录移动距离
     */
    private float moveDistance = 0;

    /**
     * 刷新头布局视图
     */
    private View views;
构造函数:
public PullAndUpToRefreshListView(Context context) {
        super(context);
        //初始化
        init(context);
    }

    public PullAndUpToRefreshListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //初始化
        init(context);
    }

    public PullAndUpToRefreshListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化
        init(context);
    }

构造函数中调用自定义方法,初始化布局

init()方法:
    /**
     * 初始化布局
     * @param context
     */
    private void init(Context context) {
        //加载刷新头布局文件
        views = View.inflate(getContext(), R.layout.header_item,null);

        //显示文字状态
        textView = (TextView) views.findViewById(R.id.description);

        //箭头图片
        imageView_arrow = (ImageView) views.findViewById(R.id.arrow);

        //进度条
        progressBar = (ProgressBar) views.findViewById(R.id.progress_bar);

        //添加条目添加点击事件
        setOnItemClickListener(this);

        //添加刷新头文件到ListView
        addHeaderView(views);
    }

上述方法主要初始化控件,给ListView添加条目点击事件,防止下拉条目时触发条目执行点击事件,通过判断isItemClick状态设置条目是否可点击,实现自定义条目点击监听接口。

覆写onTouchEvent方法:
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(isPullRefresh(ev)) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:

                    //每次点击屏幕时,恢复条目可点击
                    isItemClick = false;
                    yDown = ev.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    yMove = ev.getY();
                    int distance = (int) (yMove - yDown);

                    //当状态为正在刷新时,再次向下拉时,在原位置上接着往下移动
                    if(current_state == refresh_state) {
                        moveDistance = - hideHeaderHeight;
                    }
                    moveDistance = distance + moveDistance;

                    //计算新paddingTop值
                    int newDistance = hideHeaderHeight + (int)moveDistance;

                    //int newDistance = hideHeaderHeight + distance;
                    //判断条件,listView第一个条目可见,并且paddingTop值大于等于hideHeaderHeight
                    if(getFirstVisiblePosition() == 0 && newDistance >= hideHeaderHeight) {

                        //设置值
                        views.setPadding(views.getPaddingLeft(),newDistance,
                                views.getPaddingRight(),views.getPaddingBottom());

                        if(newDistance >= 0) {
                            //设置条目不可点击
                            isItemClick = true;
                            //更改状态
                            current_state = release_state;
                            //显示文字
                            showTextContent(current_state);
                        }else if(newDistance < 0) {

                            //设置条目可点击
                            isItemClick = false;
                            current_state = pull_state;
                            //显示文字
                            showTextContent(current_state);
                        }
                        setPressed(false);
                        //记录前一次的移动的坐标
                        yDown = yMove;
                        //不让ListView滚动
                        return true;
                    }

                    break;
                case MotionEvent.ACTION_UP:
                    if(current_state == release_state) {
                        //当状态是释放,显示
                        views.setPadding(views.getPaddingLeft(), 0, views.getPaddingRight(),
                                views.getPaddingBottom());

                        //更改状态
                        current_state = refresh_state;
                        //显示文字
                        showTextContent(current_state);

                    }else {
                        //当状态是下拉,隐藏
                        views.setPadding(views.getPaddingLeft(), hideHeaderHeight,
                                views.getPaddingRight(),views.getPaddingBottom());
                    }
                    moveDistance = 0;
                    if(isItemClick) {
                        setOnItemClickListener(null);
                    }else {
                        setOnItemClickListener(this);
                    }

                    //当状态是刷新,通过回调提供给调用者,更新数据
                    if(current_state == refresh_state && !isFlag) {
                        isFlag = true;
                        if(listener != null) {
                            new Thread(new Runnable() {
                                @Override
                                public void run() {
                                    listener.onComplete(PullAndUpToRefreshListView.this);
                                    //完成一次刷新后,将状态设置为下拉刷新
                                    current_state = pull_state;
                                    isFlag = false;
                                    //设置条目可点击
                                    isItemClick = true;
                                }
                            }).start();
                        }
                    }
                    break;
                default:
                    break;
            }
        }
        return super.onTouchEvent(ev);
    }

通过isPullRefresh(ev)方法过滤掉不符合要求的滑动,如果是向下滑动就返回true

在down事件中主要操作:

  1. 当手指按下屏幕时通过设置isItemClick让ListView条目不可点击,这样做的原因防止点击条目向下滑动时ListView会误执行点击事件
  2. 通过yDown = ev.getY();获取手指按下屏幕坐标点

在move事件中主要操作:

  1. yMove作用是记录每次手指移动时Y轴方向的坐标点
  2. distance作用是记录手指每次移动的距离
  3. moveDistance作用是为了实现当正在处于刷新状态时,再次点击屏幕向下滑动,在原来位置上继续能向下移动,当current_state == refresh_state时将moveDistance赋值为头布局的高度
  4. newDistance作用是用于方便判断位置,有两种状态:①大于hideHeaderHeight同时小于0时②大于0,当为0时正好完全显示出来
  5. 通过newDistance给当前状态赋值,三种状态:release_state松手刷新,refresh_state正在刷新,pull_state向下刷新

在up事件中主要操作:

  1. 松开手时如果当前状态为release_state这时需要显示出刷新头,当前状态赋值为正在刷新
  2. 通过isItemClick控制条目是否可点击
  3. 执行回调方法提供给调用者
    /**
     *  用于判断是否符合下拉条件
     * @return
     */
    private boolean  isPullRefresh(MotionEvent motionEvent) {

        View firstChild = getChildAt(0);
        if (firstChild != null) {
            if (getFirstVisiblePosition() == 0 && firstChild.getTop() == 0) {
                //防止当向下拉同时在将手指向上滑动
                // 使firstChild.getTop()<0后松开,再次触屏下滑时没有记录按下坐标点
                if(!isDownY) {
                    //重要
                    yDown = motionEvent.getY();
                }
                isDownY = true;
                //当ListView第一个条目可见同时top值为0时允许下拉
                return true;
            } else {
                isDownY = false;

                //当不满足条件时,隐藏
                views.setPadding(views.getPaddingLeft(), hideHeaderHeight,
                        views.getPaddingRight(),views.getPaddingBottom());

                //设置条目可点击
                setOnItemClickListener(this);
                return false;
            }
        } else {
            isDownY = false;
            setOnItemClickListener(this);
            // 如果ListView为null可以下拉
            return true;
        }
    }

这个方法主要是过滤掉不符合下拉动作,如果不符合下拉动作此方法返回false将不会执行onTouchEvent方法内switch条件语句

这里需要注意isDownY的作用,单独记录每次按下时Y轴方向的坐标,解决当手指向下刷新不松开,同时再向上滑动,将头文件滑动到不可见,同时再继续向上滑动,松开手,再次点击向下滑动到头文件可见继续上下反复滑动这时会出现跳动现象

    /**
     * 更改显示文字
     * @param state
     */
    private void showTextContent(int state) {
        if(state != preState) {
            //记录上一次状态
            preState = state;
            float pivotX = imageView_arrow.getWidth() / 2f;
            float pivotY = imageView_arrow.getHeight() / 2f;
            Log.i("stateaaaaa","======");
            switch (state) {
                case pull_state:
                    textView.setText(getResources().getString(R.string.pull_to_refresh));
                    progressBar.setVisibility(ProgressBar.INVISIBLE);
                    imageView_arrow.setVisibility(ImageView.VISIBLE);

                    //设置旋转动画
                    RotateAnimation pullAnimation =  new RotateAnimation(-180f,-360f,pivotX,pivotY);
                    pullAnimation.setDuration(200);
                    pullAnimation.setFillAfter(true);
                    imageView_arrow.startAnimation(pullAnimation);

                    break;
                case release_state:
                    textView.setText(getResources().getString(R.string.release_to_refresh));
                    progressBar.setVisibility(ProgressBar.INVISIBLE);
                    imageView_arrow.setVisibility(ImageView.VISIBLE);

                    //设置旋转动画
                    RotateAnimation releaseAnimation =  new RotateAnimation(0f,-180f,pivotX,pivotY);
                    releaseAnimation.setDuration(200);
                    releaseAnimation.setFillAfter(true);
                    imageView_arrow.startAnimation(releaseAnimation);

                    break;
                case refresh_state:
                    textView.setText(getResources().getString(R.string.refresh_to_refresh));
                    progressBar.setVisibility(ProgressBar.VISIBLE);
                    imageView_arrow.setVisibility(ImageView.INVISIBLE);

                    //清空动画
                    imageView_arrow.clearAnimation();
                    //显示进度条
                    progressBar.setVisibility(ProgressBar.VISIBLE);
                    //隐藏箭头
                    imageView_arrow.setVisibility(ImageView.INVISIBLE);

                    break;
                default:
                    break;
            }
            //将状态返回给调用者
            if(listener != null) {
                listener.onCurrentState(current_state);
            }
        }
    }

这个方法通过状态执行动画,设置文字显示

state != preState目的是当有重复状态连续执行时,只需要执行一次if语句内代码,提高性能

    @Override
    public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {

        if(listenerClick != null) {
            listenerClick.onItemClick(adapterView,view,i,l);
        }
    }

在这此方法中调用自己定义的条目点击回调方法,提供给调用者

    /**
     * 完成下拉后,初始化状态
     */
    public void finishRefreshing() {
        views.setPadding(0,hideHeaderHeight,0,0);
        //隐藏进度条
        progressBar.setVisibility(ProgressBar.INVISIBLE);
        //显示箭头
        imageView_arrow.setVisibility(ImageView.VISIBLE);
    }

当下拉结束后一定要回调此方法结束下拉状态

接下来我们来看下程序的主入口Activity中调用:

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(R.layout.activity_main);
        //初始化控件
        listView = (PullAndUpToRefreshListView) findViewById(R.id.refresh);

        listView.setOnRefreshItemClickListener(new PullAndUpToRefreshListView.RefreshItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Log.i("nnnnnnnnn","onItemClick"+":"+i);
            }
        });


        listView.setOnRefreshCompleteListener(new PullAndUpToRefreshListView.OnRefreshCompleteListener() {
            @Override
            public void onComplete(PullAndUpToRefreshListView refresh) {
                SystemClock.sleep(5000);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        //刷新结束,需要调用此方法
                        listView.finishRefreshing();
                    }
                });
            }

            @Override
            public void onCurrentState(int currentState) {
                //获取当前状态
                Log.i("currentState",""+currentState);
            }
        });

如果大家还有什么疑问,请在下方留言。

如何有不足的地方希望大家指出来,共同学习。

源码下载,请点击这里!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值