懶飯詳情頁嵌套效果仿寫(View/Compose 實現(xiàn))

信息收集

對懶飯 APP 的視頻頁和詳情頁做布局抓取分析

上面兩圖分別是菜譜視頻播放頁和菜譜詳情頁踪宠,他們之間通過上下滑可以互相切換秆剪,如上 gif 所示躯畴,但是比較奇怪的是布局層級中菜譜詳情頁和菜譜視頻播放頁他們所處的容器是這樣的

菜譜視頻播放頁

<ViewPager>
  <RecyclerView>
    <ViewPager>
      <RecyclerView>
      </RecyclerView>
    </ViewPager>
  </RecyclerView>
</ViewPager>

菜譜詳情頁

<ViewPager> //猜測是左右滑動不同菜譜使用,類似畫廊效果
  <RecyclerView>//猜測是用來做上下滑動容器使用
    <ViewPager>//不知道干啥用的
      <RecyclerView>//猜測是用來做視頻+詳情的上下滑動容器使用矛物,這里包含了視頻控件
            <RecyclerView/>//菜譜的各個用料列表,最內(nèi)層
      </RecyclerView>
    </ViewPager>
  </RecyclerView>
</ViewPager>

相當詭異跪但,第一直覺是怎么會套了那么多層 ViewPager 和 RecyclerView 呢履羞?可能對 ViewPager 做了什么修改吧,或者可能采取了 Fragment 分塊的策略屡久,把各個塊全部分割來開發(fā)了忆首?或者可能是把 RecyclerView 當成了 NestedScrollView 來做滑動容器使用?當然可能也許是使用了RecycleView + SnapHelper 被环?具體本人也沒細究糙及,感興趣的同學(xué)可以反編譯看看。本篇主要講下怎么用嵌套滾動仿寫這個效果

仿寫

View 嵌套滾動實現(xiàn)

省略各個細節(jié)筛欢,這里主要的是視頻和詳情頁的交互

<ViewPager>//左右切換容器
  <VideoView/>//視頻播放頁
  <RecyclerView/>//詳情頁
</ViewPager>

加上嵌套容器

<ViewPager>//左右切換容器
  <CookDetailContainerLayout>//嵌套容器浸锨,通常為 NestedScrollView 的擴展類
    <VideoView/>//視頻播放頁
    <RecyclerView/>//詳情頁
  <CookDetailContainerLayout/>
</ViewPager>

第一個問題:解決 NestedScrollView 嵌套 RecyclerView 導(dǎo)致復(fù)用失效的問題

NestedScrollView嵌套RecyclerView導(dǎo)致RecyclerView復(fù)用失效的原因?_One-Heart的博客-CSDN博客_nestedscrollview嵌套recyclerview 復(fù)用

問題本質(zhì)上其實就是因為高度不確定導(dǎo)致復(fù)用失效了版姑,那其實指定 RecyclerView 的高度即可

我們的頁面根本上最終布局大致這樣

根據(jù)示意圖柱搜,將 RecyclerView 高度設(shè)置為屏幕高度 - inset 欄高度

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        (recyclerView.layoutParams as MarginLayoutParams).run {
            this.height = contentHeight
        }
        (titleTv.layoutParams as MarginLayoutParams).run {
            this.height = contentHeight
        }
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
    }

讓 NestedScrollView 中的 3 塊布局可以互相協(xié)作,互相 fling

滑動嵌套

主要是 3 塊布局滑動漠酿,由于主體布局在 NestedScrollView 中冯凹,本身已經(jīng)具備了滑動的條件,第一步我們先讓 RecyclerView 能完美的嵌套在 NestedScrollView 中炒嘲。

  • 向下滾時
    • 假設(shè)將要滾動到的距離 scrollY + dy 小于 HeaderView 高度contentHeight 宇姚,并且 rv 不能向下滾動,可以向上滾動夫凸,說明 rv 到達頂部邊界點浑劳,這個時候讓 NestedScrollView 消耗滾動偏移量,并且讓 NestedScrollView 滾動
    • 因為純 move 事件會存在 deltaY 偏移超過屏幕的情況(比如快速拖動屏幕夭拌,這種機制也是為下拉刷新場景服務(wù)所用)魔熏,這種情況需要對邊界進行調(diào)整,比如這里的鸽扁,假設(shè)將要滾動到的距離 scrollY + dy 大于 HeaderView 高度contentHeight 蒜绽,rv 不能向下滾動,可以向上滾動桶现,這種情況是 NestedScrollView 滑過界了躲雅,需要將其進行校正,校正距離其實也好辦骡和,只需要校正實際高度-當前的scrollY 即可(contentHeight - scrollY
  • 向上滾就不闡述了相赁,其實就是和向下滾相反
    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {

        if (target is RecyclerView) {
            when {
                dy > 0 -> {
                    //向下滾
                    when {
                        scrollY + dy <= contentHeight
                                && !target.canScrollVertically(-1)
                                && target.canScrollVertically(1) -> {
                            //rv [0,contentHeight] 區(qū)域內(nèi)不能向下滾動相寇,可以向上滾動,說明到達頂部
                            consumed[1] = dy
                            scrollBy(0, dy)
                        }
                        scrollY + dy > contentHeight
                                && !target.canScrollVertically(-1)
                                && target.canScrollVertically(1) -> {
                            //越界的情況钮科,滑過了 [0,contentHeight] 這個范圍唤衫,需要矯正回來,矯正距離為 contentHeight - scrollY
                            val scrollViewNeedScrollY = contentHeight - scrollY
                            scrollBy(0, scrollViewNeedScrollY)
                            consumed[1] = scrollViewNeedScrollY
                        }
                    }
                }
                dy < 0 -> {
                    //向上滾
                    when {
                        scrollY + dy >= contentHeight
                                && !target.canScrollVertically(1)
                                && target.canScrollVertically(-1) -> {
                            //[contentHeight,+oo] 區(qū)域內(nèi)不能向下滾動绵脯,可以向上滾動佳励,說明到達底部
                            //到達底部,并且滑動不會過界
                            consumed[1] = dy
                            scrollBy(0, dy)
                        }
                        scrollY + dy < contentHeight
                                && !target.canScrollVertically(1)
                                && target.canScrollVertically(-1) -> {
                            //由于滑動會有些許誤差桨嫁,這里可以讓 ScrollView 邊界在 [contentHeight,contentHeight*2]內(nèi)植兰,即劃過界了份帐,那么將其劃回來
                            //到達底部璃吧,并且滑動過界了
                            val scrollViewNeedScrollY = contentHeight - scrollY
                            scrollBy(0, scrollViewNeedScrollY)
                            consumed[1] = scrollViewNeedScrollY
                        }
                    }
                }
            }
        } else {
            super.onNestedPreScroll(target, dx, dy, consumed, type)
        }
    }

fling 速度互相轉(zhuǎn)移

在 NestScrolledView 中,希望 HeaderView废境、RecyclerView畜挨、FooterView 不同部分滑動 fling 時可以將慣性滾動速度轉(zhuǎn)移到不同的區(qū)域中,那么其實只要想辦法在 fling 過程中噩凹,rv 的上邊界和下邊界的節(jié)點傳遞速度即可巴元,這樣可以將父容器速度傳遞給子容器

  • 只考慮 fling 的情況,在 HeaderView 區(qū)域觸發(fā)下滾驮宴,滾動到 rv 區(qū)域時逮刨,將速度傳輸給 rv
  • 只考慮 fling 的情況,在 FooterView 區(qū)域觸發(fā)上滾堵泽,滾動到 rv 區(qū)域時修己,將速度傳輸給 rv
       override fun computeScroll() {
        super.computeScroll()
        if (!scroller.isFinished) {
            val currVelocity = scroller.currVelocity.toInt()
            if (scroller.startY < scroller.finalY
                && scroller.startY < contentHeight
                && scrollY > contentHeight
            ) {
                //在 HeaderView 區(qū)域觸發(fā)下滾
                scroller.abortAnimation()
                // 容器的 fling 速度交給 rv
                recyclerView.fling(0, currVelocity)
            } else if (scroller.startY > scroller.finalY && scrollY > contentHeight) {
                //在 footerView 區(qū)域觸發(fā)上滾
                scroller.abortAnimation()
                recyclerView.fling(0, -currVelocity)
            }
        }
    }

定制懶飯的效果

懶飯的效果類似于 HeaderView 和 rv+FooterView 是兩個上下的頁面,所以我們要切斷他們的 fling 聯(lián)系

HeaderView 向上 fling 迎罗,控制 fling 不讓其傳輸 rv 中去

  • 注釋掉相關(guān)的聯(lián)合滾動的 fling 機制
    override fun computeScroll() {
        super.computeScroll()
        if (!scroller.isFinished) {
            val currVelocity = scroller.currVelocity.toInt()
            if (scroller.startY < scroller.finalY
                && scroller.startY < contentHeight
                && scrollY > contentHeight
            ) {
                //在 HeaderView 區(qū)域觸發(fā)下滾
//                scroller.abortAnimation()
//                scrollTo(0, contentHeight)
                // 容器的 fling 速度交給 rv
//                recyclerView.fling(0, currVelocity)
            } else if (scroller.startY > scroller.finalY && scrollY > contentHeight) {
                //在 footerView 區(qū)域觸發(fā)上滾
                scroller.abortAnimation()
                recyclerView.fling(0, -currVelocity)
            }
        }
    }

  • 對 fling 做攔截處理睬愤,在 fling 開始時,在 headerView 中纹安,并且目的地會滑動到 rv 中的情況強制做結(jié)束scroller 滾動處理尤辱,重置將 scroller 目的地改為 rv.top 邊界 contentHeight
    override fun fling(velocityY: Int) {
        super.fling(velocityY)
        if (scroller.startY < contentHeight && scroller.finalY > contentHeight) {
            scroller.abortAnimation()
            smoothScrollTo(0, contentHeight, 400)
        }
    }

RV 向上 fling 時,不讓 rv 的速度傳輸?shù)?parent 去

  • rv 頂部厢岂,fling 模式下光督,也就是 type 為 TYPE_NON_TOUCH ,并且 rv 不會消耗任何滾動距離塔粒,認為是被帶著向上滾结借,將此行為干掉,不讓 rv 翻到上一頁
     override fun onNestedScroll(
        target: View, dxConsumed: Int, dyConsumed: Int,
        dxUnconsumed: Int, dyUnconsumed: Int, type: Int, consumed: IntArray
    ) {
        when {
            type == ViewCompat.TYPE_NON_TOUCH
                    && target.canScrollVertically(1)
                    && !target.canScrollVertically(-1)
                    && dyConsumed != 0
            -> {
                // rv 頂部窗怒,fling 模式映跟,并且 rv 不會消耗任何滾動距離蓄拣,認為是被帶著向上滾,將此行為干掉努隙,不讓 rv 翻到上一頁
                return
            }
        }
        onNestedScrollInternal(dyUnconsumed, type, consumed)
    }

讓 HeaderView 和 RV 之間具有回彈效果

  • 在滾動結(jié)束時球恤,判斷當前滾動到的區(qū)域,假設(shè)是半屏之外荸镊,則翻頁咽斧,否則,復(fù)位
  override fun onScrollStateChanged(newState: ScrollStateEnum) {
        super.onScrollStateChanged(newState)
        if (newState == ScrollStateEnum.SCROLL_STATE_IDLE) {
            if (scrollY >= contentHeight / 2 && scrollY < contentHeight) {
                smoothScrollTo(0, contentHeight)
            } else if (scrollY < contentHeight / 2 && scrollY >= 0) {
                smoothScrollTo(0, 0)
            }
        }
    }

  • 注意點

    • canScrollVertically() 代表的是否能向某個方向滾動躬存,而不是滑動张惹,滾動應(yīng)該跟滑動方向相反,比如 direction 為正代表向下滾動岭洲,也就是向上滑動宛逗,其滾動方向跟進度條方向一致
image.png

Compose 實現(xiàn)

compose 實現(xiàn)起來簡直傻瓜式,官方提供了 Pager 這個控件盾剩,只需要橫向一個 Pager 雷激,再豎向一個 Pager 即可

@Composable
fun LazyCookDetailPage() {
    val screenHeight = LocalConfiguration.current.screenHeightDp.dp
    val screenWidth = LocalConfiguration.current.screenWidthDp.dp

    HorizontalPager(
        count = CookDetailConstants.detailEntities.size,
    ) { horizontalPageIndex ->
        VerticalPager(count = 2) { verticalPageIndex ->
            when (verticalPageIndex) {
                0 -> {
                    HeaderPage(screenWidth, screenHeight, horizontalPageIndex)
                }
                1 -> {
                    ContentPage(screenWidth, screenHeight, horizontalPageIndex)
                }
            }
        }
    }
}

  • 界面實現(xiàn)代碼
@Composable
private fun ContentPage(
    screenWidth: Dp,
    screenHeight: Dp,
    horizontalPageIndex: Int
) {
    LazyColumn(
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .size(screenWidth, screenHeight)
    ) {
        items(CookDetailConstants.detailEntities[horizontalPageIndex].cookDetailSteps) { item ->
            ListItem(item)
        }
        item {
            Box(
                modifier = Modifier
                    .size(screenWidth, screenHeight)
                    .background(color = Color(ColorUtils.getRandomColor())),
                contentAlignment = Alignment.Center
            ) {
                Text(
                    text = CookDetailConstants.detailEntities[horizontalPageIndex].videoName + " footer",
                    fontSize = 25.sp,
                    textAlign = TextAlign.Center,
                )
            }
        }
    }
}

@Composable
private fun HeaderPage(
    screenWidth: Dp,
    screenHeight: Dp,
    horizontalPageIndex: Int
) {
    Box(
        modifier = Modifier
            .size(screenWidth, screenHeight)
            .background(color = Color(ColorUtils.getRandomColor())),
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = CookDetailConstants.detailEntities[horizontalPageIndex].videoName + " header",
            fontSize = 25.sp,
            textAlign = TextAlign.Center,
        )
    }
}

@Composable
private fun ListItem(item: CookDetailStepEntity) {
    Row(
        horizontalArrangement = Arrangement.SpaceAround,
        modifier = Modifier
            .background(
                color = Color(ColorUtils.getRandomColor())
            )
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = item.stepName)
        Spacer(modifier = Modifier.width(16.dp))
        Text(text = item.stepDesc)
    }
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市告私,隨后出現(xiàn)的幾起案子屎暇,更是在濱河造成了極大的恐慌,老刑警劉巖驻粟,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件根悼,死亡現(xiàn)場離奇詭異,居然都是意外死亡蜀撑,警方通過查閱死者的電腦和手機挤巡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屯掖,“玉大人玄柏,你說我怎么就攤上這事√” “怎么了粪摘?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绍坝。 經(jīng)常有香客問我徘意,道長,這世上最難降的妖魔是什么轩褐? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任椎咧,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勤讽。我一直安慰自己蟋座,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布脚牍。 她就那樣靜靜地躺著向臀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪诸狭。 梳的紋絲不亂的頭發(fā)上券膀,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音驯遇,去河邊找鬼芹彬。 笑死,一個胖子當著我的面吹牛叉庐,可吹牛的內(nèi)容都是我干的舒帮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼眨唬,長吁一口氣:“原來是場噩夢啊……” “哼会前!你這毒婦竟也來了好乐?” 一聲冷哼從身側(cè)響起匾竿,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蔚万,沒想到半個月后岭妖,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡反璃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年昵慌,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淮蜈。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡斋攀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出梧田,到底是詐尸還是另有隱情淳蔼,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布裁眯,位于F島的核電站鹉梨,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏穿稳。R本人自食惡果不足惜存皂,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望逢艘。 院中可真熱鬧旦袋,春花似錦骤菠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至胰柑,卻和暖如春截亦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柬讨。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工崩瓤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人踩官。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓却桶,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蔗牡。 傳聞我的和親對象是個殘疾皇子颖系,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

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