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

一、概述

本篇续 厦门之旅 的第二篇。这期间找工作真的心态几多变化,刚开始兴致高昂,信心满满,面试了几家不错的公司,结果都是因为工资问题不了了之。后面的连面试机会都没有了,每天在狭小的租房里面吃了睡,睡了玩,陌生的环境消磨这我的意志。我很讨厌消沉的自我,这边招 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容