Раскройте свои впечатления от камеры

1. Прежде чем начать

Что особенного в складных вещах?

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

Предварительные требования

  • Базовые знания в разработке приложений для Android.
  • Базовые знания фреймворка внедрения зависимостей Hilt.

Что вы построите

В этом практическом задании вы создадите приложение камеры с оптимизированным интерфейсом для складных устройств.

6caebc2739522a1b.png

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

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

Что вы узнаете

  • Как использовать оконный менеджер Jetpack для реагирования на изменение позы
  • Как перенести ваше приложение на меньший экран складного устройства

Что вам понадобится

  • Последняя версия Android Studio
  • Складное устройство или складной эмулятор

2. Настройка

Получите стартовый код

  1. Если у вас установлен Git, вы можете просто выполнить команду ниже. Чтобы проверить, установлен ли Git, введите git --version в терминале или командной строке и убедитесь, что команда выполняется корректно.
git clone https://github.com/android/large-screen-codelabs.git
  1. Дополнительно: Если у вас нет Git, вы можете нажать следующую кнопку, чтобы загрузить весь код для этого практического занятия:

Откройте первый модуль

  • В Android Studio откройте первый модуль в папке /step1 .

Скриншот Android Studio, демонстрирующий код, относящийся к этому практическому заданию.

Если вас попросят использовать последнюю версию Gradle, смело обновляйте её.

3. Бегите и наблюдайте.

  1. Запустите код на step1 модуля.

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

a34aca632d75aa09.png

  1. Теперь попробуйте расположить устройство в полуоткрытом положении, при котором шарнир не полностью сложен или закрыт, а образует угол в 90 градусов.

Как видите, приложение не реагирует на различное положение устройства, поэтому его интерфейс не меняется, и шарнир остается посередине видоискателя.

4. Изучите Jetpack WindowManager.

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

Класс 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.

  1. Объявите эти переменные в вашем 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
  1. И инициализируйте их в методе 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()
      }
  }
}
  1. Теперь реализуйте функцию 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.

  1. Теперь реализуйте функцию 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 .

  1. Измените объявление 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]")
}
  1. Соберите и запустите приложение. Если затем вы развернете устройство и нажмете на кнопку на заднем дисплее, появится сообщение примерно такого вида:

3fa50cce0b0d4b8d.png

  1. Выберите « Переключить экраны сейчас» , чтобы ваш контент был перемещен на внешний экран!

6. Реализуйте настольный режим.

Теперь пришло время адаптировать ваше приложение к сворачиванию: вы перемещаете контент сбоку или над шарниром устройства в зависимости от ориентации сгиба. Для этого вы будете работать внутри FoldingStateActor , чтобы ваш код был отделен от Activity для большей читаемости.

Основная часть этого API состоит из интерфейса WindowInfoTracker , который создается с помощью статического метода, требующего Activity :

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

Вам не нужно писать этот код, так как он уже есть, но он полезен для понимания того, как создается WindowInfoTracker .

  1. Чтобы отслеживать изменения в окне, используйте метод onResume() вашего Activity для отслеживания этих изменений:

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity, 
         binding.viewFinder
    )
}
  1. Теперь откройте файл FoldingStateActor , так как пришло время заполнить метод checkFoldingState() .

Как вы уже видели, он выполняется на этапе RESUMED вашего Activity и использует WindowInfoTracker для отслеживания любых изменений макета.

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

Используя интерфейс WindowInfoTracker , вы можете вызвать windowLayoutInfo() , чтобы получить Flow WindowLayoutInfo , содержащий всю доступную информацию из DisplayFeature .

Последний шаг — отреагировать на эти изменения и соответствующим образом переместить контент. Это делается внутри метода updateLayoutByFoldingState() , шаг за шагом.

  1. Убедитесь, что activityLayoutInfo содержит несколько свойств DisplayFeature , и что хотя бы одно из них имеет тип FoldingFeature , иначе ничего делать не нужно:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. Рассчитайте положение сгиба, чтобы убедиться, что положение устройства влияет на вашу компоновку и не выходит за пределы вашей иерархии:

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

Теперь вы уверены, что у вас есть свойство FoldingFeature , которое влияет на вашу компоновку, поэтому вам нужно переместить контент.

  1. Проверьте, находится ли 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 , вы перемещаете содержимое вправо, в противном случае — поверх области сгиба.

  1. Создайте и запустите приложение, а затем разверните устройство и переведите его в настольный режим, чтобы увидеть, как контент перемещается соответствующим образом!

7. Поздравляем!

В этом практическом занятии вы узнали о некоторых возможностях, уникальных для складных устройств, таких как режим заднего дисплея или настольный режим, а также о том, как разблокировать их с помощью Jetpack WindowManager.

Вы готовы внедрить превосходный пользовательский опыт в ваше приложение для камеры.

Дополнительная информация

Ссылка