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

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

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

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

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

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

Что ты построишь

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

6caebc2739522a1b.png

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

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

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

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

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

  • Последняя версия 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 модуля.

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

а34aca632d75aa09.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 :

шаг1/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() :

шаг1/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() , чтобы включать или отключать заднюю кнопку селфи, в зависимости от текущего статуса:

шаг1/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 :

шаг1/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 :

шаг1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

Теперь реализуйте методы onSessionStarted и onSessionEnded внутри MainActivity . Эти методы обратного вызова чрезвычайно полезны для получения уведомлений о состоянии сеанса и соответствующего обновления приложения.

Но на этот раз для простоты просто проверьте тело функции на наличие ошибок и запишите состояние.

шаг1/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 :

шаг 1/CameraCodelabDependency.kt

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

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

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

шаг1/MainActivity.kt

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

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

шаг1/FoldingStateActor.kt

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

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

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

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

шаг1/FoldingStateActor.kt

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

шаг1/FoldingStateActor.kt

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

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

  1. Проверьте, равен ли FoldingFeature HALF_OPEN , иначе вы просто восстановите положение своего контента. Если это HALF_OPEN , вам нужно запустить еще одну проверку и действовать по-другому в зависимости от ориентации сгиба:

шаг1/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.

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

Дальнейшее чтение

Ссылка