仿【咪咕动漫】列表下拉刷新上拉加载

一、概述

本篇续 厦门之旅 的第二篇。这期间找工作真的心态几多变化,刚开始兴致高昂,信心满满,面试了几家不错的公司,结果都是因为工资问题不了了之。后面的连面试机会都没有了,每天在狭小的租房里面吃了睡,睡了玩,陌生的环境消磨这我的意志。我很讨厌消沉的自我,这边招 Android 开发并没有我以为的那么多,实在是太少了,想找到满意的工作更是难上加难。引用公众号【AndroidDeveloper】的一句话

愿意积极争取,肯努力上进的年轻人,运气不会太差

在我快要放弃在这边找工作的时候,收到了一家公司的面试通知,并顺利的拿到了 offer,他们公司旗下有一款 咪咕动漫 的产品。本篇我以自己的方式实现了 App 中列表的下拉刷新以及上拉加载。

二、效果展示

效果图一栏:

refresh

三、具体实现

1、图片资源

下载 咪咕动漫App ,修改成 .zip 格式并解压。获取到图片资源如下:

pullrefresh

pull_down (下拉)

pullrelease

pull_end (释放刷新)

refreshing1

refreshing_01 (正在刷新图片_1)

refreshing2

refreshing_02 (正在刷新图片_2)

refreshing3

refreshing_03 (正在刷新图片_3)

正在刷新的帧动画:

<?xml version="1.0" encoding="utf-8"?>
<animation-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:oneshot="false">

    <item android:duration="100" android:drawable="@drawable/refreshing_01" />
    <item android:duration="100" android:drawable="@drawable/refreshing_02" />
    <item android:duration="100" android:drawable="@drawable/refreshing_03" />

</animation-list>

属性 android:oneshot=”false” 表示动画循环播放。 如果为 true,表示动画只播放一次停止在最后一帧上。

2、下拉刷新

原理浅析

下拉刷新作为一个单独控件添加到列表顶部,并且初始状态的高度为 0 ,随着手指触摸的偏移量高度而发生改变,并且在不同的状态之间来回切换。控件的四种状态:

  • STATE_NORMAL 下拉状态 (高度小于刷新的临界高度) 默认 40dp

  • STATE_RELEASE_TO_REFRESH 释放刷新状态 (高度大于刷新的临界高度)

  • STATE_REFRESHING 刷新状态 (高度大于刷新的临界高度,手指释放后的状态)

  • STATE_DONE 刷新完成状态

处了四种状态,还需要实现三个方法:

  • void onMove(float delta); 移动,参数 delta 两点之间的偏移量

  • boolean releaseAction(); 释放是否满足刷新状态

  • void refreshComplete(); 刷新完成

刷新控件(MiGuRefreshHeader)

MiGuRefreshHeader 控件继承 LinearLayout ,MiGuRefreshHeader 的构造方法:

    public MiGuRefreshHeader(Context context) {
        this(context, null);
    }

    public MiGuRefreshHeader(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MiGuRefreshHeader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

初始化 View,比较简单我这里就不在细讲,文章最后会附上源码:

    private void initView() {

        // 初始情况,设置下拉刷新view高度为0
        mContainer = (LinearLayout) LayoutInflater.from(getContext()).inflate(R.layout.refresh_header, null);
        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        lp.setMargins(0, 0, 0, 0);
        this.setLayoutParams(lp);
        this.setPadding(0, 0, 0, 0);

        addView(mContainer, new LayoutParams(LayoutParams.MATCH_PARENT, 0));
        setGravity(Gravity.BOTTOM);

        //图片控件
        mMiGuImageView = (ImageView) findViewById(R.id.iv_refresh);
        //文本控件
        mStatusTextView = (TextView) findViewById(R.id.tv_status);

        //获取控件的默认高度方法一
        measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        mMeasureHeight = getMeasuredHeight();//获取控件高度 默认 40dp 由于测试机密度为 3 所以像素为 120px
    }

获取控件的默认高度方式二:

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mMeasureHeight = h;
    }

不同状态下文本控件,图片控件的显示:

    public void setState(int state) {
        if (state == mState) return;

        if (state == STATE_NORMAL) {
            //下拉
            mMiGuImageView.setImageResource(R.drawable.pull_down);
            mStatusTextView.setText(R.string.pull_refresh);
        } else if (state == STATE_RELEASE_TO_REFRESH) {
            //释放
            mMiGuImageView.setImageResource(R.drawable.pull_end);
            mStatusTextView.setText(R.string.release_refresh);
        } else if (state == STATE_REFRESHING) {
            //刷新
            mStatusTextView.setText(R.string.refreshing);
            mMiGuImageView.setImageResource(R.drawable.refreshing);
            mMiGuDrawable = (AnimationDrawable) mMiGuImageView.getDrawable();
            //播放动画
            mMiGuDrawable.start();

            smoothScrollTo(mMeasureHeight);
        } else if (state == STATE_DONE) {
            //完成
            if (mMiGuDrawable != null)
                //停止动画
                mMiGuDrawable.stop();
        }

        mState = state;
    }

如果你需要替换文本或图片,请修改这里。

以下是3个方法的实现,onMove(float delta) 方法:

        if (getVisibleHeight() > 0 || delta > 0) {
            //控件滑动的距离
            setVisibleHeight((int) delta + getVisibleHeight());
            //处于释放刷新状态
            if (mState <= STATE_RELEASE_TO_REFRESH) {
                //判定距离是否大于刷新的临界值
                if (getVisibleHeight() < mMeasureHeight) {
                    setState(STATE_NORMAL);
                } else {
                    setState(STATE_RELEASE_TO_REFRESH);
                }
            }
        }

releaseAction() 方法:

    @Override
    public boolean releaseAction() {

        boolean isOnRefresh = false;

        int height = getVisibleHeight();

        if (height == 0) {
            isOnRefresh = false;
        }

        if (getVisibleHeight() > mMeasureHeight && mState < STATE_REFRESHING) {
            //刷新状态
            setState(STATE_REFRESHING);
            isOnRefresh = true;
        }

        if (mState == STATE_REFRESHING && height <= mMeasureHeight) {
            //处于刷新状态,手指还在向上滑动
        }

        if (mState != STATE_REFRESHING) {
            smoothScrollTo(0);
        }

        if (mState == STATE_REFRESHING) {
            int destHeight = mMeasureHeight;
            smoothScrollTo(destHeight);
        }

        return isOnRefresh;
    }

刷新完成 refreshComplete() 方法:

    @Override
    public void refreshComplete() {
        setState(STATE_DONE);
        reset();
    }

理解了刷新控件的四种状态,再来分析代码就比较容易了。接着我们处理 RecyclerView 的 onTouchEvent 方法获取 Y 轴的偏移量作为参数传入 onMove(float delta) 方法中。

MiGuRecyclerView

MiGuRecyclerView 继承 RecyclerView 控件。主要分析 onTouchEvent 方法,如果你对其他地方还有疑问,请留言。

注意:本文使用的 RecyclerView 基于RecyclerView 之通用适配,重写 setAdapter 方法添加头部刷新控件:

 mRefreshAdapter.addHeaderView(mMiGuRefreshHeader, 0);

onTouchEvent 方法代码如下:

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (mLastY == -1) {
            mLastY = ev.getRawY();
        }

        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mLastY = ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (mRefreshAdapter != null) {
                    final float deltaY = ev.getRawY() - mLastY;
                    mLastY = ev.getRawY();
                    if (isScrollTop && isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) {
                        mMiGuRefreshHeader.onMove(deltaY / DRAG_RATE);
                        if (mMiGuRefreshHeader.getVisibleHeight() > 0 && mMiGuRefreshHeader.getState() < MiGuRefreshHeader.STATE_REFRESHING) {
                            return false;
                        }
                    }
                }
                break;
            default:
                if (mRefreshAdapter != null) {
                    mLastY = -1; // reset
                    if (isScrollTop && isOnTop() && pullRefreshEnabled && appbarState == AppBarStateChangeListener.State.EXPANDED) {
                        if (mMiGuRefreshHeader.releaseAction()) {
                            if (mRefreshListener != null) {
                                mRefreshListener.onRefresh();
                            }
                        }
                    }
                }
                break;
        }

        return super.onTouchEvent(ev);
    }

ACTION_DOWN 手势获取相对于屏幕触摸点 Y 坐标

    final float deltaY = ev.getRawY() - mLastY;
    mLastY = ev.getRawY();

获取手势滑动两点的偏移量,isScrollTop 判定当前 RecyclerView 是否滑动到顶部。我采取了重写 onScrolled 方法通过:

findFirstCompletelyVisibleItemPosition();

来判定 isScrollTop 值,如果您有好的方案可以给我留言,万分感谢。

3、上拉加载

【咪咕动漫】采取的是播放帧动画,比较简单。实现方案你可以参考源码。

技术交流群欢迎你的加入

qq

源码传送门

参考文献

http://www.open-open.com/lib/view/open1474266969512.html

http://blog.csdn.net/xk632172748/article/details/53939161

猜你喜欢

转载自blog.csdn.net/u012551350/article/details/68951536