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)
歡迎關注「字節跳動技術團隊」