index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html
本文深入探讨了视频编辑页的重构过程,着重于解决旧版本中轨道冗余、操作不便等问题。通过采用MVVM+UDF的架构模式,并进行精细化的模块拆分和依赖注入,实现了代码的可维护性和可扩展性。文章详细介绍了界面布局、用户交互、功能优化及新增功能的具体方案,并重点阐述了如何兼容旧业务功能、设计复杂的轨道控件以及实现Redo/Undo功能。此次重构历时半年,代码修改量巨大,充分体现了团队协作的重要性,并为未来的软件架构设计提供了宝贵经验。
🌟 **现代化架构升级**:为应对视频编辑页日益增长的业务复杂性,引入MVVM+UDF架构模式,将界面与逻辑解耦,实现数据驱动的UI更新和单向数据流。这一架构设计遵循Google的指南,提高了代码的可测试性、可维护性和可重用性,并为跨平台开发奠定了基础。
🛠️ **模块化与兼容性设计**:通过将页面横向拆分为多个UI组件和纵向分层为界面层、业务层、数据层,提升了代码内聚性。同时,针对旧业务面板的兼容性问题,采用了接口化设计和代理层实现,确保了新旧界面的平滑过渡和无缝协作。
🎨 **核心控件与功能优化**:对视频轨道控件进行了精细化设计,采用自定义Canvas绘制和优化的事件分发机制,支持丰富的动画和高性能渲染。此外,Redo/Undo功能被设计为基于备忘录模式(并提出了基于UDF的命令行模式优化方案),以提高操作的容错性,并对底部功能面板进行了统一管理和模态设计,提升了用户体验。
原创 大前端 2025-08-01 12:03 上海
视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。
1.背景介绍 在数字内容井喷的时代,移动端已成为视频创作的重要阵地,而视频编辑页作为创作工具的核心场景,不仅为创作者提供了丰富的表达手段和创意平台,更是提升视频制作的效率。通过直观的操作界面和丰富的功能集成,用户可以轻松地将素材、音频、特效及文字等进行融合,创造出独具风格、彰显个性的作品。 然而,视频编辑页的页面设计和代码实现也充满了复杂性和挑战。在单一页面中集成了视频、音频、文字、贴纸、特效等多种功能。横向上,数十个模块互相交织与影响;纵向上,每个模块又提供了丰富的功能和精细化的操作。如此的业务复杂度,对页面架构以及功能代码的设计提出了更高的要求。 2. 明确需求 视频编辑页的初版上线后,一直未进行大型升级,旧版本存在较多不足。如轨道过多,信息冗余,用户理解成本高,主轨道设计落后,视频轨道操作不方便等问题。 产品团队希望进行一次业务升级,以轻量化的视频编辑为核心思路,从界面布局、用户交互、已有功能优化、新增必要功能等方面对编辑页进行改版。 3. 需求分析 在工程实施之前,我们需要对本次需求进行全面分析,从而指导代码设计。 3.1 业务升级要做些什么的? 本次业务升级的重点,可以归纳为以下四个方面。 界面布局: 重新定义编辑页的交互方式,划分操作区域,升级主页面和操作面板的视觉设计。 用户交互: 全新设计视频轨道、素材多轨道、工具栏、快速编辑等核心控件的交互方式。 功能优化: 对剪辑、文字、贴纸、转场、音乐等已有业务功能进行升级和多轨道改造。 新增功能: 增加Redo/Undo、文字快速编辑、画面编辑、全屏预览等实用功能。 3.2 代码的现状是怎么样的? 那当前的代码支持进行如此大型的业务升级吗?我们从以下几个方面进行了分析: 页面架构: 旧界面缺乏架构设计,以MVC为主,夹杂MVP的使用。绝大部分代码存在于Activity以及Fragment中,存在超过5000行代码的超级类,逻辑杂糅,维护困难。 业务模块: 功能分布以主Actvitiy(主界面)+ Fragment(模块功能)的形式实现。通过互相持有对象的方式进行Activity与Fragment的通信,耦合严重。 灰度要求: 如果在原代码上修改,实现新旧界面的灰度势必是场灾难。如果通过copy代码的方式进行,没有解决旧代码的问题,又新增很多代码,只会让页面的可维护性再次降低。 3.3 需要考虑的关键问题 基于业务诉求和代码现状,我们决定对编辑页进行重构,从架构设计到关键功能点进行全面升级。那么接下来需要思考如何进行界面重构及部分功能点的设计? 页面的架构选择 移动应用开发领域的常见的页面架构,包括MVC、MVP、MVVM、MVI、VPIER等多种类型。综合考虑各种架构的优缺点和实现成本,结合业务特点,我们最终选择MVVM+UDF的页面架构。 新架构如何兼容旧功能? 业务升级会以多个需求并行开发,逐步上线的方式完成,并不是一次性完成所有升级,所以需要考虑如何兼容旧的功能面板。 怎样设计Redo/Undo功能? Redo/Undo作为一个必要的、使用广泛的功能,代码设计时需要考虑其低侵入性、高可用性和高性能。 如何设计复杂的轨道控件? 作为编辑页的控件中可操作性强,逻辑复杂性高的代表,该控件的代码设计也非常重要。 4. 具体方案 4.1 页面架构设计 4.1.1 设计原则 在确定采用MVVM+UDF的架构后,我们确定了以下的页面架构设计原则: (1)整体采用MVVM进行架构设计。 (2)遵循数据模型(UiState)驱动界面的原则。 (3)基本遵循单一可信数据源的原则。(SSOT:Single Source of Truth) (4)遵循单向数据流原则。(UDF:Unidirectional Data Flow) (5)对业务进行模块拆分,提高模块内的代码内聚,降低模块间的代码耦合。 (6)使用依赖注入,提高代码复用性,解决对象依赖问题。 如下图为简化的架构设计图: 4.1.2 分层设计 架构设计在纵向上进行了分层设计,整体拆分为界面层 + 业务层 + 数据层。 UI Layer: 界面层,分为UI元素(Activity/Fragment)以及状态容器ViewModel,通过UiState(界面状态)来控制界面元素的变化。 UiState/UiStateHolder:UiState作为界面的唯一可信数据源,以不可变类型公开数据。具有聚合数据,保护数据,易于跟踪对数据的更改等多种优势。UiStateHolder用于聚合同一业务模块的UiState。 以播放按钮的界面状态为例,其UiState定义如下:
data class PlayBtnUiState ( val isShowPlayView: Boolean , val isPlaying: Boolean , val onPlayViewClick: (() -> Unit )? ) LiveData/StateFlow:作为数据流的中介,界面层通过监听UiState的变化,控制界面状态变化。
val playBtnUiState = MutableLiveData <PlayBtnUiState >() stateHolder.playBtnUiState .observe (owner ) { uiState -> updatePlayViewUiState (uiState) } Domain Layer: 网域层,在视频编辑页这样复杂度的单页面架构设计中,该层必不可少,其封装复杂的业务逻辑,使用大量的UseCase来用于处理复杂的业务逻辑并且支持业务代码的可重用性。 Data Layer: 数据层,由多个业务仓库(Repository)以及数据源(Data Sources)组成。在实际设计中,为了尽可能的兼容已有的数据获取方式以及业务特点,该层的具体实施中并不完全照本宣科,而是基于推荐的架构设计进行了一定程度的改造。 4.1.3 模块拆分 在横向上按照业务模块在界面层、网域层进行了模块拆分。 UiComponent: 按照界面特点拆分为顶部栏模块、预览模块、中部状态模块、主操作模块、业务面板模块等。 UiStateHolder: 对于不同的业务模块,每个业务模块会使用一个或多个UiStateHolder来持有LiveData/StateFlow。 UseCase: 根据作用域的不同,UseCase可以分为以下三个级别: 界面模块级:其模块划分遵循界面层的定义,和Ui的模块划分保持一致。 业务子模块级:按照剪辑、字幕、贴纸、音乐等不同的编辑业务进行模块拆分。 业务子功能级:在业务功能模块中,将不同的子功能进行聚合,众多子功能组成该模块的完整功能。 4.1.4 单向数据流(UDF) 在架构的事件设计中,遵循状态(UiState)从数据层流向界面,事件(Event)从界面流向数据层的设计原则。 UDF有助于实现数据一致性、可测试性、可维护性。如Google官方数据流转图所示: 4.1.5 依赖注入(DI)的使用 当业务功能进行细粒度的拆分之后,如何进行优雅的进行对象依赖、代码复用和对象生命周期管理就成了问题。我们结合业务现状选择了依赖注入框架Hilt来解决这些问题。 以添加贴纸的功能点,来说明如何使用依赖注入的。
@HiltViewModel class EditorStickerListViewModel @Inject ( private val projectRepository: ProjectRepository, private val streamingRepository: StreamingRepository, private val stickerAddUseCase: EditorStickerAddUseCase, private val materialTrackUseCase: TrackCommonUseCase ) EditorStickerListViewModel: 该ViewModel是贴纸列表面板所对应的VM,其通过@HiltViewModel进行构建,生命周期与Fragment对象保持一致。在它构造函数中,我们注入了多个对象,其作用如下: ProjectRepository: 项目数据管理。其他内部持有唯一的项目数据EditorProject。 StreamingRepository: 渲染引擎管理。其内部通过与EditorEngine交互,实现对渲染引擎的数据设置和状态控制。 TrackCommonUseCase: 负责轨道控件的显示状态控制,其内部注入UiStateHolder对象,通过修改UiState来更新该控件的显示状态。 简化的添加贴纸素材的代码如下:
fun addStickerMaterials (dataList: List < MaterialItem >) { val stickerClipList = createStickerBClipList(dataList) projectRepository.addStickerClips(clipList) streamingRepository.insertStickerClipList(insertTime,stickerClipList) materialTrackUseCase.refreshMultiMaterialTrack() streamingRepository.refreshCurrentTime() } 依赖注入的对象的生命周期管理 使用Hilt框架进行依赖注入时,需要合理的选择每个对象应该有的生命周期。基于不同的生命周期合理选择@ActivityScoped、@ViewModelScoped、@FragmentScoped等注解,或自定义生命周期。 4.1.6 架构设计的总结 我们基本遵循Google的架构设计指南,在上述的架构设计原则下进行整体的架构设计和代码实践。 在MVVM + UDF的架构中,MVVM进行视图和逻辑的解耦,而UDF决定了状态的管理范式。 该架构解决了传统MVVM(双向绑定)的缺陷,让程序的关注点分离清晰,避免了数据不一致和状态管理分散的问题,使得代码易于调试和测试。 其也是现代UI框架(Compose,SwiftUI等)理念的自然延伸和最佳实践,在如今跨平台的浪潮下,无疑也是构建跨端的、健壮的、可维护和可测试应用的强大范式,随着Compose Multiplaform的成熟,该架构在KMP中能进一步实现UI与逻辑的跨平台复用。 4.2 部分功能点的设计和实现 4.2.1 如何兼容旧业务功能 核心问题: 在新编辑器页与旧业务面板共存的过渡阶段,需要确保新页面完整支持旧面板的显示和其依赖的界面能力,如主界面UI控制、引擎状态监听与操作、数据流交互等。 技术方案: 将旧面板对主界面的强依赖改造为接口化设计。 (1)抽象关键能力:将原Activity中被旧面板调用的100余函数,抽象为接口。 (2)代理层实现:旧界面Activity直接实现接口,保持原有逻辑。新界面通过代理机制,将函数路由到新架构的对应功能模块。 (3)灰度控制:通过实验结果动态切换接口的具体实现方。 后续优化: 过渡期完整保留代理层,确保新旧界面无缝协作。旧业务面板改造完成后,直接移除该部分代码。 4.2.2 底部功能面板组件 编辑页中,有数十个大小不一,功能迥异的功能面板通过Fragment的方式嵌入在界面中,需要对功能面板进行统一的设计和管理。 面板的基类(EditorBaseFragment): 新基类仅拥有少量与业务无关,用于定义面板通用规范的函数。 面板的管理: 统一管理Fragment的添加和移除、进出动画、面板栈等。 面板模态设计: 定义三种不同高度和编辑模式的面板模态,从Fragment的容器高度就确定了面板的高度,从而规范了面板的设计。
enum class FragmentContainerModal { * 半模态-露出部分轨道 * */ MODAL_HALF, * 全模态-遮挡整个操作区域 * */ MODAL_ALL, * 全屏模态-容器为全屏 * */ MODAL_FULL_SCREEN } 4.2.3 视频轨道控件的设计 视频轨道控件作为编辑页的复杂控件之一,其高可操作性和业务复杂性对控件的设计提出了较高的要求。该控件的代码结构设计如下: 控件状态管理: 统一定义控件的状态,用来管理控件在不同业务场景下的UI状态及操作状态变化。 绘制体系: 整个控件的绘制体系采用自定义Canvas绘制 + 少量View的方式设计。其中绘制顺序及View的添加顺序决定了元素的显示层级。 事件体系: 如果是对装饰性View的操作,则直接采用对View设置监听事件的方式实现,对于自定义绘制的视频节点、转场等元素,则自定义事件的分发流程,按照元素的位置+层级来确定事件的分发优先级。 动画体系: 从节点、装饰元素、控件等多个层面,和点击、位移、滚动等多种事件类型,提供自定义动画支持。 性能优化: 绘制优化:采用只绘制处于屏幕内的内容的方式来提升控件的绘制性能。 抽帧优化:设计了抽帧组件以及帧图片的两级缓存框架,从而提高取帧的性能。 4.2.4 Redo/Undo功能的设计 视频编辑页中,用户会频繁的对视频、音频、特效等进行各种不同的编辑操作,而允许对用户的操作进行撤销和重做就成了非常必要的事情,该功能提高了视频编辑操作的容错性。 通常Redo/Undo功能有两种不同的设计思路:备忘录模式和命令行模式。在平衡性能、实施成本和实际效果后,我们选择了备忘录模式进行功能的设计和实施。 备份数据: 备份数据为视频编辑项目数据对象,其是对当前视频编辑项目的详细描述,是渲染引擎和核心界面状态的数据来源,通过备份和重置项目数据,即可使整个编辑页回到某个指定的状态。 二级缓存机制: 设置内存缓存和磁盘缓存两级存储结构。内存缓存作为一级缓存,提供高速访问的能力。磁盘缓存作为二级缓存,提供大容量存储。 滑动窗口与链式结构: 在内存缓存中设置一个滑动窗口,备份数据采用双向链表结构存储,指针始终指向第三个备份。 问题和更优的方案: 该功能目前的设计方案是时间方面妥协的产物,并不是视频编辑页的Redo/Undo功能的最佳方案,其存在全量刷UI、内存占用等方面的问题。 在编辑页采用MVVM+UDF的架构设计现状下,该功能天然适配命令行模式,用户的行为或意图(Action/Intent)即视频编辑的命令(Command),其经过命令执行后,带来界面状态(UiState)和渲染引擎的变化,与UDF的理念非常吻合,无疑是更加合理的、更符合页面架构的设计方案。 5. 总结 视频编辑页作为业务复杂度较高的单页面,经过多次需求迭代,最终完成所有业务升级,前后历时半年,代码修改量9w+。本次业务升级的顺利完成是产品、设计、测试和研发等多个团队紧密协作、齐心合作的结果。 经过本次编辑器改版,个人对于软件开发和架构设计也有一些新的感悟。 什么时候该进行代码重构或架构升级? 局部重构应该是一件持续进行的事情,最好的时间就是现在。而颠覆式的架构升级应该全面评估业务影响和ROI,在没有专门的时间进行架构升级时,跟随重大的需求变更同步进行架构演进,会更容易落地。 软件架构设计的重点是什么? 软件架构的设计并不是越复杂、越精细就会越好。过于复杂的设计只会让代码维护成本增加。执着于细节只会陷入无穷无尽的业务逻辑中。抓住主要问题、结合业务特点、确立架构设计原则、团队达成共识是更为重要的事情。 -End- 作者丨阿建 开发者问答 对于移动端的页面架构设计,大家有什么好的想法呢? 欢迎在留言区分享你的见解~ 转发本文至朋友圈并留言,即可参与 下方抽奖 ⬇️ 小编将抽取1位幸运的小伙伴获取 JOJO的奇妙冒险 石之海 冷水杯 抽奖截止时间:8月8日12:00 如果喜欢本期内容的话,欢迎点个“在看”吧!
往期精彩指路 阅读原文
跳转微信打开