Scene 最初用于解决西瓜视频的直播业务在演进过程中遇到的问题,后来又在抖音的拍摄工具中落地,经过了实践与验证,于是团队觉得将其开源到社区,希望能够帮助大家在更多的场景解决问题。
Github 项目地址与使用文档: https://github.com/bytedance/scene 。
西瓜视频在 1.0.8 版本有做过一次播放体验的优化,希望首页正在播放的短视频跳转到详情页面时,能够有一个平滑的动画过渡。
下面的视频是老版本的过度效果:
下面的视频是新版本的过度效果:
这种复杂的过渡动画,是不可能拿 Activity 实现的。然而 Fragment 在那个时候也会出现各种怪异的状态保存引发的崩溃(虽然知道崩溃的原理,但是不能接受这种设计),于是西瓜视频技术团队设计了名为 Page 的 UI 方案,来实现过渡动画这个需求。
但是 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) (https://www.youtube.com/watch?v=2k8x8V77CrU)
Fragments: Past, Present, and Future (Android Dev Summit '19)(https://www.youtube.com/watch?v=RS1IACnZLy4)
Conductor (https://github.com/bluelinelabs/Conductor)
Uber RIBs (https://github.com/uber/RIBs)