Android NDK开发详解大屏设备之让应用具备折叠感知能力

Android NDK开发详解大屏设备之让应用具备折叠感知能力

    • 窗口信息
      • Kotlin Flow
      • Java 回调
      • RxJava 支持
    • 可折叠设备显示屏的功能
    • 窗口大小变化
    • 其他资源
      • 示例
      • Codelab

借助展开的大显示屏和独特的折叠状态,我们能够在可折叠设备上打造全新用户体验。如需让应用具备折叠感知能力,请使用 Jetpack WindowManager 库,它为可折叠设备的窗口功能(如折叠边或合页)提供了一个 API surface。如果应用具备折叠感知能力,就能调整其布局,避免将重要内容放在折叠边或合页区域,并将折叠边或合页用作自然分隔符。

窗口信息

Jetpack WindowManager 中的 WindowInfoTracker 接口会公开窗口布局信息。该接口的 windowLayoutInfo() 方法会返回一个 WindowLayoutInfo 数据流,该数据流会将可折叠设备的折叠状态告知您的应用。WindowInfoTracker getOrCreate() 方法会创建一个 WindowInfoTracker 实例。

WindowManager 支持通过 Kotlin Flow 和 Java 回调收集 WindowLayoutInfo 数据。

Kotlin Flow

如需开始和停止 WindowLayoutInfo 数据收集,您可以使用可重启的生命周期感知型协程,在生命周期至少为 STARTED 时,系统会执行 repeatOnLifecycle 代码块,当生命周期为 STOPPED 时停止执行。当生命周期再次为 STARTED 时,系统会自动重新开始执行代码块。在以下示例中,代码块会收集并使用 WindowLayoutInfo 数据:

class DisplayFeaturesActivity : AppCompatActivity() {

    private lateinit var binding: ActivityDisplayFeaturesBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView(binding.root)

        lifecycleScope.launch(Dispatchers.Main) {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                    .windowLayoutInfo(this@DisplayFeaturesActivity)
                    .collect { newLayoutInfo ->
                        // Use newLayoutInfo to update the layout.
                    }
            }
        }
    }
}

Java 回调

借助 androidx.window:window-java 依赖项中包含的回调兼容性层,您无需使用 Kotlin Flow 即可收集 WindowLayoutInfo 更新。该工件包含 WindowInfoTrackerCallbackAdapter 类,该类会调整 WindowInfoTracker 来支持注册(和取消注册)用于接收 WindowLayoutInfo 更新的回调,例如:

public class SplitLayoutActivity extends AppCompatActivity {

    private WindowInfoTrackerCallbackAdapter windowInfoTracker;
    private ActivitySplitLayoutBinding binding;
    private final LayoutStateChangeCallback layoutStateChangeCallback =
            new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoTracker =
                new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoTracker.addWindowLayoutInfoListener(
                this, Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoTracker
           .removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo newLayoutInfo) {
           SplitLayoutActivity.this.runOnUiThread( () -> {
               // Use newLayoutInfo to update the layout.
           });
       }
   }
}

RxJava 支持

如果您已在使用 RxJava(版本 2 或 3),您可以通过工件来使用 Observable 或 Flowable 收集 WindowLayoutInfo 更新,而无需使用 Kotlin Flow。

androidx.window:window-rxjava2 和 androidx.window:window-rxjava3 依赖项提供的兼容性层包含 WindowInfoTracker#windowLayoutInfoFlowable() 和 WindowInfoTracker#windowLayoutInfoObservable() 方法,使您的应用能够接收 WindowLayoutInfo 更新,例如:

class RxActivity: AppCompatActivity {

    private lateinit var binding: ActivityRxBinding

    private var disposable: Disposable? = null
    private lateinit var observable: Observable<WindowLayoutInfo>

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

        // Create a new observable
        observable = WindowInfoTracker.getOrCreate(this@RxActivity)
            .windowLayoutInfoObservable(this@RxActivity)
   }

   @Override
   protected void onStart() {
       super.onStart();

        // Subscribe to receive WindowLayoutInfo updates
        disposable?.dispose()
        disposable = observable
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe { newLayoutInfo ->
            // Use newLayoutInfo to update the layout
        }
   }

   @Override
   protected void onStop() {
       super.onStop();

        // Dispose the WindowLayoutInfo observable
        disposable?.dispose()
   }
}

可折叠设备显示屏的功能

Jetpack WindowManager 的 WindowLayoutInfo 类会以 DisplayFeature 元素列表的形式提供显示窗口的功能。

FoldingFeature 是一种 DisplayFeature,它提供了有关可折叠设备显示屏的信息,其中包括:

state:设备的折叠状态,即 FLAT 或 HALF_OPENED
orientation:折叠边或合页的方向,即 HORIZONTAL 或 VERTICAL
occlusionType:折叠边或合页是否遮住了显示屏的一部分,即 NONE 或 FULL
isSeparating:折叠边或合页是否创建了两个逻辑显示区域,即 true 或 false
注意:虽然可折叠设备上的合页允许设备折叠到各种角度,但 FoldingFeature 不会在 API 中公开相应角度。不同的设备有不同的报告范围,传感器的准确度也可能会因设备而异;因此,基于精确合页角度的动画或逻辑必须根据设备进行微调。
HALF_OPENED 的可折叠设备始终将 isSeparating 报告为 true,因为屏幕被分为两个显示区域。此外,当应用跨两块屏幕时,isSeparating 在双屏设备上始终为 true。

FoldingFeature bounds 属性(继承自 DisplayFeature)表示折叠功能的边界矩形(如折叠边或合页)。可以用边界将元素放到与功能相关的屏幕上。

请使用 FoldingFeature state 来确定设备是处于桌面模式还是图书模式,以便相应地自定义应用布局,例如:

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        // The block passed to repeatOnLifecycle is executed when the lifecycle
        // is at least STARTED and is cancelled when the lifecycle is STOPPED.
        // It automatically restarts the block when the lifecycle is STARTED again.
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from windowInfoRepo when the lifecycle is STARTED
            // and stops collection when the lifecycle is STOPPED
            WindowInfoTracker.getOrCreate(this@MainActivity)
                .windowLayoutInfo(this@MainActivity)
                .collect { layoutInfo ->
                    // New posture information
                    val foldingFeature = layoutInfo.displayFeatures
                        .filterIsInstance()
                        .firstOrNull()
                    when {
                            isTableTopPosture(foldingFeature) ->
                                enterTabletopMode(foldingFeature)
                            isBookPosture(foldingFeature) ->
                                enterBookMode(foldingFeature)
                            isSeparating(foldingFeature) ->
                            // Dual-screen device
                            if (foldingFeature.orientation == HORIZONTAL) {
                                enterTabletopMode(foldingFeature)
                            } else {
                                enterBookMode(foldingFeature)
                            }
                            else ->
                                enterNormalMode()
                        }
                }

        }
    }
}

@OptIn(ExperimentalContracts::class)
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}

@OptIn(ExperimentalContracts::class)
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}

@OptIn(ExperimentalContracts::class)
fun isSeparating(foldFeature : FoldingFeature?) : Boolean {
    contract { returns(true) implies (foldFeature != null) }
    return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating
}

Java

private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
                new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    ...
    windowInfoTracker =
            new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
    super.onStart();
    windowInfoTracker.addWindowLayoutInfoListener(
            this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
    super.onStop();
    windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
    @Override
    public void accept(WindowLayoutInfo newLayoutInfo) {
        // Use newLayoutInfo to update the Layout
        List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
        for (DisplayFeature feature : displayFeatures) {
            if (feature instanceof FoldingFeature) {
                if (isTableTopPosture((FoldingFeature) feature)) {
                    enterTabletopMode(feature);
                } else if (isBookPosture((FoldingFeature) feature)) {
                    enterBookMode(feature);
                } else if (isSeparating((FoldingFeature) feature)) {
                    // Dual-screen device
                    if (((FoldingFeature) feature).getOrientation() ==
                              FoldingFeature.Orientation.HORIZONTAL) {
                        enterTabletopMode(feature);
                    } else {
                        enterBookMode(feature);
                    }
                } else {
                    enterNormalMode();
                }
            }
        }
    }
}

private boolean isTableTopPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

private boolean isBookPosture(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

private boolean isSeparating(FoldingFeature foldFeature) {
    return (foldFeature != null) &&
           (foldFeature.getState() == FoldingFeature.State.FLAT) &&
           (foldFeature.isSeparating() == true);
}

在双屏设备上,即使 FoldingFeature 状态为 FLAT,也请使用为桌面模式和图书模式设计的布局。

当 isSeparating 为 true 时,请避免使界面控件与折叠边或合页靠得太近,否则会难以触及这些控件。请使用 occlusionType 决定是否将内容放置在折叠功能 bounds 内。

窗口大小变化

应用的显示区域可能会因设备配置变更(例如设备折叠或展开、旋转,或窗口在多窗口模式下调整大小)而发生变化。

借助 Jetpack WindowManager 的 WindowMetricsCalculator 类,您可以检索当前和最大窗口指标。与 API 级别 30 中引入的平台 WindowMetrics 一样,WindowManager WindowMetrics 会提供窗口边界,不过 API 可向后兼容到 API 级别 14。

请在 activity 的 onCreate() 或 onConfigurationChanged() 方法中使用 WindowMetrics 来针对当前窗口大小配置应用的布局,例如:

Kotlin

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    val windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this@MainActivity)
    val bounds = windowMetrics.getBounds()
    ...
}

Java

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    final WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate()
        .computeCurrentWindowMetrics(this);
    final Rect bounds = windowMetrics.getBounds();
    ...
}

另请参阅支持不同的屏幕尺寸。

其他资源

示例

Jetpack WindowManager:如何使用 Jetpack WindowManager 库的示例
Jetcaster:使用 Compose 实现桌面模式

Codelab

借助 Jetpack WindowManager 支持可折叠设备和双屏设备
本页面上的内容和代码示例受内容许可部分所述许可的限制。Java 和 OpenJDK 是 Oracle 和/或其关联公司的注册商标。

最后更新时间 (UTC):2023-11-24。

你可能感兴趣的:(学习交流,android,kotlin,c++,c语言,gitee)