Сделайте ваше приложение осведомленным о сворачивании

Большие развернутые дисплеи и уникальные состояния в сложенном состоянии открывают новые возможности для пользователей на складных устройствах. Чтобы ваше приложение учитывало свертывание, используйте библиотеку Jetpack WindowManager , которая предоставляет поверхность API для таких функций складных окон устройств, как складки и петли. Когда ваше приложение поддерживает складку, оно может адаптировать свой макет, чтобы избежать размещения важного контента в области сгибов или петель и использовать складки и петли в качестве естественных разделителей.

Понимание того, поддерживает ли устройство такие конфигурации, как положение столешницы или книги, может помочь принять решение о поддержке различных макетов или предоставлении определенных функций.

Информация об окне

Интерфейс WindowInfoTracker в Jetpack WindowManager предоставляет информацию о макете окна. Метод windowLayoutInfo() интерфейса возвращает поток данных WindowLayoutInfo , который информирует ваше приложение о состоянии складывания складного устройства. Метод WindowInfoTracker#getOrCreate() создает экземпляр WindowInfoTracker .

WindowManager обеспечивает поддержку сбора данных WindowLayoutInfo с использованием потоков Kotlin и обратных вызовов Java.

Котлин потоки

Чтобы запустить и остановить сбор данных WindowLayoutInfo , вы можете использовать перезапускаемую сопрограмму, учитывающую жизненный цикл , в которой блок кода repeatOnLifecycle выполняется, когда жизненный цикл по крайней мере STARTED , и останавливается, когда жизненный цикл 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 позволяет собирать обновления WindowLayoutInfo без использования потока Kotlin. Артефакт включает в себя класс 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.

Уровень совместимости, предоставляемый зависимостями 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 of the WindowLayoutInfo observable.
        disposable?.dispose()
   }
}

Особенности складных дисплеев

Класс WindowLayoutInfo Jetpack WindowManager делает функции окна отображения доступными в виде списка элементов DisplayFeature .

FoldingFeature — это тип DisplayFeature , который предоставляет информацию о складных дисплеях, включая следующее:

  • state : сложенное состояние устройства, FLAT или HALF_OPENED

  • orientation : ориентация сгиба или петли: HORIZONTAL или VERTICAL

  • occlusionType : скрывает ли складка или шарнир часть дисплея, NONE или FULL

  • isSeparating : создает ли сгиб или шарнир две логические области отображения: true или false.

Складное устройство с параметром HALF_OPENED всегда сообщает isSeparating как true, поскольку экран разделен на две области отображения. Кроме того, isSeparating всегда имеет значение true на устройстве с двумя экранами, когда приложение охватывает оба экрана.

Свойство bounds FoldingFeature (наследованное от DisplayFeature ) представляет ограничивающий прямоугольник элемента сгиба, такого как сгиб или шарнир. Границы можно использовать для позиционирования элементов на экране относительно объекта:

Котлин

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            // Safely collects from WindowInfoTracker 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<FoldingFeature>()
                        .firstOrNull()
                    // Use information from the foldingFeature object.
                }

        }
    }
}

Ява

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) {
                // Use information from the feature object.
            }
        }
    }
}

Поза стола

Используя информацию, содержащуюся в объекте FoldingFeature , ваше приложение может поддерживать такие положения, как столешница, когда телефон находится на поверхности, шарнир находится в горизонтальном положении, а складной экран наполовину открыт.

Положение стола позволяет пользователям удобно управлять телефоном, не держа его в руках. Поза за столом отлично подходит для просмотра мультимедиа, фотосъемки и видеозвонков.

Рис. 1. Приложение видеоплеера в настольном положении.

Используйте FoldingFeature.State и FoldingFeature.Orientation , чтобы определить, находится ли устройство в положении стола:

Котлин


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

Ява


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

Как только вы узнаете, что устройство находится в положении «на столе», соответствующим образом обновите макет приложения. Для мультимедийных приложений это обычно означает размещение воспроизведения над сгибом и размещение элементов управления и дополнительного контента чуть ниже для просмотра или прослушивания без помощи рук.

В Android 15 (уровень API 35) и более поздних версиях вы можете вызвать синхронный API, чтобы определить, поддерживает ли устройство положение стола, независимо от текущего состояния устройства.

API предоставляет список поз, поддерживаемых устройством. Если список содержит настольное расположение, вы можете разделить макет приложения для поддержки этого положения и запустить A/B-тесты пользовательского интерфейса вашего приложения для настольного и полноэкранного макетов.

Котлин

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Ява

if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
    List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
    if (postures.contains(SupportedPosture.TABLETOP)) {
        // Device supports tabletop posture.
    }
}

Примеры

Книга поза

Еще одна уникальная особенность складывания — книжная поза, при которой устройство наполовину открыто, а петли расположены вертикально. Поза книги отлично подходит для чтения электронных книг. Благодаря двухстраничному макету на большом экране, который можно сложить, как переплетенную книгу, положение книги передает ощущение чтения настоящей книги.

Его также можно использовать для фотографии, если вы хотите запечатлеть другое соотношение сторон при съемке без помощи рук.

Реализуйте позу книги, используя те же приемы, что и позу за столом. Единственное отличие состоит в том, что код должен проверять, что ориентация элемента сгиба вертикальная, а не горизонтальная:

Котлин

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

Ява

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

Изменение размера окна

Область отображения приложения может измениться в результате изменения конфигурации устройства, например, когда устройство складывается или разворачивается, поворачивается или изменяется размер окна в многооконном режиме.

Класс Jetpack WindowManager WindowMetricsCalculator позволяет получать текущие и максимальные показатели окна. Как и платформа WindowMetrics представленная на уровне API 30, WindowManager WindowMetrics обеспечивает границы окна, но API обратно совместим вплоть до уровня API 14.

См. раздел Использование классов размеров окон .

Дополнительные ресурсы

Образцы

  • Jetpack WindowManager : пример использования библиотеки Jetpack WindowManager.
  • Jetcaster : реализация положения стола с помощью Compose

Кодлабы