专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
鸿洋  ·  App前台,Activity会被回收吗? ·  2 天前  
鸿洋  ·  突发,这可能是软考最好过的一年! ·  3 天前  
鸿洋  ·  WebView 经历的各种干货方案分享 ·  6 天前  
郭霖  ·  提升 WebView ... ·  1 周前  
stormzhang  ·  胖东来的「另类」 ·  1 周前  
51好读  ›  专栏  ›  鸿洋

简洁高效:类抖音视频列表设计思路

鸿洋  · 公众号  · android  · 2024-10-24 08:35

正文


本文作者


作者:麦客奥德彪

链接:

https://juejin.cn/post/7348464590159773748

本文由作者授权发布。


列表类产品现在非常多,可以说是10个APP中9个是有列表功能的,今天要说的是视频、直播类切换类型的负责业务解耦。具体业务场景可以用抖音短视频为例,只讨论其实现方式。

这种类型的产品一般实现方式有两种。

1
常见此种逻辑的代码实现

1.1 使用ViewPager2 + Fragment

优点:

  1. 模块化: 每个功能都在独立的 Fragment 中完成,使得代码更易于维护和管理。
  2. 复用性: 可以轻松地在不同的页面中重复使用 Fragment,避免了代码的重复编写。
  3. 灵活性: Fragment 提供了更多的生命周期方法和回调,可以更精细地控制页面的行为和状态。
  4. 易于管理状态: 每个 Fragment 都有自己的生命周期,可以方便地管理页面状态和数据加载。

缺点:

  1. 内存消耗: 每个 Fragment 都有自己的视图层次结构和生命周期,可能会占用较多的内存,尤其是在包含大量视频或图片的页面。
  2. 性能问题: 在 ViewPager2 中使用 Fragment 可能会存在性能问题,特别是在加载大量页面时,会影响滑动的流畅性。

1.2 使用ViewPager2 + 自定义View

优点:

  1. 轻量级: 自定义 View 可以更轻量地处理页面内容,避免了 Fragment 的复杂性。
  2. 性能优化: 自定义 View 可以更灵活地优化绘制和布局,提高了页面的渲染效率。
  3. 更小的内存消耗: 自定义 View 可以更精简地管理页面状态,减少了内存占用。
  4. 更灵活的布局: 自定义 View 提供了更灵活的布局方式,可以更容易地实现各种复杂的界面效果。

缺点:

  1. 复杂性增加: 自定义 View 的开发相对于 Fragment 更复杂,需要更多的自定义绘制和布局代码。
  2. 可维护性降低: 自定义 View 可能会导致代码结构不够清晰,降低了代码的可读性和可维护性。
  3. 复用性降低: 自定义 View 不像 Fragment 那样容易进行模块化和重用,可能会导致代码冗余和重复编写。

曾经我在直播类产品中使用过第一种方式实现直播间的切换,效果还行,但是也确实遇到了很多坑,不仅是上述的几个缺点,总之太多笨重,如果有实现这种需求的用第一种方式要三思啊。

2
今天要讨论的是这样一个问题

不管使用哪种方式,面对如此庞大的业务,每一个Item 的代码应该怎么写?RecyclerView 这个最常用的组件,作者将其写到了一个文件中,有人说聚合性高,写法很牛逼,但是换个人,如果是我写的,他们还会这么说吗?毕竟代码还需要给别人看。比如常规的写法我们应该是这样的:
  1. Activity/Fragment -> 数据填充、属性、触发渲染。
  2. ViewPager2 + CustomView -> 常规列表功能。
  3. Custom View中 播放视频、展示图片、用户信息、视频信息、手势处理等等。
呈现出来的应该是这样:
当然这是我们看见的,还有看不见的快进/退、放大、缩小、图文类型、直播类型等等。
那这种写法有什么问题呢?
  1. 代码复杂度增加:一个类中包含大量的业务逻辑和功能会使得代码变得庞大而复杂,不易于维护和阅读。
  2. 单一职责原则违反:单一职责原则是面向对象编程中的基本原则之一,一个类应该只负责一种类型的职责。将多个不同类型的功能聚合在一个类中,违反了这个原则,会导致代码结构混乱,难以理解和修改。
  3. 难以测试和调试:功能过于集中的类会导致测试和调试变得困难,因为它们会有过多的依赖和交互。
  4. 耦合度增加:各个功能之间可能会产生较大的耦合,导致代码的灵活性和可维护性降低。
  5. 可扩展性受限:如果后续需要添加新的功能或修改现有功能,可能会涉及到整个类的修改,增加了开发的风险和成本。

3
那我们希望的是什么呢?


当然就是解决上面的这几个问题。

3.1 梳理一个独立使用的根布局

想法是这样的,为了方便任意位置的使用,我们直接自定义一个View,这个View的职责:主要管理一级视频功能,即视频列表的根布局。
  1. 列表刷新、加载更多等数据源处理,需要考虑数据的来源。
  2. 列表,即ViewPager2 或者可滑动容器的添加。
  3. 全局的动画等(比如加载数据时的动画)。

3.2 对滑动容器进行独立,让上述的独立布局成为一个可以添加任意具备上述功能的容器

为了让这个可滑动的容器,职责单一,我们在此只提供以下功能:
  1. 滑动列表初始化。
  2. 列表适配器创建及绑定。
此时我们就具备了两个层级的可滑动的列表。

3.3 业务在Adapter中创建,准确来说是添加

这就是一个正常的Adapter,在onCreateViewHolder中进行Item View 的创建,但是如果单纯的创建,不就是上述的问题出现吗,所以此处需要对ItemView 进行解耦。

3.4 解耦ItemView 的负责逻辑(重点在这里)

看图的话是不是很简单,不过这种场景中不好处理的问题在于,多个业务可能有互相使用的场景:
  1. 所有视图都应该知道有没有链接到对应的ItemView 中。
  2. 所有视图业务可能都需要触摸事件 比如双击、缩放等。
  3. 部分视图可能需要感知播放器的状态以做出自己对应的策略。
  4. 所有视图业务都需要知道滑动切换后的数据及动作。
  5. 所有的试图对于手势事件的特定场景应该保持一致动作(比如隐藏除视频播放器外的所有视图)等。
如果单纯的使用类文件将这些解耦会出现很多问题,比如上述的问题。怎么做到上述的这几点呢?怎么优雅的创建这些组件呢,最起码要做到:
  1. 组件添加、移除流程必须清晰。
  2. 组件可以感知播放器的各种状态或者订阅状态。
  3. 各个组件的操作应该是层级间可以互相影响调用的。
  4. 组件间的操作应该可以影响视频播放器器的。
  5. 组件的添加应该是可配置的(比如某些场景下不用展示用户信息)。
可以分层设计,为了能满足上述的功能:
类图关系大概如下:
可以描述为:
  1. VideoLayer 类表示视频图层,它可以通过 bindLayerHost() 方法绑定到 VideoLayerHost,通过 unbindLayerHost() 方法解除绑定。VideoLayer 可以与 VideoView 关联,它的具体功能需要在子类中实现。
  2. VideoLayerHost 类表示视频图层的宿主,可以包含多个 VideoLayer。它可以与 VideoView 关联,通过 attachToVideoView() 方法将自身附加到 VideoView 上。同时,它可以添加和移除 VideoLayerHostListener 监听器,用于监听宿主与 VideoView 的关联状态。
  3. VideoView 类表示视频视图,它可以包含一个 VideoLayerHost 作为其宿主。可以通过 bindLayerHost() 方法将 VideoLayerHost 绑定到当前的 VideoView 上。
这样一来,可以通过VideoLayerHost来管理VideoLayer 的添加删除,并通过它联系起来这个视图层的运作,在使用过程中,可以用以下流程描述添加一个ItemView 的流程及其原理。

3.5 抽象工厂模式配置化图层的创建与管理

在任意使用位置可进行自定义抽象工厂来改变图层。
class MyVideoViewFactory : VideoViewFactory {
    override fun createVideoView(context: Context): VideoView {
        val videoView = VideoView(context)
        val layerHost = VideoLayerHost(context)
        val videoInfoLayer = VideoInfoLayer()
        layerHost.addLayer(videoInfoLayer)
        layerHost.attachToVideoView(videoView)
        videoView.setBackgroundColor(ContextCompat.getColor(context, R.color.default_bg))
        return videoView
    }
}
4
使用流程 部分代码

4.1 在目标页面进行视图绑定

private fun testArchitecture() {
        val videoItems = mutableListOf()
        testData(videoItems)
        // 使用前根据配置指定图层
//        VideoViewFactory.setVideoViewFactory(MyVideoViewFactory())
        val videoSceneView = VideoSceneView(this)
            .apply {
                videoPageView.setLifeCycle(lifecycle)
                videoPageView.setItems(videoItems)
            }
        setContentView(videoSceneView)
    }
5
总结

这种设计方案的优势在于提供了更灵活、可扩展的方式来管理和组织视频播放页面的各个组件,从而降低了代码的复杂度,提高了代码的可维护性和可读性。以下是对该设计方案的总结:
  1. 模块化和组件化: 通过将视频播放页面的功能拆分成不同的组件,实现了模块化和组件化。每个组件都具有清晰的职责和功能,便于单独开发、测试和维护。
  2. 解耦和灵活性: 使用图层和宿主的设计模式,实现了各个组件之间的解耦,使得它们可以独立存在并且相互影响。同时,通过抽象工厂模式配置化图层的创建与管理,进一步提高了灵活性,使得可以根据需要动态的配置和切换不同的图层。
  3. 单一职责原则: 每个组件都遵循单一职责原则,只负责特定的功能和逻辑,使得代码结构清晰,易于理解和修改。
  4. 可扩展性: 通过定义清晰的接口和抽象类,使得可以很容易地扩展和添加新的功能和组件,而不会影响到已有的代码逻辑。
  5. 配置化: 通过抽象工厂模式,将图层的创建和管理配置化,使得可以根据需要动态的配置和切换不同的图层,从而满足不同场景下的需求。
参考:
VEVodDemo-android 火山视频UI解耦部分


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!