Scene導航和頁面切分組件庫
Scene 是字節(jié)跳動開源的一個基于 View 的輕量級導航和頁面切分組件庫,主要特性:
- 簡單方便的頁面導航和棧管理,支持MultiStack
- 完善的生命周期的管理和分發(fā)
- 可以更簡單的實現(xiàn)復雜的過場動畫
- 支持對Activity和Window屬性的修改和恢復
- 支持頁面之間拿返回值,支持在Scene中申請權限
- 支持頁面銷毀時保存狀態(tài)和恢復
介紹
Scene 旨在導航和頁面切分上替代Activity和Fragment的使用。
Activity目前存在的主要問題:
- 棧管理弱,Intent和LaunchMode混亂,即使各種Hack仍然不能完全避免黑屏等問題
- Activity的性能較差,普通的空白頁面啟動也平均60ms以上(三星S9測試)
- 因為Activity被強制需要支持銷毀恢復,導致了一些問題:
- 轉場動畫能力有限,無法實現(xiàn)較復雜的交互動畫,
- 共享元素動畫基本不可用,有Framework層的崩潰無法解決
- 每次啟動新的Activity,都需要上個頁面執(zhí)行完onSaveInstance,損失性能
- Activity依賴Manifest文件導致注入困難,動態(tài)化需要各種Hack
Fragment目前存在的主要問題:
- 官方長期無法解決的崩潰較多,即使不用Fragment,在AppCompatActivity的onBackPressed()中仍然可能觸發(fā)崩潰
- add/remove/hide/show操作不是立刻執(zhí)行,在嵌套時即使使用commitNow也不能保證子Fragment狀態(tài)更新
- 動畫支持糟糕,頁面切換時無法保證Z軸順序
- 導航功能很弱,除了基本的打開和關閉,高級的棧管理
- 原生Fragment和Support v4包中的Fragment的生命周期并不完全相同
Scene框架嘗試去解決上面提到的Activity和Fragment存在的問題
提供簡單可靠、易擴展的API,來實現(xiàn)一套輕量的導航和頁面切分解決方案
同時我們提供了一系列的遷移方案,來幫助開發(fā)者漸進式地從Activity和Fragment遷移到Scene。
Get Started
在依賴中添加:
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' implementation 'com.bytedance.scene:scene-ktx:$latest_version'
Scene有2個子類:NavigationScene和GroupScene,其中:
- NavigationScene支持頁面切換
- GroupScene支持頁面切分
| Scene | NavigationScene | GroupScene |
|---|---|---|
簡單的接入,讓主Activity繼承于SceneActivity即可:
class MainActivity : SceneActivity() { override fun getHomeSceneClass(): Class<out Scene> { return MainScene::class.java } override fun supportRestore(): Boolean { return false } }
一個簡單的Scene示例:
class MainScene : AppCompatScene() { private lateinit var mButton: Button override fun onCreateContentView(inflater: LayoutInflater, container: ViewGroup, savedInstanceState: Bundle?): View? { val frameLayout = FrameLayout(requireSceneContext()) mButton = Button(requireSceneContext()) mButton.text = "Click" frameLayout.addView(mButton, FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)) return frameLayout } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) setTitle("Main") toolbar?.navigationIcon = null mButton.setOnClickListener { navigationScene?.push(SecondScene()) } } } 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 } }
Migration to Scene
一個新的App可以通過直接繼承SceneActivity的方式接入Scene,
但如果已有的Activity不方便更改繼承關系,則可參考SceneActivity的代碼直接使用SceneDelegate來處理,
以西瓜視頻的首頁遷移方案為例:
首先在首頁的XML申明一個存放Scene的布局:scene_container
<?xml version="1.0" encoding="utf-8"?> <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <...> <...> <!-- 上面是這個Activity的已有布局 --> <FrameLayout android:id="@+id/scene_container" android:layout_width="match_parent" android:layout_height="match_parent" /> </merge>
再創(chuàng)建一個透明的Scene作為根Scene
public static class EmptyHolderScene extends Scene { @NonNull @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return new View(getActivity()); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getView().setBackgroundColor(Color.TRANSPARENT); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ArticleMainActivity activity = (ArticleMainActivity) requireActivity(); activity.createSceneLifecycleCallbacksToDispatchLifecycle(getNavigationScene()); } }
綁定這個透明的Scene到 R.id.scene_container
mSceneActivityDelegate = NavigationSceneUtility.setupWithActivity(this, R.id.scene_container, null, new NavigationSceneOptions().setDrawWindowBackground(false) .setFixSceneWindowBackgroundEnabled(true) .setSceneBackground(R.color.material_default_window_bg) .setRootScene(EmptyHolderScene.class, null), false);
實質上是有個透明的Scene蓋在首頁,但是視覺上看不出來
然后在Activity中提供Push的方法
public void push(@NonNull Class<? extends Scene> clazz, @Nullable Bundle argument, @Nullable PushOptions pushOptions) { if (mSceneActivityDelegate != null) { mSceneActivityDelegate.getNavigationScene().push(clazz, argument, pushOptions); } }
這樣就基本遷移完成,可以在這個Activity中直接打開新的Scene頁面了。
Issues
由于Scene是基于View來實現(xiàn)其功能的,有一些已知但暫時無法解決的問題:
Dialog
一個正常Dialog的Window是獨立于并蓋在Activity的Window之上的,
所以如果在Dialog中點擊打開一個Scene,就會導致Scene出現(xiàn)在Dialog后面。
可以選擇點擊的時候關閉對話框,也可以選擇使用Scene來實現(xiàn)對話框,來替代系統(tǒng)的Dialog。
SurfaceView and TextureView
在Scene返回時,會先執(zhí)行Scene的生命周期后執(zhí)行動畫,
但是如果遇到SurfaceView/TextureView,這個過程會導致SurfaceView/TextureView黑屏,
對于TextureView可以選擇結束前,獲得Surface,動畫前把這個Surface重新賦值
對于SurfaceView,結束前,捕獲Bitmap,設置到ImageView,這個過程中因為涉及大的Bitmap創(chuàng)建,
可以Try catch,然后在動畫結束后回收這個Bitmap。
Status Bar related
劉海屏在Android P之前沒有官方API,各個廠商有自己的實現(xiàn)
如果用Window Flag或View UiVisibility來隱藏狀態(tài)欄圖標,都會引發(fā)整個Activity的重新布局,
這同時也會導致Scene頁面的位置變化,某些情況下可能會有不符合預期的行為
