Scene 是字节跳动西瓜视频技术团队开源的一款 Android 页面导航和组合框架,用于实现 Single Activity Applications,有着灵活的栈管理,页面拆分,以及完整的各类动画支持。java
Scene 最初用于解决西瓜视频的直播业务在演进过程当中遇到的问题,后来又在抖音的拍摄工具中落地,通过了实践与验证,因而团队以为将其开源到社区,但愿可以帮助你们在更多的场景解决问题。android
Github 项目地址与使用文档:https://github.com/bytedance/scene。git
西瓜视频在 1.0.8 版本有作过一次播放体验的优化,但愿首页正在播放的短视频跳转到详情页面时,可以有一个平滑的动画过渡。github
下面的视频是老版本的过分效果:app
下面的视频是新版本的过分效果:框架
这种复杂的过渡动画,是不可能拿 Activity 实现的。然而 Fragment 在那个时候也会出现各类怪异的状态保存引起的崩溃(虽然知道崩溃的原理,可是不能接受这种设计),因而西瓜视频技术团队设计了名为 Page 的 UI 方案,来实现过渡动画这个需求。ide
可是 Page 自己跟业务耦合很是严重,无法单独抽出去给其余场景用。后来,随着西瓜直播业务的壮大,也有了须要相似框架的需求,为了解决 Activity 栈管理太弱、各类黑屏、动画能力太弱等问题,同时解决 Fragment 崩溃过多问题,咱们开发了 Scene 这套通用的框架。工具
下面是西瓜长视频详情页和抖音拍摄页面使用Scene的场景截图:组件化
这里简单列下 Activity 和 Support 28 的 Fragment 的不足,部分问题已经在 Android X 的 Fragment 上修复了。布局
java.lang.NullPointerException(android.app.EnterTransitionCoordinator);
复制代码
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
复制代码
对于这种状况,西瓜直接在本身的 Activity 基类对 super.onBackPressed()
进行了try catch。
getChildFragmentManager().executePendingTransactions()
后,开发者会误觉得 Child Fragment 都已经切到最新的 Parent Fragment 状态,其实并无;onClick
回调依然继续触发,致使回调内部不得不补大量的判空逻辑;if (getActivity() == null) {
return;
}
复制代码
Scene 提供页面导航、页面组合两大功能,特色以下:
Scene 框架有3种基本组件:Scene、NavigationScene、GroupScene。
用处 | |
---|---|
Scene | 全部 Scene 的基类,带生命周期和 View 支持的组件 |
NavigationScene | 支持页面导航 |
GroupScene | 支持将任何 Scene 组合 |
Scene
NavigationScene
GroupScene
这里介绍简单的上手,更多用法见 Github 仓库的示例。
添加依赖:
dependencies {
implementation 'com.bytedance.scene:scene:$latest_version'
implementation 'com.bytedance.scene:scene-ui:$latest_version'
implementation 'com.bytedance.scene:scene-shared-element-animation:$latest_version'
// Kotlin
implementation 'com.bytedance.scene:scene-ktx:$latest_version'
}
复制代码
建立首页:
class MainScene : AppCompatScene() {
override fun onCreateContentView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View? {
return View(requireSceneContext())
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setTitle("Main")
toolbar?.navigationIcon = null
}
}
复制代码
建立 Activity:
class MainActivity : SceneActivity() {
override fun getHomeSceneClass(): Class<out Scene> {
return MainScene::class.java
}
override fun supportRestore(): Boolean {
return false
}
}
复制代码
添加到 Manifest.xml,注意把输入法模式也改了:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
复制代码
运行就能够了。
这是新应用想所有使用 Scene 写的方式。若是是老应用重构迁移,或者只想用页面组合替代 Fragment,导航依旧用 Activity 的作法,能够见 Github 的 Demo。
打开新页面:
requireNavigationScene().push(TargetScene::class.java) 复制代码
返回:
requireNavigationScene().pop()
复制代码
打开页面拿结果:
requireNavigationScene().push(TargetScene::class.java, null, PushOptions.Builder().setPushResultCallback { result ->
}
}.build())
复制代码
设置结果:
requireNavigationScene().setResult(this@TargetScene, YOUR_RESULT)
复制代码
组合的 API 相似 Fragment,继承 GroupScene,而后能够操做任意 Scene 添加到本身的 View 布局内:
void add(@IdRes int viewId, @NonNull Scene childScene, @NonNull String tag);
void remove(@NonNull Scene childScene);
void show(@NonNull Scene childScene);
void hide(@NonNull Scene childScene);
@Nullable
<T extends Scene> T findSceneByTag(@NonNull String tag);
复制代码
示例:
class SecondScene : AppCompatScene() {
private val mId: Int by lazy { View.generateViewId() }
override fun onCreateContentView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View? {
val frameLayout = FrameLayout(requireSceneContext())
frameLayout.id = mId
return frameLayout
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setTitle("Second")
add(mId, ChildScene(), "TAG")
}
}
class ChildScene : Scene() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
val view = View(requireSceneContext())
view.setBackgroundColor(Color.GREEN)
return view
}
}
复制代码
Scene 支持 ViewModel,能够经过 by activityViewModels,by viewModels 拿到托管到 Activity 或者本身的 ViewModel:
class ViewModelSceneSamples : GroupScene() {
private val viewModel: SampleViewModel by activityViewModels()
复制代码
示例:
class ViewModelSceneSamples : GroupScene() {
private val viewModel: SampleViewModel by activityViewModels()
private lateinit var textView: TextView
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel.counter.observe(this, Observer<Int> { t -> textView.text = "" + t })
add(R.id.child, ViewModelSceneSamplesChild(), "Child")
}
}
class ViewModelSceneSamplesChild : Scene() {
private val viewModel: SampleViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View {
return Button(requireSceneContext()).apply {
text = "Click to +1"
}
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
requireView().setOnClickListener {
val countValue = viewModel.counter.value ?: 0
viewModel.counter.value = countValue + 1
}
}
}
class SampleViewModel : ViewModel() {
val counter: MutableLiveData<Int> = MutableLiveData()
}
复制代码
在 Push 的时候,经过 PushOptions 能够配置简单的过场动画:
val enter = R.anim.slide_in_from_right
val exit = R.anim.slide_out_to_left requireNavigationScene().push(TargetScene::class.java, null, PushOptions.Builder().setAnimation(requireActivity(), enter, exit).build()) 复制代码
复杂的共享元素动画,手势动画,参考 Demo。
Scene 内置右划返回手势,你直接继承 AppCompatScene,而后打开手势:
setSwipeEnabled(true)
复制代码
Scene Router,开发中,以即可以支持流行的 Android 组件化开发。
Scene Dialog,开发中,用于解决 Android 框架的 Dialog 由于是基于 Window 会盖在普通的 View 之上的问题。
关于单 Activity 的想法,业界早在 Fragment 刚推出的时候就有探讨,社区诞生了 Conductor 之类的框架,甚至这2年,Google 官方也在作 Navigation Component,可是毕竟 Fragment 的坑太大,基于Fragment 作导航,总免不了受限于 Fragment 的兼容性,以致于后来,Google 为了解决这些兼容性问题,直接打算魔改 Fragment,废掉以前用了不少年的接口。
基于 View 从新实现的导航和组合方案,一方面是没有以前的技术债,一方面能够跳出 Google 的想法,好比说能够控制状态保存的范围,来实现更增强大的动画能力和组件通信能力,这是官方的组件不会提供给开发者的。
仓库中的 Demo,已经把 Android 平常开发中大部分场景都补了示例,没有在本文中列出来的功能,能够参考 Demo 的写法。
Single Activity: Why, When, and How (Android Dev Summit '18)
Fragments: Past, Present, and Future (Android Dev Summit '19)
欢迎关注「字节跳动技术团队」