Scene

导航和页面切分组件库
授权协议 Apache 2.0
开发语言 Java
所属分类 手机/移动开发、 Android UI 组件
软件类型 开源软件
地区 国产
投 递 者 司空宗清
操作系统 跨平台
开源组织
适用人群 未知
 软件概览

Scene 是字节跳动开源的一个基于 View 的轻量级导航和页面切分组件库,主要特性:

  1. 简单方便的页面导航和栈管理,支持MultiStack
  2. 完善的生命周期的管理和分发
  3. 可以更简单的实现复杂的过场动画
  4. 支持对Activity和Window属性的修改和恢复
  5. 支持页面之间拿返回值,支持在Scene中申请权限
  6. 支持页面销毁时保存状态和恢复

介绍

Scene 旨在导航和页面切分上替代Activity和Fragment的使用。

Activity目前存在的主要问题:

  1. 栈管理弱,Intent和LaunchMode混乱,即使各种Hack仍然不能完全避免黑屏等问题
  2. Activity的性能较差,普通的空白页面启动也平均60ms以上(三星S9测试)
  3. 因为Activity被强制需要支持销毁恢复,导致了一些问题:
    • 转场动画能力有限,无法实现较复杂的交互动画,
    • 共享元素动画基本不可用,有Framework层的崩溃无法解决
    • 每次启动新的Activity,都需要上个页面执行完onSaveInstance,损失性能
  4. Activity依赖Manifest文件导致注入困难,动态化需要各种Hack

Fragment目前存在的主要问题:

  1. 官方长期无法解决的崩溃较多,即使不用Fragment,在AppCompatActivity的onBackPressed()中仍然可能触发崩溃
  2. add/remove/hide/show操作不是立刻执行,在嵌套时即使使用commitNow也不能保证子Fragment状态更新
  3. 动画支持糟糕,页面切换时无法保证Z轴顺序
  4. 导航功能很弱,除了基本的打开和关闭,高级的栈管理
  5. 原生Fragment和Support v4包中的Fragment的生命周期并不完全相同

Scene框架尝试去解决上面提到的Activity和Fragment存在的问题

提供简单可靠、易扩展的API,来实现一套轻量的导航和页面切分解决方案

同时我们提供了一系列的迁移方案,来帮助开发者渐进式地从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,其中:

  1. NavigationScene支持页面切换
  2. 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>

再创建一个透明的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来实现其功能的,有一些已知但暂时无法解决的问题:

Dialog

一个正常Dialog的Window是独立于并盖在Activity的Window之上的,

所以如果在Dialog中点击打开一个Scene,就会导致Scene出现在Dialog后面。

可以选择点击的时候关闭对话框,也可以选择使用Scene来实现对话框,来替代系统的Dialog。

SurfaceView and TextureView

在Scene返回时,会先执行Scene的生命周期后执行动画,

但是如果遇到SurfaceView/TextureView,这个过程会导致SurfaceView/TextureView黑屏,

对于TextureView可以选择结束前,获得Surface,动画前把这个Surface重新赋值

对于SurfaceView,结束前,捕获Bitmap,设置到ImageView,这个过程中因为涉及大的Bitmap创建,

可以Try catch,然后在动画结束后回收这个Bitmap。

Status Bar related

刘海屏在Android P之前没有官方API,各个厂商有自己的实现

如果用Window Flag或View UiVisibility来隐藏状态栏图标,都会引发整个Activity的重新布局,

这同时也会导致Scene页面的位置变化,某些情况下可能会有不符合预期的行为

 

 相关资料
  • 对于搜索引擎或电子商务网站,常常将信息分页显示,这样可以减少页面大小,进而提高页面的加载速度。分页显示后,就需要通过分页导航来告诉用户要浏览的信息量,方便用户快速跳过一些不想看的信息,也便于定位和查找。因此,分页导航也是很常见的、很重要的一种导航。 实际上,分页导航的制作方法也很简单。分页导航一般包括上一页、页码、下一页三部分。首先,创建一个容器,来包裹分页导航的链接。如: <div class

  • 导航组件说明 组件 说明 最低版本 navigator 页面链接 1.0.0 functional-page-navigator 用于跳转插件功能页 不支持 navigator 属性 类型 默认值 必填 说明 最低版本 url string 否 当前小程序内的跳转链接 open-type navigate 否 跳转方式 hover-class string navigator-hover 否 指定

  • 我正在一个新的Android应用程序上使用导航组件,但我不知道怎么做 首先,我有我的主活动,我有main_navigation_graph 主要活动 NavHostFragment main_navigation_graph里面有3个碎片 这里一切都很好。问题是当我到达最后一个片段时,因为在这个片段上,我想根据BottomNavigationView输入(暂时)显示一些子片段(在新的NavHost

  • 导航规则是JSF Framework提供的那些规则,用于描述单击按钮或链接时要显示的视图。 可以在名为faces-config.xml的JSF配置文件中定义导航规则。 它们可以在托管bean中定义。 导航规则可以包含可以显示结果视图的条件。 JSF 2.0也提供隐式导航,其中不需要定义导航规则。 隐式导航 JSF 2.0提供了名为implicit navigation auto view page

  • 我有一个非常简单的应用程序,由三个片段和一个底部导航栏组成,使用“New Project”创建- 有没有“标准”的方法来做到这一点? 我尝试过: 使用导航组件的,它似乎启动了带有自己的后栈的片段并破坏了底部导航。 以不同的方式使用,这要么导致异常,要么以不同的方式破坏底部导航。 在这篇帖子中,有人问了完全相同的问题,但被标记为重复。我没有找到答案,尤其是关于导航组件。

  • 问题内容: 我已经使用facelets模板完成了一个基本的JSF应用程序。我的模板如下: 然后,我有一个 主页 (如下所示),它导航到 第二页 。两个页面都使用上面的模板。 如果我在faces-config.xml中使用它,它将进行导航,但是将重新加载 整个 页面。我的问题是: 有没有一种方法可以从页面导航到另一个仅更新模板部分的内容?(同时保留页面的其余部分不变) 谢谢! 问题答案: 您可以通过

  • 实际上,我正在我的应用程序中使用新的架构组件,并且我已经设置了导航组件。我有一个导航抽屉,我想用它。我已经设置好了,但我面临一些问题: 1-抽屉不会自动关闭。菜单正常工作并导航到正确的位置,但导航后不会关闭。我必须添加一个目的地ChangedListener才能自己关闭它。 在代码实验室里,抽屉自动关闭,我真的不明白为什么。 2-向上按钮打开抽屉。当我导航到非顶级片段时,菜单图标变为向上箭头,但当

  • Navigate the Scene View quickly and efficiently with the Scene View controls. 利用场景视图控制器快速有效地操控场景视图。 Arrow Movement 用箭头移动 Use Arrow Keys to move around the scene. Holding down Shift will make you move