Android 高仿美团外卖详情页

您所在的位置:网站首页 美团外卖怎么看三年前的订单详情 Android 高仿美团外卖详情页

Android 高仿美团外卖详情页

2024-07-16 02:33| 来源: 网络整理| 查看: 265

1.需求分析 美团外卖详情页

需求特点

多重嵌套滚动 标题栏 内容跟随滚动变化 店铺信息(店铺名、描述、评分、优惠信息、公告等)滚动时 折叠隐藏 或 完全展开 “点菜” 、“评价” 及 “商家” 栏滚动时 悬停吸顶 “点菜” 页面内,列表悬停 效果及菜品 列表 Item 吸顶过渡替换 底部满减神器、满减优惠、选中价格等内容 随店铺信息展开渐变隐藏

  从需求特点来看,这些功能都是比较常见的功能,普遍对应的解决方案如下

功能分析

多重嵌套滚动:   1.事件分发   2.NestedScroll嵌套滑动机制   3.CoordinatorLayout 与 Behavior 配合实现 内容跟随滚动变化   通过监听滚动事件,配合属性动画实现 悬停吸顶   1.绘制两个相同的 View,AView随布局滚动,BView固定在布局某处,再根据滚动距离,动态隐藏或显示 BView,造成吸顶假象   2.CoordinatorLayout 与 Behavior 配合实现 列表 Item 吸顶过渡替换:    通过自定义 RecyclerView.Decoration 实现 2.具体实现 2.1效果展示

​ 仿美团外卖详情页

主要通过 CoordinatorLayout + 自定义 Behavior 的方式实现

CoordinatorLayout : ​

官方文档描述

CoordinatorLayout 是功能更强大的 FrameLayout CoordinatorLayout 适用于两个主要用例:   作为顶层应用程序装饰或 chrome 布局   作为与一个或多个子视图进行特定交互的容器   通过指定 BehaviorsCoordinatorLayout 的子视图,您可以在单个父视图中提供许多不同的交互,并且这些视图也可以彼此交互。当视图类用作带 CoordinatorLayout.DefaultBehavior 注释的 CoordinatorLayout 的子级时,可以指定默认行为 。

CoordinatorLayout.Behavior:Behavior 是 CoordinatorLayout 的一个抽象内部类 ​

官方文档描述

  互动行为插件,用在位于 CoordinatorLayout 中的子视图上   行为实现了用户可以在子视图上进行的一个或多个交互。这些交互可能包括拖动,滑动,甩动或任何其他手势

  主要是通过为 CoordinatorLayout 设置 CoordinatorLayout.Behavior ,在 CoordinatorLayout.Behavior 的一系列回调方法中,操作 CoordinatorLayout 中包含的子 View ,实现想要的交互效果

为 CoordinatorLayout 设置 Behavior(有三种方式):   1.在 xml 布局通过 app:layout_behavior 来指定   2.在代码中,通过 child.getLayoutParams().setBehavior() 来指定   3.在目标 childView 类上,通过 @DefaultBehavior 来指定   (本文采用最常用的方式1进行设置) Behavior 包含的主要方法有:   // 确定使用 Behavior 的 View 位置   onLayoutChild ()   // 确定使用 Behavior 的 View 要依赖的 View ,可以在此处得到 CoordinatorLayout 下的其它子 View   layoutDependsOn ()   // 当被依赖的 View 状态改变时回调   onDependentViewChanged ()   // 嵌套滑动开始,确定 Behavior 是否要监听此次事件   onStartNestedScroll ()   // 嵌套滑动进行中,要监听的子 View 将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制)   onNestedPreScroll ()   // 接受嵌套滚动   onNestedScrollAccepted ()   // 要监听的子View即将惯性滑动(开始非实际触摸的惯性滑动)   onNestedPreFling ()   // 嵌套滑动结束   onStopNestedScroll () 与滚动动作相关的方法回调中,都有一个 @NestedScrollType type : Int 参数,像 onStartNestedScroll () 、 onNestedPreScroll () 、 onNestedScrollAccepted () 、 onStopNestedScroll () 等,该 type 是用来区分当前的滚动是由实际触摸引起的,还是由触摸结束后的惯性引起的。其中:   当 type = ViewCompat.TYPE_TOUCH 时,表示滚动是由实际触摸引起的(正在触摸中)   当 type = ViewCompat.TYPE_NON_TOUCH 时,表示滚动是由惯性引起的(触摸已经结束,甩动动作带动的滑动)

  嵌套滚动两种情况下( 抬起时无甩动动作 及 抬起时有甩动动作 ),滚动相关回调方法触发顺序 ​

无甩动动作时,滚动相关方法调用顺序示意图-1 ​ 有甩动动作时,滚动相关方法调用顺序示意图-2

  手指抬起时有甩动动作引起的惯性嵌套滚动,是在执行完实际触摸引起的嵌套滚动后执行的。也就是上面示意图1执行完之后才会执行示意图2。

  强调上述内容,是为了更好的处理手指快速滑动时,CoordinatorLayout 内的子 View 交互

2.2布局分析

布局分析   XML代码如下,将各部分分别抽成成一个 View(点击跳转查看源码:activity_shop_details.xml、ShopDiscountLayout、ShopContentLayout、ShopTitleLayout、ShopPriceLayout)

2.3代码分析 2.3.1自定义 CoordinatorLayout.Behavior

  自定义 ShopContentBehavior (点击查看源码) 继承于 CoordinatorLayout.Behavior ,并将 ShopContentLayout 视图设置为其使用者

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

  声明 xml 布局中 CoordinatorLayout 内需要根据滚动进行交互的子 view,并分别在 onLayoutChild、layoutDependsOn 方法中得到它们的实例

/** * 顶部标题栏:返回、搜索、收藏、更多 */ private lateinit var mShopTitleLayoutView: ShopTitleLayout /** * 中上部分店铺信息:配送时间、描述、评分、优惠及公告 */ private lateinit var mShopDiscountLayoutView: ShopDiscountLayout /** * 中下部分:点菜(广告、菜单)、评价、商家 */ private lateinit var mShopContentLayoutView: ShopContentLayout /** * 底部价格:满减神器、满减优惠、选中价格 */ 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 }

  解决嵌套滚动冲突问题:在 onNestedPreScroll 方法中,根据子 View 是否可以滚动的回调方法判断是否为内部 View 设置偏移   实现滚动过程中,各部分子 View 随着滚动程度进行相应变化:主要是在 onNestedPreScroll 方法中,根据滚动距离对内部 View 设置属性(透明度、偏移量、缩放等),实现嵌套滚动交互效果,配合工具类 ViewState(点击查看源码) 实现(记录 View 的起始状态和目标状态及对应状态下的属性,再根据滚动进度动态设置目标 View 的相关属性,达到指定 View 样式随滚动程度变化的目的)

/** * 嵌套滑动进行中,要监听的子 View 将要滑动,滑动事件即将被消费(但最终被谁消费,可以通过代码控制) * @param type = ViewCompat.TYPE_TOUCH 表示是触摸引起的滚动 = ViewCompat.TYPE_NON_TOUCH 表示是触摸后的惯性引起的滚动 */ override fun onNestedPreScroll( coordinatorLayout: CoordinatorLayout, child: ShopContentLayout, target: View, dx: Int, dy: Int, consumed: IntArray, type: Int ) { if (mIsScrollToHideFood) { consumed[1] = dy return // scroller 滑动中.. do nothing } mVerticalPagingTouch += dy if (mVpMain.isScrollable && abs(mVerticalPagingTouch) > mPagingTouchSlop) { mVpMain.isScrollable = false // 屏蔽 pager横向滑动干扰 } if (type == ViewCompat.TYPE_NON_TOUCH && mIsFlingAndDown) { //当处于惯性滑动时,有触摸动作进入,屏蔽惯性滑动,以防止滚动错乱 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 { //折叠状态 if (child.getRootScrollView() != null //这个判断是防止按着bannerView滚动时导致scrollView滚动速度翻倍 && (child.getScrollableView() is RecyclerView) ) { if (dy > 0) { child.getRootScrollView()!!.scrollY += dy } } } }

  实现点击指定 View 展开/收缩布局:同样是通过工具类 ViewState(点击查看源码)内的拓展函数 Any?.statesChangeByAnimation () 借由属性动画去更新指定 View 的属性

// ViewState 内提供的拓展函数 /** * 通过属性动画更新指定 View 状态 */ fun Any?.statesChangeByAnimation( views: Array, 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 中点击展开和收缩时的调用示例 /** * 展开/收缩当前布局 */ fun switch( expanded: Boolean, byScrollerSlide: Boolean = false ) { if (mIsExpanded == expanded) { return } sv_main.scrollTo(0, 0) mIsExpanded = expanded // 目标 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

  通过自定义 RecyclerView.ItemDecoration 实现列表 Item 吸顶过渡替换效果(点击跳转查看源码)

3.最后

  要通过 CoordinatorLayout + 自定义 Behavior 实现多重嵌套滚动交互效果,主要还是要了解自定义 Behavior 中嵌套滚动时触发的相关方法的具体调用时机和作用,然后通过为子 View 去设置相关 View 属性,从而实现滚动交互效果。该 Demo 都是业务代码,也没什么需要细讲的地方,具体实现可参考查阅源码。

源码及 Demo 地址:https://github.com/ziwenL/MeituanDetailDemo 实现过程中借鉴参考的博客:https://blog.csdn.net/bfbx5173/article/details/80624322 如有更好的见解或建议,欢迎留言


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3