[Digging] 支付寶首頁交互三部曲 3 實(shí)現(xiàn)支付寶首頁交互

cover_3

博客原文:kyleduo.com

前言

這個系列源自前幾天看到一篇使用CoordinatorLayout實(shí)現(xiàn)支付寶首頁效果的文章销部,下載看了效果和源碼,不敢茍同亚隙,所以打算自己動手宾抓。實(shí)現(xiàn)的過程有點(diǎn)曲折抑月,但也發(fā)現(xiàn)了一些有意思的事情树叽,用三篇文章來記錄并分享給大家。

  • CoordinatorLayout和Behavior
  • 自定義CoordinatorLayout.Behavior
  • 支付寶首頁效果實(shí)現(xiàn)

文中:CoL代表CoordinatorLayout谦絮,ABL表示AppBarLayout题诵,CTL表示CollapsingToolbarLayout,SRL表示SwipeRefreshLayout层皱,RV表示RecyclerView性锭。

源碼:Github

先看下最終效果:

跳轉(zhuǎn)到優(yōu)酷

效果分析

支付寶首頁基本可以看成4個部分:

alipay_home_struct

折疊時QuickAction部分折疊,繼續(xù)向上滑動叫胖,GridMenu移出屏幕草冈。下拉時,刷新動畫出現(xiàn)在GridMenu和MessageList之間瓮增。

結(jié)構(gòu)設(shè)計

前一部分只是分析了一下結(jié)構(gòu)怎棱,這里就要開始設(shè)計了。為了實(shí)現(xiàn)QuickAction折疊的效果钉赁,其實(shí)有好幾種設(shè)計方法:

  1. 除SearchBar外蹄殃,剩下的部分均使用RecyclerView實(shí)現(xiàn)携茂。
  2. SearchBar和QuickAction作為Header你踩,其他部分使用RecyclerView實(shí)現(xiàn)。
  3. SearchBar讳苦、QuickAction带膜、GridMenu作為Header,MessageList使用RecyclerView實(shí)現(xiàn)鸳谜。
  4. ……

為了方便擺放下拉刷新的位置膝藕,我選擇了第三種結(jié)構(gòu),同時使用SwipeRefreshLayout實(shí)現(xiàn)下拉刷新咐扭,這種結(jié)構(gòu)也是為了方便替換成其他下拉刷新控件芭挽。看下最終的實(shí)現(xiàn)效果:

視頻

除了下拉刷新效果使用了SwipeRefreshLayout以及在GridMenu和QuickAction位置下拉不能觸發(fā)下拉刷新外蝗肪,其他的交互效果都和支付寶無異袜爪。

在開始動手之前,我還查看了支付寶的實(shí)現(xiàn)方法薛闪,很意外的是支付寶是使用ListView實(shí)現(xiàn)的這個頁面辛馆,除了SearchBar,其他部分均為ListView豁延。

alipay-home-uiviewer

實(shí)現(xiàn)

我們的效果實(shí)際上和AppBarLayout有很多相似之處昙篙,通過上篇文章腊状,我們知道了AppBarLayout使用的兩個Behavior使用了3個基類,如果能用就好了苔可。不過這三個基類的訪問權(quán)限是包可見缴挖,所以只好從Support中拷出來使用了。還有些步驟需要修改基類中的方法焚辅,以及增加方法可見性醇疼。

APHeaderView

APHeaderView包括除MessageList以外的其他部分,要實(shí)現(xiàn)的大致相當(dāng)于AppBarLayout和CollapsingToolbarLayout結(jié)合的效果法焰。

APHeaderView繼承自ViewGroup秧荆。

首先在onFinishInflate()方法中獲取子View的引用,mBar是SearchBar部分埃仪,mSnapView是QuickAction部分乙濒,mScrollableViews是其余的View。之所以沒有使用上面的命名方式卵蛉,是因?yàn)槲也幌氚堰@個效果限制的那么死颁股,這些變量就以他們的功能命名了。

@Override
protected void onFinishInflate() {
    super.onFinishInflate();
    final int childCount = getChildCount();
    if (childCount < 2) {
        throw new IllegalStateException("Child count must >= 2");
    }
    mBar = findViewById(R.id.alipay_bar);
    mSnapView = findViewById(R.id.alipay_snap);
    mScrollableViews = new ArrayList<>();
    for (int i = 0; i < childCount; i++) {
        View v = getChildAt(i);
        if (v != mBar && v != mSnapView) {
            mScrollableViews.add(v);
        }
    }
    mBar.bringToFront();
}

最后一行語句將mBar移至頂部傻丝,這樣可以一直顯示甘有。

布局部分沒啥好說的,實(shí)現(xiàn)的是類似LinearLayout的布局葡缰,子View順次排列亏掀。偏移量的處理并不在此處。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    if (heightSize == 0) {
        heightSize = Integer.MAX_VALUE;
    }

    int height = 0;

    final int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
        View c = getChildAt(i);
        measureChildWithMargins(
                c,
                MeasureSpec.makeMeasureSpec(widthSize - getPaddingLeft() - getPaddingRight(), MeasureSpec.EXACTLY),
                0,
                MeasureSpec.makeMeasureSpec(heightSize - getPaddingTop() - getPaddingBottom(), MeasureSpec.AT_MOST),
                height
        );
        height += c.getMeasuredHeight();
    }

    height += getPaddingTop() + getPaddingBottom();

    setMeasuredDimension(
            widthSize,
            height
    );
}

@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    int childTop = getPaddingTop();
    int childLeft = getPaddingLeft();
    mBar.layout(childLeft, childTop, childLeft + mBar.getMeasuredWidth(), childTop + mBar.getMeasuredHeight());
    childTop += mBar.getMeasuredHeight();

    mSnapView.layout(childLeft, childTop, childLeft + mSnapView.getMeasuredWidth(), childTop + mSnapView.getMeasuredHeight());

    childTop += mSnapView.getMeasuredHeight();

    for (View sv : mScrollableViews) {
        sv.layout(childLeft, childTop, childLeft + sv.getMeasuredWidth(), childTop + sv.getMeasuredHeight());
        childTop += sv.getMeasuredHeight();
    }
}

滾動區(qū)域是控制滾動的重要部分泛释,這里涉及到兩個方法滤愕。getScrollRange返回總的可滾動區(qū)域,getSnapRange返回折疊效果的區(qū)域怜校。

public int getScrollRange() {
    int range = mSnapView.getMeasuredHeight();
    if (mScrollableViews != null) {
        for (View sv : mScrollableViews) {
            range += sv.getMeasuredHeight();
        }
    }
    return range;
}

private int getSnapRange() {
    return mSnapView.getHeight();
}

APHeaderView.Behavior

繼承自HeaderBehavior间影,和AppBarLayout一樣,天生自帶Offset處理和Touch事件處理茄茁,需要實(shí)現(xiàn)的魂贬,是NestedScrolling和snap效果。比AppBarLayout更多的裙顽,APHeaderView.Behavior實(shí)現(xiàn)了精確地Fling效果付燥。也就是說Fling效果和RecyclerView也是聯(lián)動的。這里主要說一下我是怎么處理Fling效果的锦庸。

Header -> ScrollingView

fling效果的實(shí)現(xiàn)机蔗,是通過Scroller不斷修改偏移量最終呈現(xiàn)出連貫的動畫。如果不處理fling效果,結(jié)果就是ScrollingView在fling到頂端時萝嘁,出現(xiàn)overscroll效果梆掸,也就是剩余了部分偏移量沒有消費(fèi)。所以牙言,要實(shí)現(xiàn)fling的聯(lián)動酸钦,就是消費(fèi)多余的偏移量

fling可能在兩個方法中觸發(fā)咱枉,一個是onTouchEvent卑硫,還有就是onNestedPreFling。我們在onNestedPreFling方法中蚕断,判斷如果是向上滑動欢伏,就手動調(diào)用fling方法,和onTouchEvent一致亿乳。

@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, APHeaderView child, View target, float velocityX, float velocityY) {
    if (velocityY > 0 && getTopAndBottomOffset() > -child.getScrollRange()) {
        fling(coordinatorLayout, child, -child.getScrollRange(), 0, -velocityY);
        mWasFlung = true;
        return true;
    }
    return false;
}

HeaderBehavior類中的Scroller回調(diào)硝拧,最終調(diào)用setHeaderTopAndBottomOffset方法設(shè)置偏移量:

@Override
public void run() {
    if (mLayout != null && mScroller != null) {
        if (mScroller.computeScrollOffset()) {
            setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
            // Post ourselves so that we run on the next animation
            ViewCompat.postOnAnimation(mLayout, this);
        } else {
            onFlingFinished(mParent, mLayout);
        }
    }
}

APHeaderView.Behavior的實(shí)現(xiàn),就是覆寫這個方法葛假,將沒有消費(fèi)的偏移量分發(fā)出去障陶。我們先看覆寫的fling方法:如果判斷向上滑動,除了設(shè)置標(biāo)記為為true聊训,同時會修改邊界值:

@Override
protected boolean fling(CoordinatorLayout coordinatorLayout, APHeaderView layout, int minOffset, int maxOffset, float velocityY) {
    int min = minOffset;
    int max = maxOffset;
    if (velocityY < 0) {
        // 向上滾動
        mShouldDispatchFling = true;
        mTempFlingDispatchConsumed = 0;
        mTempFlingMinOffset = minOffset;
        mTempFlingMaxOffset = maxOffset;
        min = Integer.MIN_VALUE;
        max = Integer.MAX_VALUE;
    }
    return super.fling(coordinatorLayout, layout, min, max, velocityY);
}

修改邊界值是因?yàn)槲覀兿M词惯_(dá)到邊界抱究,fling效果依然不能停止,因?yàn)槲覀円讯嘤嗟钠屏吭俅畏职l(fā)給ScrollingView带斑。

@Override
public int setHeaderTopBottomOffset(CoordinatorLayout parent, APHeaderView header, int newOffset, int minOffset, int maxOffset) {
    final int curOffset = getTopAndBottomOffset();
    final int min;
    final int max;
    if (mShouldDispatchFling) {
        min = Math.max(mTempFlingMinOffset, minOffset);
        max = Math.min(mTempFlingMaxOffset, maxOffset);
    } else {
        min = minOffset;
        max = maxOffset;
    }

    int consumed = super.setHeaderTopBottomOffset(parent, header, newOffset, min, max);
    // consumed 的符號和 dy 相反

    header.dispatchOffsetChange(getTopAndBottomOffset());

    int delta = 0;

    if (mShouldDispatchFling && header.mOnHeaderFlingUnConsumedListener != null) {
        int unconsumedY = newOffset - curOffset + consumed - mTempFlingDispatchConsumed;
        if (unconsumedY != 0) {
            delta = header.mOnHeaderFlingUnConsumedListener.onFlingUnConsumed(header, newOffset, unconsumedY);
        }
        mTempFlingDispatchConsumed += -delta;
    }

    return consumed + delta;
}

首先修正邊界值鼓寺,然后調(diào)用父類的setHeaderTopBottomOffset實(shí)現(xiàn),這個方法返回父類消費(fèi)的偏移量遏暴。然后計算剩余的偏移量:

int unconsumedY = newOffset - curOffset + consumed - mTempFlingDispatchConsumed;

注意這里的mTempFlingDispatchConsumed變量侄刽,因?yàn)椴荒苤苯荧@取總的dy,在使用newOffset-curOffset獲取dy時朋凉,當(dāng)?shù)竭_(dá)實(shí)際邊界時,因?yàn)閏urOffset不會繼續(xù)變小醋安,所以獲取到的dy實(shí)際上是累計的杂彭,所以使用mTempFlingDispatchConsumed變量存儲額外消費(fèi)的掉的偏移量。

當(dāng)unconsumedY不為0時吓揪,說明有剩余未消費(fèi)的偏移量亲怠,我們把它分發(fā)出去,同時記錄Listener消費(fèi)的值柠辞,把這個值加上header本身消費(fèi)的值团秽,作為總消費(fèi)量返回。

mHeaderView.setOnHeaderFlingUnConsumedListener(new APHeaderView.OnHeaderFlingUnConsumedListener() {
    @Override
    public int onFlingUnConsumed(APHeaderView header, int targetOffset, int unconsumed) {
        APHeaderView.Behavior behavior = mHeaderView.getBehavior();
        int dy = -unconsumed;
        if (behavior != null) {
            mRecyclerView.scrollBy(0, dy);
        }
        return dy;
    }
});

在listener中,直接調(diào)用RecyclerView的scrollBy方法進(jìn)行滑動(注意符號)习勤。

這樣就完成了Header向ScrollingView的fling分發(fā)踪栋。

ScrollingView -> Header

ScrollingView需要在向下觸發(fā)fling效果時,將未消費(fèi)的偏移量交給Header處理图毕。RecyclerView依賴LayoutManager進(jìn)行滾動夷都。具體為scrollVerticallyBy方法,我們需要覆寫這個方法予颤,分發(fā)未消費(fèi)的偏移量囤官,這里直接使用匿名內(nèi)部類進(jìn)行覆寫。

final LinearLayoutManager lm = new LinearLayoutManager(mActivity, LinearLayoutManager.VERTICAL, false) {

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int scrolled = super.scrollVerticallyBy(dy, recycler, state);
        if (dy < 0 && scrolled != dy) {
            // 有剩余
            APHeaderView.Behavior behavior = mHeaderView.getBehavior();
            if (behavior != null) {
                int unconsumed = dy - scrolled;
                int consumed = behavior.scroll((CoordinatorLayout) mHeaderView.getParent(), mHeaderView, unconsumed, -mHeaderView.getScrollRange(), 0);
                scrolled += consumed;
            }
        }
        return scrolled;
    }
};

和RecyclerView類似蛤虐,調(diào)用HeaderBehavior.scroll方法進(jìn)行滾動党饮,注意邊界的處理。

雖然scroll也會調(diào)用到setHeaderTopBottomOffset驳庭,但是因?yàn)榇藭rmShouldDispatchFling一定是為false的劫谅,所以不會造成循環(huán)調(diào)用。

這樣就實(shí)現(xiàn)了fling事件的雙向分發(fā)嚷掠。

APScrollingBehavior

因?yàn)锳PScrollingBehavior和AppBarLayout.ScrollingBehavior并沒有特別不同捏检,這里就不贅述了。

總結(jié)

雖然實(shí)現(xiàn)這個效果沒用多少時間不皆,但是借此機(jī)會又完整的分析了AppBarLayout贯城、CoordinatorLayout、Behavior等官方的實(shí)現(xiàn)霹娄,讓這個效果變得有意義了一些能犯。

如果單獨(dú)評價這個交互的話,我倒覺得下拉刷新應(yīng)該出現(xiàn)在GridMenu上面犬耻,這樣頁面看起來重心就比較穩(wěn)了踩晶,不至于頭重腳輕。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末枕磁,一起剝皮案震驚了整個濱河市渡蜻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌计济,老刑警劉巖茸苇,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沦寂,居然都是意外死亡学密,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門传藏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來腻暮,“玉大人彤守,你說我怎么就攤上這事】蘧福” “怎么了具垫?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長款青。 經(jīng)常有香客問我做修,道長,這世上最難降的妖魔是什么抡草? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任饰及,我火速辦了婚禮,結(jié)果婚禮上康震,老公的妹妹穿的比我還像新娘燎含。我一直安慰自己,他們只是感情好腿短,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布屏箍。 她就那樣靜靜地躺著,像睡著了一般橘忱。 火紅的嫁衣襯著肌膚如雪赴魁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天钝诚,我揣著相機(jī)與錄音颖御,去河邊找鬼。 笑死凝颇,一個胖子當(dāng)著我的面吹牛潘拱,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拧略,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼芦岂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了垫蛆?” 一聲冷哼從身側(cè)響起禽最,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎月褥,沒想到半個月后弛随,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宁赤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了栓票。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片决左。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡愕够,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出佛猛,到底是詐尸還是另有隱情惑芭,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布继找,位于F島的核電站遂跟,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏婴渡。R本人自食惡果不足惜幻锁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望边臼。 院中可真熱鬧哄尔,春花似錦、人聲如沸柠并。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臼予。三九已至鸣戴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粘拾,已是汗流浹背窄锅。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留半哟,地道東北人酬滤。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像寓涨,于是被迫代替她去往敵國和親盯串。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容