1. Прежде чем начать
Что особенного в складных вещах?
Складные устройства — это инновации, которые появляются раз в поколение. Они обеспечивают уникальный опыт и открывают уникальные возможности для того, чтобы порадовать пользователей такими отличительными функциями, как настольный интерфейс для использования без помощи рук.
Предварительные требования
- Базовые знания в разработке приложений для Android.
- Базовые знания фреймворка внедрения зависимостей Hilt.
Что вы построите
В этом практическом задании вы создадите приложение камеры с оптимизированным интерфейсом для складных устройств.

Вы начинаете с простого приложения камеры, которое не реагирует на положение устройства и не использует преимущества улучшенной задней камеры для качественных селфи. Вы обновляете исходный код, чтобы переместить предварительный просмотр на меньший экран, когда устройство разложено, и реагировать на установку телефона в настольный режим.
Хотя приложение камеры является наиболее удобным примером использования этого API, обе функции, которые вы изучите в этом практическом занятии, могут быть применены к любому приложению.
Что вы узнаете
- Как использовать оконный менеджер Jetpack для реагирования на изменение позы
- Как перенести ваше приложение на меньший экран складного устройства
Что вам понадобится
- Последняя версия Android Studio
- Складное устройство или складной эмулятор
2. Настройка
Получите стартовый код
- Если у вас установлен Git, вы можете просто выполнить команду ниже. Чтобы проверить, установлен ли Git, введите
git --versionв терминале или командной строке и убедитесь, что команда выполняется корректно.
git clone https://github.com/android/large-screen-codelabs.git
- Дополнительно: Если у вас нет Git, вы можете нажать следующую кнопку, чтобы загрузить весь код для этого практического занятия:
Откройте первый модуль
- В Android Studio откройте первый модуль в папке
/step1.

Если вас попросят использовать последнюю версию Gradle, смело обновляйте её.
3. Бегите и наблюдайте.
- Запустите код на
step1модуля.
Как видите, это простое приложение камеры. Вы можете переключаться между фронтальной и тыловой камерой, а также регулировать соотношение сторон. Однако первая кнопка слева в данный момент ничего не делает — но она будет точкой входа в режим «Селфи с тыльной стороны» .

- Теперь попробуйте расположить устройство в полуоткрытом положении, при котором шарнир не полностью сложен или закрыт, а образует угол в 90 градусов.
Как видите, приложение не реагирует на различное положение устройства, поэтому его интерфейс не меняется, и шарнир остается посередине видоискателя.
4. Изучите Jetpack WindowManager.
Библиотека Jetpack WindowManager помогает разработчикам приложений создавать оптимизированные интерфейсы для складных устройств. Она содержит класс FoldingFeature , описывающий сгиб гибкого дисплея или шарнир между двумя физическими панелями дисплея. Ее API предоставляет доступ к важной информации, относящейся к устройству:
-
state()возвращаетFLATесли шарнир открыт на 180 градусов, илиHALF_OPENEDв противном случае. -
orientation()возвращаетFoldingFeature.Orientation.HORIZONTAL, если ширина объектаFoldingFeatureбольше его высоты; в противном случае возвращаетFoldingFeature.Orientation.VERTICAL. -
bounds()предоставляет границы объектаFoldingFeatureв форматеRect.
Класс FoldingFeature содержит дополнительную информацию, такую как occlusionType() или isSeparating() , но в этом примере кода они подробно не рассматриваются.
Начиная с версии 1.2.0-beta01 , библиотека использует WindowAreaController — API, который позволяет в режиме заднего дисплея перемещать текущее окно на экран, выровненный по задней камере. Это отлично подходит для съемки селфи с помощью задней камеры и многих других задач!
Добавить зависимости
- Для использования Jetpack WindowManager в вашем приложении необходимо добавить следующие зависимости в файл
build.gradleна уровне модуля:
шаг 1/build.gradle
def work_version = '1.2.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"
Теперь вы можете использовать классы FoldingFeature и WindowAreaController в своем приложении. Используйте их для создания идеальной камеры со складным экраном!
5. Реализовать режим селфи с задней камеры.
Начните с режима заднего дисплея.
API, позволяющий использовать этот режим, — это WindowAreaController , который предоставляет информацию и определяет поведение при перемещении окон между дисплеями или областями отображения на устройстве.
Это позволяет запросить список WindowAreaInfo , с которыми в данный момент можно взаимодействовать.
С помощью WindowAreaInfo можно получить доступ к WindowAreaSession — интерфейсу, представляющему активную функцию оконной области и статус доступности для конкретной WindowAreaCapability.
- Объявите эти переменные в вашем
MainActivity:
step1/MainActivity.kt
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var rearDisplaySession: WindowAreaSession? = null
private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
private var rearDisplayStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
- И инициализируйте их в методе
onCreate():
step1/MainActivity.kt
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowAreaController.windowAreaInfos
.map{info->info.firstOrNull{it.type==WindowAreaInfo.Type.TYPE_REAR_FACING}}
.onEach { info -> rearDisplayWindowAreaInfo = info }
.map{it?.getCapability(rearDisplayOperation)?.status?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
.distinctUntilChanged()
.collect {
rearDisplayStatus = it
updateUI()
}
}
}
- Теперь реализуйте функцию
updateUI()для включения или выключения задней кнопки селфи в зависимости от текущего состояния:
step1/MainActivity.kt
private fun updateUI() {
if(rearDisplaySession != null) {
binding.rearDisplay.isEnabled = true
// A session is already active, clicking on the button will disable it
} else {
when(rearDisplayStatus) {
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not supported on this device"
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not currently available
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
binding.rearDisplay.isEnabled = true
// You can enable RearDisplay Mode
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
binding.rearDisplay.isEnabled = true
// You can disable RearDisplay Mode
}
else -> {
binding.rearDisplay.isEnabled = false
// RearDisplay status is unknown
}
}
}
}
Этот последний шаг необязателен, но очень полезен для изучения всех возможных состояний WindowAreaCapability.
- Теперь реализуйте функцию
toggleRearDisplayMode, которая закроет сессию, если эта возможность уже активна, или вызовите функциюtransferActivityToWindowArea:
step1/CameraViewModel.kt
private fun toggleRearDisplayMode() {
if(rearDisplayStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
if(rearDisplaySession == null) {
rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(rearDisplayOperation)
}
rearDisplaySession?.close()
} else {
rearDisplayWindowAreaInfo?.token?.let { token ->
windowAreaController.transferActivityToWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaSessionCallback = this
)
}
}
}
Обратите внимание на использование MainActivity в качестве WindowAreaSessionCallback .
API для заднего дисплея работает по принципу прослушивания: при запросе на перемещение контента на другой дисплей инициируется сессия, которая возвращается через метод onSessionStarted() прослушивателя. Если же вы хотите вернуться к внутреннему (и большему) дисплею, сессия закрывается, и вы получаете подтверждение в методе onSessionEnded() . Для создания такого прослушивателя необходимо реализовать интерфейс WindowAreaSessionCallback .
- Измените объявление
MainActivityтаким образом, чтобы оно реализовывало интерфейсWindowAreaSessionCallback:
step1/MainActivity.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
Теперь реализуйте методы onSessionStarted и onSessionEnded внутри MainActivity . Эти методы обратного вызова чрезвычайно полезны для получения уведомлений о статусе сессии и соответствующего обновления приложения.
Но на этот раз, для простоты, достаточно проверить наличие ошибок в теле функции и записать состояние в лог.
step1/MainActivity.kt
override fun onSessionEnded(t: Throwable?) {
if(t != null) {
Log.d("Something was broken: ${t.message}")
}else{
Log.d("rear session ended")
}
}
override fun onSessionStarted(session: WindowAreaSession) {
Log.d("rear session started [session=$session]")
}
- Соберите и запустите приложение. Если затем вы развернете устройство и нажмете на кнопку на заднем дисплее, появится сообщение примерно такого вида:

- Выберите « Переключить экраны сейчас» , чтобы ваш контент был перемещен на внешний экран!
6. Реализуйте настольный режим.
Теперь пришло время адаптировать ваше приложение к сворачиванию: вы перемещаете контент сбоку или над шарниром устройства в зависимости от ориентации сгиба. Для этого вы будете работать внутри FoldingStateActor , чтобы ваш код был отделен от Activity для большей читаемости.
Основная часть этого API состоит из интерфейса WindowInfoTracker , который создается с помощью статического метода, требующего Activity :
step1/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
Вам не нужно писать этот код, так как он уже есть, но он полезен для понимания того, как создается WindowInfoTracker .
- Чтобы отслеживать изменения в окне, используйте метод
onResume()вашегоActivityдля отслеживания этих изменений:
step1/MainActivity.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
- Теперь откройте файл
FoldingStateActor, так как пришло время заполнить методcheckFoldingState().
Как вы уже видели, он выполняется на этапе RESUMED вашего Activity и использует WindowInfoTracker для отслеживания любых изменений макета.
step1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
Используя интерфейс WindowInfoTracker , вы можете вызвать windowLayoutInfo() , чтобы получить Flow WindowLayoutInfo , содержащий всю доступную информацию из DisplayFeature .
Последний шаг — отреагировать на эти изменения и соответствующим образом переместить контент. Это делается внутри метода updateLayoutByFoldingState() , шаг за шагом.
- Убедитесь, что
activityLayoutInfoсодержит несколько свойствDisplayFeature, и что хотя бы одно из них имеет типFoldingFeature, иначе ничего делать не нужно:
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- Рассчитайте положение сгиба, чтобы убедиться, что положение устройства влияет на вашу компоновку и не выходит за пределы вашей иерархии:
step1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
Теперь вы уверены, что у вас есть свойство FoldingFeature , которое влияет на вашу компоновку, поэтому вам нужно переместить контент.
- Проверьте, находится ли
FoldingFeatureвHALF_OPEN, иначе вы просто восстановите положение содержимого. Если оно находится вHALF_OPEN, вам необходимо выполнить дополнительную проверку и действовать по-разному в зависимости от ориентации сгиба:
step1/FoldingStateActor.kt
if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
when (foldingFeature.orientation) {
FoldingFeature.Orientation.VERTICAL -> {
cameraViewfinder.moveToRightOf(foldPosition)
}
FoldingFeature.Orientation.HORIZONTAL -> {
cameraViewfinder.moveToTopOf(foldPosition)
}
}
} else {
cameraViewfinder.restore()
}
Если область сгиба VERTICAL , вы перемещаете содержимое вправо, в противном случае — поверх области сгиба.
- Создайте и запустите приложение, а затем разверните устройство и переведите его в настольный режим, чтобы увидеть, как контент перемещается соответствующим образом!
7. Поздравляем!
В этом практическом занятии вы узнали о некоторых возможностях, уникальных для складных устройств, таких как режим заднего дисплея или настольный режим, а также о том, как разблокировать их с помощью Jetpack WindowManager.
Вы готовы внедрить превосходный пользовательский опыт в ваше приложение для камеры.