歡樂的票圈重構之旅——RecyclerView的上下拉以及logo的聯動

羽翼君發表於2018-01-09

專案重構的Git地址:https://github.com/razerdp/FriendCircle

下集預告:歡樂的票圈重構之旅——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-

相關文章