在 Kanzi Android framework (droidfw) 中观察状态变化和处理事件。

观察状态更改

您可以实现 KanziViewListener 来监听 KanziView 中的状态变化。使用此方法注入自定义初始化和反初始化逻辑。

例如:

  • 使用 KanziViewListener.onAttachedToWindow 回调来访问域,注册 Kanzi Engine Java 插件或手动设置视图的节点树。

    view.addListener(new KanziViewListener() {
       ...
    
       @Override
       public void onAttachedToWindow(View view, Domain domain)
       {
             // Register a plugin.
             domain.registerPlugin(new MyPlugin());
    
             // Manually setup the node tree.
             // Instantiate a node.
             ObjectRef<TextBlock2D> text = TextBlock2D.create(domain, "text");
             // Set as root of the view.
             ((KanziView) view).setRoot(text.get());
             // Set properties on a node.
             text.get().setProperty(TextBox2D.TextProperty, "Hello Kanzi!");
    
    view.addListener(object : KanziViewListener {
       ...
    
       override fun onAttachedToWindow(view: View, domain: Domain) {
             // Register a plugin.
             domain.registerPlugin(MyPlugin())
    
             // Manually setup the node tree.
             // Instantiate a node.
             val text = TextBlock2D.create(domain, "text")
             // Set as root of the view.
             (view as KanziView).root = text.get()
             // Set properties on a node.
             text.get().setProperty(TextBox2D.TextProperty, "Hello Kanzi!")
    
  • 使用 KanziViewListener.onStartupPrefabLoaded 回调来访问和修改您配置的启动预设实例化的节点树。

    view.addListener(new KanziViewListener() {
       ...
    
       @Override
       public void onStartupPrefabLoaded(View view, Domain domain)
       {
             // Look up a child node starting from the root.
             TextBox2D text = ((KanziView)view).getRoot().lookupNode("#Text");
             // Set properties on a node.
             text.setProperty(TextBox2D.TextProperty, "Hello world!");
    
    view.addListener(object : KanziViewListener {
       ...
    
       override fun onStartupPrefabLoaded(view: View, domain: Domain) {
             // Look up a child node starting from the root.
             val text = (view as KanziView).root.lookupNode<TextBox2D>("#Text")
             // Set properties on a node.
             text.setProperty(TextBox2D.TextProperty, "Hello world!")
    

要了解有关 Kanzi Engine Java 插件的更多信息,请参阅 创建 Kanzi Engine Java 插件。

处理输入

默认情况下,KanziSurfaceView 和 KanziTextureView 处理输入事件。如果您希望 Kanzi Engine 处理使用 KanziViewAdapter 的任意视图的事件,则必须手动转发此类输入事件。为此,请使用这些接口:

默认情况下,所有 Kanzi 视图将 focusableInTouchMode 设置为 True,除非通过编程或布局属性另行设置。这使得它们在被点击时获得并保持焦点。请参阅 Android touch mode。

处理来自 Android Automotive 旋转控制器的输入

默认情况下,KanziSurfaceView 和 KanziTextureView 处理旋转控制器输入。

旋转操作

Kanzi 如何处理这些操作

../../../_images/rotary-input-nudge.png
  • 微调向上

  • 微调向下

  • 微调左移

  • 右微调

Kanzi Android framework (droidfw) 将这些操作处理为相应的箭头键事件。

Kanzi 不支持并忽略来自对角微调的输入。

../../../_images/rotary-input-center.png

居中按钮

Kanzi Android framework (droidfw) 尝试为聚焦的 Kanzi 视图启用直接操作模式。如果 Kanzi Android framework (droidfw) 无法启用直接操作模式,它会将按键事件转发给 Kanzi Engine。Kanzi Engine 将旋转中心按钮的按键代码 KeyEvent.KEYCODE_DPAD_CENTER 转换为 Enter 逻辑按键。请参阅 Direct manipulation mode。

../../../_images/rotary-input-back.png

上一步按钮 (Back button)

Kanzi Android framework (droidfw) 将键事件转发到 Kanzi Engine。如果 Kanzi Engine 不消费该事件,Kanzi Android framework (droidfw) 将禁用 Kanzi 视图的直接操作模式。

../../../_images/rotary-input-rotate.png
  • 逆时针旋转

  • 顺时针旋转

Android 发送一个通用运动事件,其中包含旋转方向和按压次数。

Kanzi 视图将所有通用运动事件转发到 KanziViewAdapter.handleGenericMotionEvent。

KanziViewAdapter 确定事件是否来自旋转控制器,并检查 Kanzi 视图是否处于直接操作模式。如果 Kanzi 视图处于直接操作模式,Kanzi 视图将旋转信息转发给 Kanzi Engine。Kanzi Engine 翻译:

  • 顺时针旋转进入 Tab 键事件。

  • 逆时针旋转进入 BackTab 键事件。

要禁用默认的旋转控制器输入处理,使用以下方法之一:

  • 在布局文件中,为 Kanzi 视图元素,将 rotaryHandlingEnabled 属性设置为 false:

    <com.rightware.kanzi.KanziSurfaceView
        android:id="@+id/rotaryview"
        app:name="Rotary"
        app:kzbPathList="rotary.kzb"
        app:startupPrefabUrl="kzb://rotary/StartupPrefab"
        app:rotaryHandlingEnabled="false"/>
    
  • 在活动代码中,将 false 传递给 KanziViewAdapter.setRotaryHandlingEnabled 函数调用:

    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.activity_main);
        mView = findViewById(R.id.rotaryview);
        mView.getAdapter().setRotaryHandlingEnabled(false);
    }
    

处理方向更改

当屏幕方向发生变化时,Android 应用程序默认情况下会销毁并重新创建活动。这允许应用程序为纵向和横向屏幕使用备用布局,但这也意味着您会丢失所有内部 Kanzi 状态。

您可以通过在 AndroidManifest.xml 文件中添加 android:configChanges="orientation" 属性来覆盖此设置。使用此选项,活动在方向更改时不再被销毁,而是在活动上调用 onConfigurationChanged()。手动将此事件转发到 KanziView.handleOrientationChange。此选项避免重新创建 Kanzi 控件,因此保留了所有内部状态。请参阅 Android 配置更改。

您可以在视图拥有表面之前或视图失去表面之后转发该事件。在这种情况下,Kanzi 会忽略该事件。

该事件不会旋转 Kanzi 内容。要设置 Kanzi 内容的方向,请使用 屏幕节点 的 Orientation 属性。

处理 Android 生命周期

虽然 KanziSurfaceView 和 KanziTextureView 默认情况下处理窗口和表面事件,但使用 KanziViewAdapter 的任意视图必须通过 KanziViewAdapter.handleAttachedToWindow、KanziViewAdapter.handleSurfaceCreated、KanziViewAdapter.handleSurfaceChanged、KanziViewAdapter.handleSurfaceDestroyed 和 KanziViewAdapter.handleDetachedFromWindow 接口手动转发它希望 Kanzi 处理的窗口和表面事件。

处理视图的用户可见性

Kanzi Android framework (droidfw) 会自动持续跟踪每个已连接的视图对用户是否可见,并停止隐藏视图的 rendering 和输入处理。

要检查视图是否对用户可见,Kanzi Android framework (droidfw) 考虑:

  • 视图的可见性状态

  • 视图的所有祖先的可见性状态

  • 视图的 alpha 值

  • 通过 View.getGlobalVisibleRect() 计算的视图的有效可见部分。

使用 KanziViewAdapter 的任意视图必须手动转发可见性事件,以便通过 KanziViewAdapter.handleVisibilityChange 接口处理 Kanzi。

空闲状态

Kanzi Android framework (droidfw) 在没有工作时会自动挂起不必要的操作。当至少一个 Kanzi 视图处于活动状态时,Kanzi Android framework (droidfw) 会在有工作可做时唤醒。当没有视图处于活动状态时,Kanzi Android framework (droidfw) 不运行任何帧。请参阅 应用程序空闲状态 和 在没有视图处于活动状态时提交任务。

为了在暂停期间检测来自 Kanzi 主循环外部的更改,Kanzi Android framework (droidfw) 可以:

  • 订阅 Android MessageQueue.IdleHandler 事件。

  • 在 Android Choreographer 上运行轻量级任务。

默认情况下,Kanzi Android framework (droidfw) 订阅 Android 空闲事件,并在 Android 应用程序进入空闲状态时检查外部更改。在此模式下,Kanzi 在暂停期间的 CPU 使用率真正降为零。

您可以通过调用 KanziRuntime.enableSuspendedChangeTrackingTask 将 Kanzi 切换为使用后台任务。在此模式下,Kanzi 以通过 KanziRuntime.setSuspendedChangeCheckRate 设置的频率在 Android Choreographer 上运行轻量级任务。通过更改此频率,您可以控制 Kanzi 检查外部更改的频率。默认值为 0 时,Kanzi 以显示器 vsync 频率跟踪外部更改。您可以进一步降低此频率及相关的 CPU 消耗,但代价是在有工作时唤醒会延迟。此频率独立于 rendering 和动画的频率,后两者由帧率限制控制。

在没有视图处于活动状态时提交任务

Kanzi Android framework (droidfw) 仅在至少一个 Kanzi 视图处于活动状态时运行 Kanzi 主循环。Kanzi 视图在拥有表面且其可见性为 VISIBLE 时处于活动状态。当 KanziTextureView 的 activity 停止时,该视图也不再处于活动状态。仅因用户可见性而被 Kanzi Android framework (droidfw) 隐藏的视图(例如因为被另一个视图遮挡)仍保持活动状态。请参阅 处理视图的用户可见性。

当没有视图处于活动状态时,例如您的应用程序在后台时,Kanzi 不运行任何帧。在此期间您提交给 TaskDispatcher 的任务会处于等待状态。Kanzi 会在视图再次变为活动状态后的第一帧中执行所有这些任务。如果您的应用程序持续以高频率提交任务,例如每次数据源更新时都提交任务,这些任务就会堆积。它们会使视图的第一帧延迟,延迟时长为执行所有这些任务所需的时间。

在没有视图处于活动状态期间,不要为每次更新都提交任务。而应保留每个数据项的最新值,并且仅在没有等待中的应用这些值的任务时才提交任务。这样,无论没有视图处于活动状态的时间有多长,最多只有一个任务处于等待状态。例如:

private final AtomicReference<String> mLatestValue = new AtomicReference<>();
private final AtomicBoolean mTaskPending = new AtomicBoolean(false);

// Set this to domain.getTaskDispatcher(), for example in
// KanziViewListener.onAttachedToWindow. Start the updates only after that.
private volatile TaskDispatcher mTaskDispatcher;

// Called from any thread for each update of the data source.
void onDataChanged(String value)
{
    mLatestValue.set(value);
    if (mTaskPending.compareAndSet(false, true))
    {
        mTaskDispatcher.submit(() -> {
            mTaskPending.set(false);
            // Set the latest value to a Kanzi object, for example a data object.
            applyValue(mLatestValue.get());
        });
    }
}
private val latestValue = AtomicReference<String>()
private val taskPending = AtomicBoolean(false)

// Set this to domain.taskDispatcher, for example in
// KanziViewListener.onAttachedToWindow. Start the updates only after that.
@Volatile
private lateinit var taskDispatcher: TaskDispatcher

// Called from any thread for each update of the data source.
fun onDataChanged(value: String) {
    latestValue.set(value)
    if (taskPending.compareAndSet(false, true)) {
        taskDispatcher.submit {
            taskPending.set(false)
            // Set the latest value to a Kanzi object, for example a data object.
            applyValue(latestValue.get())
        }
    }
}