欢乐的票圈重构之旅——RecyclerView的上下拉以及logo的联动

项目重构的Git地址:
https://github.com/razerdp/FriendCircle/tree/main-dev
项目同步更新的文集:
http://www.jianshu.com/notebooks/3224048/latest

下集预告:欢乐的票圈重构之旅——RecyclerView的头尾布局增加

前言

在沉寂了五六个月的时间后,终于有空来收拾一下朋友圈项目的残局了。
这次换了一个服务器,毕竟咱们不是写后端的,当时一时头脑发热,开了一个阿里云,其实是为了毕业设计项目着想,后来实在吃不消108软妹币每个月的负担和代码的维护,于是无奈关掉服务器。
而现在,在平衡了一下LearnCloud和Bmob之后,打算采用Bmob作为我们项目的后端支持。
于是乎,在改造的过程中发现咱们的朋友圈项目似乎要大改,改着改着,干脆咬咬牙,全部推倒从来算了(写过的控件除外)。

所以,羽翼君又可以来简书更新一下文章(骗赞了←_←)。

Rv的上下拉

废话不多说了,直接进入主题。

这次重构因为将会从ListView换成RecyclerView,所以很多东西都要重新部署,比如上下拉。

因为朋友圈的特殊性,我们的上下拉需要符合至少两个条件:

  • 下拉刷新可以获取到偏移量(用来联动logo)
  • 下拉刷新时,可以隐藏刷新头部,而只展示我们的logo动画

对于懒惰的我来说,首当其冲还是找库吧。。。。结果找了一下,瞬间想哭了,因为要同时符合上面两个条件的,似乎还真的找不到。。。有一两个比较接近的(比如:IRecyclerView)却因为各种问题导致不能使用。。。

没办法,只好强撸了。。


先来一根烟压压惊

于是乎,在一次提交中,狂撸Touch事件。。。
commit here

提交部分截图

写着写着,想到了还得做动画,还得做返回,还得做各种各样的事件分发。。。。天呐噜,我还是乖乖去上班吧。。。

这时候忽然灵机一闪,想起以前撸ListView时不是有个overScroll的吗,那Rv也应会有的,于是面向谷歌编程的我,虽然找不到比较好的描述,但找到了这么一个库:
overscroll-decor

初步看了一下代码,其核心相当于接管了touch事件,通过setTranslationY来进行View的移动的,而且最重要的是,提供的接口有着状态和偏移量的返回!!!!(拍黑板,这是重点!

有了这两个东西,那就可以嘿嘿嘿了。

控件的布局

首先,我们确定一下我们的控件应该怎么写。

在微信朋友圈中,以我们的目测,至少有三个要求(本项目以iOS的交互为标准):

谜一样的截图
  • (1) logo要随着下拉的动作同时下拉
  • (2) RecyclerView拉下来之后,要露出后面的背景
  • (3) 咱们的logo是跟RecyclerView同级的

所以,咱们的布局肯定不能继承RecyclerView然后干,而是一个ViewGroup,这次我选择了FrameLayout。

所以咱们的初始化这么写:


  //构造器什么的,忽略啦~都指向于这里

  private void init(Context context) {
        //渐变背景(黑色的背景在上半部分,下半部分是白色的)
        GradientDrawable background = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xff323232, 0xff323232, 0xffffffff, 0xffffffff});
        setBackground(background);
        //rv初始化
        if (recyclerView == null) {
            recyclerView = new RecyclerView(context);
            recyclerView.setBackgroundColor(Color.WHITE);
            recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
        }
        //logo初始化
        if (refreshIcon == null) {
            refreshIcon = new ImageView(context);
            refreshIcon.setBackgroundColor(Color.TRANSPARENT);
            refreshIcon.setImageResource(R.drawable.rotate_icon);
        }
        FrameLayout.LayoutParams iconParam = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        iconParam.leftMargin = UIHelper.dipToPx(12);

        //add
        addView(recyclerView, RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT);
        addView(refreshIcon, iconParam);

        //触发刷新的警戒线
        refreshPosition = UIHelper.dipToPx(90);

        //logo的观察类
        iconObserver = new InnerRefreshIconObserver(refreshIcon, refreshPosition);

    }

接下来就是咱们的下拉刷新了。前面说过,咱么用的是overscroll那个库,我们针对的是偏移量,所以我们所有的工作都依赖于这个偏移:

private void initOverScroll() {
        IOverScrollDecor decor = new VerticalOverScrollBounceEffectDecorator(new RecyclerViewOverScrollDecorAdapter(recyclerView), 2f, 1f, 2f);
        decor.setOverScrollUpdateListener(new IOverScrollUpdateListener() {
            @Override
            public void onOverScrollUpdate(IOverScrollDecor decor, int state, float offset) {
                if (offset > 0) {
                    //正在刷新就不鸟它
                    if (currentStatus == REFRESHING) return;
                    //更新logo的位置
                    iconObserver.catchPullEvent(offset);
                    if (offset >= refreshPosition && state == STATE_BOUNCE_BACK) {
                        //state变成返回时,意味着已经松手了,则进行刷新逻辑
                        if (currentStatus != REFRESHING) {
                            setCurrentStatus(REFRESHING);
                            if (onRefreshListener != null) {
                                Log.i(TAG, "refresh");
                                onRefreshListener.onRefresh();
                            }
                            iconObserver.catchRefreshEvent();
                        }
                    }
                } else if (offset < 0) {
                    //底部的overscroll
                }
            }
        });
    }

代码不多,因为多的东西都在库里面干完了。。。

在调用了setAdapter之后,我们执行这个初始化方法,从回调的接口处,不难看到offset的回调有两种,分别是大于0和小于0,其中大于0是从顶部下拉(下拉刷新),而小于0则是从底部上拉(上拉加载)。

但是,有一个问题是,我们没有办法知道松手的触发,也就是相当于touch的up事件。不过幸好,接口同时还返回了状态,当状态发生改变的时候,就肯定是手势发生了变化,通过状态,我们就相当于捕捉到了up事件。所以就有了以上的代码。

因为朋友圈并不需要上拉加载,而是滑动到底部自动加载更多,所以这offset<0的地方我就没有做任何逻辑了,如果有需求的话,也是可以做到上拉加载更多的。

做完上下拉的逻辑之后,接下来就是logo的联动。

从代码上来看,我把所有的逻辑都封到了iconObserver里面了(其实我觉得起名叫iconHelper可能更好,但就是觉得Observer高大上一点←_←)。

在observer里面,我们主要做的东西都是跟UI有关的。代码比较简单,所有就把解释写到代码里面了

   /**
     * 刷新Icon的动作观察者
     */

    private static class InnerRefreshIconObserver {
        private ImageView refreshIcon;
        private final int refreshPosition;
        private float lastOffset = 0.0f;
        private RotateAnimation rotateAnimation;
        private ValueAnimator mValueAnimator;

        public InnerRefreshIconObserver(ImageView refreshIcon, int refreshPosition) {
            this.refreshIcon = refreshIcon;
            this.refreshPosition = refreshPosition;

            rotateAnimation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
            rotateAnimation.setDuration(600);
            rotateAnimation.setInterpolator(new LinearInterpolator());
            rotateAnimation.setRepeatCount(Animation.INFINITE);

        }

        public void catchPullEvent(float offset) {
            if (checkHacIcon()) {
                refreshIcon.setRotation(offset * 2);
                if (offset >= refreshPosition) {
                    offset = refreshPosition;
                }
                int resultOffset = (int) (offset - lastOffset);
                refreshIcon.offsetTopAndBottom(resultOffset);
                Log.d(TAG, "pull  >>  " + offset + "  resultOffset   >>>   " + resultOffset);
                adjustRefreshIconPosition();
                lastOffset = offset;
            }

        }

        /**
         * 调整icon的位置界限
         */
        private void adjustRefreshIconPosition() {
            if (refreshIcon.getTop() < 0) {
                refreshIcon.offsetTopAndBottom(Math.abs(refreshIcon.getTop()));
            } else if (refreshIcon.getTop() > refreshPosition) {
                refreshIcon.offsetTopAndBottom(-(refreshIcon.getTop() - refreshPosition));
            }
        }

        public void catchRefreshEvent() {
            if (checkHacIcon()) {
                refreshIcon.clearAnimation();
                refreshIcon.startAnimation(rotateAnimation);
            }
        }

        public void catchResetEvent() {
            refreshIcon.clearAnimation();
            if (mValueAnimator == null) {
                mValueAnimator = ValueAnimator.ofFloat(refreshPosition, 0);
                mValueAnimator.setInterpolator(new DecelerateInterpolator());
                mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        float result = (float) animation.getAnimatedValue();
                        catchPullEvent(result);
                    }
                });
                mValueAnimator.setDuration(300);
            }
            mValueAnimator.start();
        }

        private boolean checkHacIcon() {
            return refreshIcon != null;
        }
    }

最后是demo动图:

demo

本篇比较简单,算是一个开始吧,接下来的重构咱么就愉快地进行吧-V-

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

推荐阅读更多精彩内容