Android 高仿美團(tuán)外賣(mài)詳情頁(yè)

1.需求分析

美團(tuán)外賣(mài)詳情頁(yè)

需求特點(diǎn)

  • 多重嵌套滾動(dòng)
  • 標(biāo)題欄 內(nèi)容跟隨滾動(dòng)變化
  • 店鋪信息(店鋪名、描述、評(píng)分、優(yōu)惠信息、公告等)滾動(dòng)時(shí) 折疊隱藏完全展開(kāi)
  • “點(diǎn)菜” 、“評(píng)價(jià)” 及 “商家” 欄滾動(dòng)時(shí) 懸停吸頂
  • “點(diǎn)菜” 頁(yè)面內(nèi),列表懸停 效果及菜品 列表 Item 吸頂過(guò)渡替換
  • 底部滿減神器、滿減優(yōu)惠、選中價(jià)格等內(nèi)容 隨店鋪信息展開(kāi)漸變隱藏

  從需求特點(diǎn)來(lái)看,這些功能都是比較常見(jiàn)的功能,普遍對(duì)應(yīng)的解決方案如下

功能分析

  • 多重嵌套滾動(dòng):
      1.事件分發(fā)
      2.NestedScroll嵌套滑動(dòng)機(jī)制
      3.CoordinatorLayout 與 Behavior 配合實(shí)現(xiàn)
  • 內(nèi)容跟隨滾動(dòng)變化
      通過(guò)監(jiān)聽(tīng)滾動(dòng)事件,配合屬性動(dòng)畫(huà)實(shí)現(xiàn)
  • 懸停吸頂
      1.繪制兩個(gè)相同的 View,AView隨布局滾動(dòng),BView固定在布局某處,再根據(jù)滾動(dòng)距離,動(dòng)態(tài)隱藏或顯示 BView,造成吸頂假象
      2.CoordinatorLayout 與 Behavior 配合實(shí)現(xiàn)
  • 列表 Item 吸頂過(guò)渡替換:
       通過(guò)自定義 RecyclerView.Decoration 實(shí)現(xiàn)

2.具體實(shí)現(xiàn)

2.1效果展示

?
仿美團(tuán)外賣(mài)詳情頁(yè)

主要通過(guò) CoordinatorLayout + 自定義 Behavior 的方式實(shí)現(xiàn)

CoordinatorLayout :
?

官方文檔描述

CoordinatorLayout 是功能更強(qiáng)大的 FrameLayout
CoordinatorLayout 適用于兩個(gè)主要用例:
  作為頂層應(yīng)用程序裝飾或 chrome 布局
  作為與一個(gè)或多個(gè)子視圖進(jìn)行特定交互的容器
  通過(guò)指定 BehaviorsCoordinatorLayout 的子視圖,您可以在單個(gè)父視圖中提供許多不同的交互,并且這些視圖也可以彼此交互。當(dāng)視圖類(lèi)用作帶 CoordinatorLayout.DefaultBehavior 注釋的 CoordinatorLayout 的子級(jí)時(shí),可以指定默認(rèn)行為 。

CoordinatorLayout.Behavior:Behavior 是 CoordinatorLayout 的一個(gè)抽象內(nèi)部類(lèi)
?

官方文檔描述

  互動(dòng)行為插件,用在位于 CoordinatorLayout 中的子視圖上
  行為實(shí)現(xiàn)了用戶可以在子視圖上進(jìn)行的一個(gè)或多個(gè)交互。這些交互可能包括拖動(dòng),滑動(dòng),甩動(dòng)或任何其他手勢(shì)

  主要是通過(guò)為 CoordinatorLayout 設(shè)置 CoordinatorLayout.Behavior ,在 CoordinatorLayout.Behavior 的一系列回調(diào)方法中,操作 CoordinatorLayout 中包含的子 View ,實(shí)現(xiàn)想要的交互效果

  • 為 CoordinatorLayout 設(shè)置 Behavior(有三種方式):
      1.在 xml 布局通過(guò) app:layout_behavior 來(lái)指定
      2.在代碼中,通過(guò) child.getLayoutParams().setBehavior() 來(lái)指定
      3.在目標(biāo) childView 類(lèi)上,通過(guò) @DefaultBehavior 來(lái)指定
      (本文采用最常用的方式1進(jìn)行設(shè)置)
  • Behavior<V> 包含的主要方法有:
      // 確定使用 Behavior 的 View 位置
      onLayoutChild ()
      // 確定使用 Behavior 的 View 要依賴的 View ,可以在此處得到 CoordinatorLayout 下的其它子 View
      layoutDependsOn ()
      // 當(dāng)被依賴的 View 狀態(tài)改變時(shí)回調(diào)
      onDependentViewChanged ()
      // 嵌套滑動(dòng)開(kāi)始,確定 Behavior 是否要監(jiān)聽(tīng)此次事件
      onStartNestedScroll ()
      // 嵌套滑動(dòng)進(jìn)行中,要監(jiān)聽(tīng)的子 View 將要滑動(dòng),滑動(dòng)事件即將被消費(fèi)(但最終被誰(shuí)消費(fèi),可以通過(guò)代碼控制)
      onNestedPreScroll ()
      // 接受嵌套滾動(dòng)
      onNestedScrollAccepted ()
      // 要監(jiān)聽(tīng)的子View即將慣性滑動(dòng)(開(kāi)始非實(shí)際觸摸的慣性滑動(dòng))
      onNestedPreFling ()
      // 嵌套滑動(dòng)結(jié)束
      onStopNestedScroll ()
  • 與滾動(dòng)動(dòng)作相關(guān)的方法回調(diào)中,都有一個(gè) @NestedScrollType type : Int 參數(shù),像 onStartNestedScroll ()onNestedPreScroll ()onNestedScrollAccepted ()onStopNestedScroll () 等,該 type 是用來(lái)區(qū)分當(dāng)前的滾動(dòng)是由實(shí)際觸摸引起的,還是由觸摸結(jié)束后的慣性引起的。其中:
      當(dāng) type = ViewCompat.TYPE_TOUCH 時(shí),表示滾動(dòng)是由實(shí)際觸摸引起的(正在觸摸中)
      當(dāng) type = ViewCompat.TYPE_NON_TOUCH 時(shí),表示滾動(dòng)是由慣性引起的(觸摸已經(jīng)結(jié)束,甩動(dòng)動(dòng)作帶動(dòng)的滑動(dòng))

  嵌套滾動(dòng)兩種情況下( 抬起時(shí)無(wú)甩動(dòng)動(dòng)作抬起時(shí)有甩動(dòng)動(dòng)作 ),滾動(dòng)相關(guān)回調(diào)方法觸發(fā)順序
?

無(wú)甩動(dòng)動(dòng)作時(shí),滾動(dòng)相關(guān)方法調(diào)用順序示意圖-1

?
有甩動(dòng)動(dòng)作時(shí),滾動(dòng)相關(guān)方法調(diào)用順序示意圖-2

  手指抬起時(shí)有甩動(dòng)動(dòng)作引起的慣性嵌套滾動(dòng),是在執(zhí)行完實(shí)際觸摸引起的嵌套滾動(dòng)后執(zhí)行的。也就是上面示意圖1執(zhí)行完之后才會(huì)執(zhí)行示意圖2。

  強(qiáng)調(diào)上述內(nèi)容,是為了更好的處理手指快速滑動(dòng)時(shí),CoordinatorLayout 內(nèi)的子 View 交互

2.2布局分析

?

布局分析

  XML代碼如下,將各部分分別抽成成一個(gè) View(點(diǎn)擊跳轉(zhuǎn)查看源碼:activity_shop_details.xml、ShopDiscountLayoutShopContentLayoutShopTitleLayout、ShopPriceLayout

<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cl_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white">

    <!--頂部 店鋪信息+優(yōu)惠活動(dòng) 內(nèi)容-->
    <com.ziwenl.meituan_detail.ui.shop.ShopDiscountLayout
        android:id="@+id/layout_discount"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!--中下部 點(diǎn)菜/評(píng)論/商家 內(nèi)容-->
    <com.ziwenl.meituan_detail.ui.shop.ShopContentLayout
        android:id="@+id/layout_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="@dimen/top_min_height"
        app:layout_behavior=".ui.shop.ShopContentBehavior" />

    <!--頂部 標(biāo)題欄 內(nèi)容-->
    <com.ziwenl.meituan_detail.ui.shop.ShopTitleLayout
        android:id="@+id/layout_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <!--底部 滿減神器、滿減優(yōu)惠、價(jià)格費(fèi)用-->
    <com.ziwenl.meituan_detail.ui.shop.ShopPriceLayout
        android:id="@+id/layout_price"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</androidx.coordinatorlayout.widget.CoordinatorLayout>

2.3代碼分析

2.3.1自定義 CoordinatorLayout.Behavior

  自定義 ShopContentBehavior (點(diǎn)擊查看源碼) 繼承于 CoordinatorLayout.Behavior ,并將 ShopContentLayout 視圖設(shè)置為其使用者

class ShopContentBehavior(private val context: Context, attrs: AttributeSet?) :
    CoordinatorLayout.Behavior<ShopContentLayout>(context, attrs) {
    ......
}

  聲明 xml 布局中 CoordinatorLayout 內(nèi)需要根據(jù)滾動(dòng)進(jìn)行交互的子 view,并分別在 onLayoutChild、layoutDependsOn 方法中得到它們的實(shí)例

    /**
     * 頂部標(biāo)題欄:返回、搜索、收藏、更多
     */
    private lateinit var mShopTitleLayoutView: ShopTitleLayout

    /**
     * 中上部分店鋪信息:配送時(shí)間、描述、評(píng)分、優(yōu)惠及公告
     */
    private lateinit var mShopDiscountLayoutView: ShopDiscountLayout

    /**
     * 中下部分:點(diǎn)菜(廣告、菜單)、評(píng)價(jià)、商家
     */
    private lateinit var mShopContentLayoutView: ShopContentLayout

    /**
     * 底部?jī)r(jià)格:滿減神器、滿減優(yōu)惠、選中價(jià)格
     */
    private lateinit var mShopPriceLayoutView: ShopPriceLayout

    override fun onLayoutChild(
        parent: CoordinatorLayout,
        child: ShopContentLayout,
        layoutDirection: Int
    ): Boolean {
        if (!this::mShopContentLayoutView.isInitialized) {
            mShopContentLayoutView = child
            ......
        }
        return super.onLayoutChild(parent, child, layoutDirection)
    }

    override fun layoutDependsOn(
        parent: CoordinatorLayout,
        child: ShopContentLayout,
        dependency: View
    ): Boolean {
        when (dependency.id) {
            R.id.layout_title -> mShopTitleLayoutView = dependency as ShopTitleLayout
            R.id.layout_discount -> mShopDiscountLayoutView = dependency as ShopDiscountLayout
            R.id.layout_price -> mShopPriceLayoutView = dependency as ShopPriceLayout
            else -> return false
        }
        return true
    }

  解決嵌套滾動(dòng)沖突問(wèn)題:在 onNestedPreScroll 方法中,根據(jù)子 View 是否可以滾動(dòng)的回調(diào)方法判斷是否為內(nèi)部 View 設(shè)置偏移
  實(shí)現(xiàn)滾動(dòng)過(guò)程中,各部分子 View 隨著滾動(dòng)程度進(jìn)行相應(yīng)變化:主要是在 onNestedPreScroll 方法中,根據(jù)滾動(dòng)距離對(duì)內(nèi)部 View 設(shè)置屬性(透明度、偏移量、縮放等),實(shí)現(xiàn)嵌套滾動(dòng)交互效果,配合工具類(lèi) ViewState(點(diǎn)擊查看源碼) 實(shí)現(xiàn)(記錄 View 的起始狀態(tài)和目標(biāo)狀態(tài)及對(duì)應(yīng)狀態(tài)下的屬性,再根據(jù)滾動(dòng)進(jìn)度動(dòng)態(tài)設(shè)置目標(biāo) View 的相關(guān)屬性,達(dá)到指定 View 樣式隨滾動(dòng)程度變化的目的)

    /**
     * 嵌套滑動(dòng)進(jìn)行中,要監(jiān)聽(tīng)的子 View 將要滑動(dòng),滑動(dòng)事件即將被消費(fèi)(但最終被誰(shuí)消費(fèi),可以通過(guò)代碼控制)
     * @param type = ViewCompat.TYPE_TOUCH 表示是觸摸引起的滾動(dòng) = ViewCompat.TYPE_NON_TOUCH 表示是觸摸后的慣性引起的滾動(dòng)
     */
    override fun onNestedPreScroll(
        coordinatorLayout: CoordinatorLayout,
        child: ShopContentLayout,
        target: View,
        dx: Int,
        dy: Int,
        consumed: IntArray,
        type: Int
    ) {
        if (mIsScrollToHideFood) {
            consumed[1] = dy
            return // scroller 滑動(dòng)中.. do nothing
        }
        mVerticalPagingTouch += dy
        if (mVpMain.isScrollable && abs(mVerticalPagingTouch) > mPagingTouchSlop) {
            mVpMain.isScrollable = false // 屏蔽 pager橫向滑動(dòng)干擾
        }

        if (type == ViewCompat.TYPE_NON_TOUCH && mIsFlingAndDown) {
            //當(dāng)處于慣性滑動(dòng)時(shí),有觸摸動(dòng)作進(jìn)入,屏蔽慣性滑動(dòng),以防止?jié)L動(dòng)錯(cuò)亂
            consumed[1] = dy
            return
        }
        if (type == ViewCompat.TYPE_NON_TOUCH) {
            mIsScrollToFullFood = true
        }
        mHorizontalPagingTouch += dx
        if ((child.translationY < 0 || (child.translationY == 0F && dy > 0))
            && !child.getScrollableView().canScrollVertically(-1)
        ) {
            val effect = mShopTitleLayoutView.effectByOffset(dy)
            val transY = -mSimpleTopDistance * effect
            mShopDiscountLayoutView.translationY = transY
            if (transY != child.translationY) {
                child.translationY = transY
                consumed[1] = dy
            }

        } else if ((child.translationY > 0 || (child.translationY == 0F && dy < 0))
            && !child.getScrollableView().canScrollVertically(-1)
        ) {
            if (mIsScrollToFullFood) {
                child.translationY = 0F
            } else {
                child.translationY -= dy
                mShopDiscountLayoutView.effectByOffset(child.translationY)
                mShopPriceLayoutView.effectByOffset(child.translationY)
            }
            consumed[1] = dy
        } else {
            //折疊狀態(tài)
            if (child.getRootScrollView() != null
                //這個(gè)判斷是防止按著bannerView滾動(dòng)時(shí)導(dǎo)致scrollView滾動(dòng)速度翻倍
                && (child.getScrollableView() is RecyclerView)
            ) {
                if (dy > 0) {
                    child.getRootScrollView()!!.scrollY += dy
                }
            }
        }
    }

  實(shí)現(xiàn)點(diǎn)擊指定 View 展開(kāi)/收縮布局:同樣是通過(guò)工具類(lèi) ViewState(點(diǎn)擊查看源碼)內(nèi)的拓展函數(shù) Any?.statesChangeByAnimation () 借由屬性動(dòng)畫(huà)去更新指定 View 的屬性

// ViewState 內(nèi)提供的拓展函數(shù)
/**
 * 通過(guò)屬性動(dòng)畫(huà)更新指定 View 狀態(tài)
 */
fun Any?.statesChangeByAnimation(
    views: Array<View>,
    startTag: Int,
    endTag: Int,
    start: Float = 0F,
    end: Float = 1F,
    updateCallback: AnimationUpdateListener? = null,
    updateStateListener: AnimatorListenerAdapter? = null,
    duration: Long = 400L,
    startDelay: Long = 0L
): ValueAnimator {
    return ValueAnimator.ofFloat(start, end).apply {
        this.startDelay = startDelay
        this.duration = duration
        interpolator = AccelerateDecelerateInterpolator()
        addUpdateListener { animation ->
            val p = animation.animatedValue as Float
            updateCallback?.onAnimationUpdate(startTag, endTag, p)
            for (it in views) it.stateRefresh(startTag, endTag, animation.animatedValue as Float)
        }
        updateStateListener?.let { addListener(it) }
        start()
    }
}
// ShopDiscountLayout 中點(diǎn)擊展開(kāi)和收縮時(shí)的調(diào)用示例
/**
 * 展開(kāi)/收縮當(dāng)前布局
 */
fun switch(
    expanded: Boolean,
    byScrollerSlide: Boolean = false
) {
    if (mIsExpanded == expanded) {
        return
    }
    sv_main.scrollTo(0, 0)
    mIsExpanded = expanded // 目標(biāo)
    val start = effected
    val end = if (expanded) 1F else 0F
    statesChangeByAnimation(
        animViews(), R.id.viewStateStart, R.id.viewStateEnd, start, end,
        null, if (!byScrollerSlide) internalAnimListener else null, 500
    )
}

2.3.2自定義 RecyclerView.ItemDecoration

  通過(guò)自定義 RecyclerView.ItemDecoration 實(shí)現(xiàn)列表 Item 吸頂過(guò)渡替換效果(點(diǎn)擊跳轉(zhuǎn)查看源碼)

3.最后

  要通過(guò) CoordinatorLayout + 自定義 Behavior 實(shí)現(xiàn)多重嵌套滾動(dòng)交互效果,主要還是要了解自定義 Behavior 中嵌套滾動(dòng)時(shí)觸發(fā)的相關(guān)方法的具體調(diào)用時(shí)機(jī)和作用,然后通過(guò)為子 View 去設(shè)置相關(guān) View 屬性,從而實(shí)現(xiàn)滾動(dòng)交互效果。該 Demo 都是業(yè)務(wù)代碼,也沒(méi)什么需要細(xì)講的地方,具體實(shí)現(xiàn)可參考查閱源碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,055評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,365評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,561評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,346評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,889評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,118評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,637評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,558評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,739評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,980評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,362評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,619評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,347評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,702評(píng)論 2 370

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