Otimizar o app de câmera em dispositivos dobráveis com o Jetpack WindowManager

1. Antes de começar

O que há de especial nos dispositivos dobráveis?

Os dispositivos dobráveis são inovações que marcam uma geração. Eles proporcionam experiências exclusivas e, com elas, oportunidades para encantar os usuários com recursos diferenciados, como a interface do usuário de mesa para uso do viva-voz.

Pré-requisitos

  • Noções básicas de desenvolvimento de apps Android.
  • Noções básicas do framework da injeção de dependências com Hilt.

O que você vai criar

Neste codelab, você vai criar um app de câmera com layouts otimizados para dispositivos dobráveis.

c5e52933bcd81859.png

Vamos começar com um app de câmera básico que não considera as mudanças de posição dos dispositivos ou aproveita a câmera traseira de maior qualidade para tirar selfies melhores. Vamos atualizar o código-fonte para mover a visualização para a tela menor quando o dispositivo for desdobrado e reagir ao smartphone sendo configurado no modo de mesa.

Embora o app da câmera seja o caso de uso mais conveniente para esta API, os dois recursos que você aprenderá neste codelab podem ser aplicados a qualquer app.

O que você vai aprender

  • Como usar o Jetpack Window Manager para reagir à mudança de posição do dispositivo.
  • Como mover o app para a tela menor de um dispositivo dobrável.

O que é necessário

  • Uma versão recente do Android Studio.
  • Um dispositivo dobrável ou emulador.

2. Começar a configuração

Fazer o download do código inicial

  1. Se você tiver o Git instalado, basta executar o comando abaixo. Para verificar se o Git está instalado, digite git --version no terminal ou na linha de comando e verifique se ele é executado corretamente.
git clone https://github.com/android/large-screen-codelabs.git
  1. Opcional: caso não tenha o Git, clique no botão abaixo para fazer o download de todo o código deste codelab:

Abrir o primeiro módulo

  • No Android Studio, abra o primeiro módulo em /step1.

Captura de tela do Android Studio mostrando o código relacionado a este codelab

Atualize para a versão mais recente do Gradle se aparecer um aviso pedindo isso.

3. Executar e observar

  1. Execute o código no módulo step1.

Este é um app de câmera simples. É possível alternar entre a câmera frontal e a traseira e ajustar a proporção. No entanto, o primeiro botão da esquerda não faz nada no momento, mas será o ponto de entrada para o modo Selfie de câmera traseira.

149e3f9841af7726.png

  1. Agora, tente colocar o dispositivo na posição semi-aberta, em que a dobradiça não fica totalmente plana ou fechada, mas forma um ângulo de 90 graus.

O app não responde às posições diferentes do dispositivo e, portanto, o layout não muda, deixando a dobra no meio do visor.

4. Saiba mais sobre o Jetpack WindowManager

A biblioteca do Jetpack WindowManager ajuda os desenvolvedores de apps a criar experiências otimizadas para dispositivos dobráveis. Ela contém a classe FoldingFeature, que descreve uma dobra em telas flexíveis ou uma articulação entre dois painéis de tela física. A API dela fornece acesso a informações importantes relacionadas ao dispositivo:

A classe FoldingFeature contém outros dados, como occlusionType() ou isSeparating(), mas este codelab não os explora profundamente.

Na versão 1.2.0-beta01 e mais recentes, a biblioteca usa a WindowAreaController, uma API que permite que o modo de tela traseira mova a janela atual para a tela alinhada com a câmera traseira, o que é ótimo para tirar selfies com a câmera traseira e muitos outros casos de uso.

Adicionar dependências

  • Para usar o Jetpack WindowManager no app, adicione estas dependências ao nível do módulo do arquivo build.gradle:

step1/build.gradle

def work_version = '1.2.0'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

Agora você pode acessar as classes FoldingFeature e WindowAreaController no app. Elas servem para criar a melhor experiência de câmera dobrável.

5. Implementar o modo de selfie de câmera traseira

Comece com o modo de tela traseira.

A API habilita esse modo é a WindowAreaController. Ela fornece informações e o comportamento ao mover janelas entre telas ou áreas de exibição em um dispositivo.

Ela também permite que você consulte a lista das WindowAreaInfo que estão disponíveis para interação no momento.

Usando WindowAreaInfo, é possível acessar a WindowAreaSession, uma interface para representar um recurso de área de janela ativa e o status de disponibilidade para uma WindowAreaCapability. específica.

  1. Declare essas variáveis em 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. Inicialize-as no método 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. Agora, implemente a função updateUI() para ativar ou desativar o botão de selfie de câmera traseira, dependendo do status atual:

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
            }
        }
    }
}

Essa última etapa é opcional, mas muito útil para aprender todos os estados possíveis de uma WindowAreaCapability.

  1. Agora, implemente a função toggleRearDisplayMode, que vai fechar a sessão se a capability já estiver ativa, ou chame a função 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
            )
        }
    }
}

Observe o uso da MainActivity como um WindowAreaSessionCallback.

A API Rear Display funciona com uma abordagem de listener: ao pedir para mover o conteúdo para a outra tela, você inicia uma sessão que é retornada usando o método onSessionStarted() do listener. Quando quiser retornar à tela interna (e maior), você fecha a sessão e recebe uma confirmação no método onSessionEnded(). Para criar esse listener, é preciso implementar a interface WindowAreaSessionCallback.

  1. Modifique a declaração MainActivity para que ela implemente a interface do WindowAreaSessionCallback:

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

Agora, implemente os métodos onSessionStarted e onSessionEnded dentro da MainActivity. Esses métodos de callback são muito úteis para receber notificações sobre o status da sessão e atualizar o app adequadamente.

Desta vez, para simplificar, verifique se há erros no corpo da função e registre o estado.

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. Crie e execute o app. Se você desdobrar o dispositivo e tocar no botão da tela traseira, uma mensagem como esta vai aparecer:

ba878f120b7c8d58.png

  1. Selecione Trocar de tela agora para mover o conteúdo para a tela externa.

6. Implementar o modo de mesa

Agora é hora de tornar o app sensível à dobra: mova o conteúdo para a lateral ou acima da dobra do dispositivo com base na orientação da dobra. Para fazer isso, você trabalhará dentro do FoldingStateActor para que o código seja desacoplado da Activity para facilitar a leitura.

A parte central da API do Hilt consiste na interface WindowInfoTracker, que é criada com um método estático que exige uma Activity:

step1/CameraCodelabDependencies.kt

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

Você não precisa escrever esse código porque ele já está presente, mas é útil entender como o WindowInfoTracker é criado.

  1. Detecte todas as mudanças de janela no método onResume() da Activity:

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity, 
         binding.viewFinder
    )
}
  1. Agora, abra o arquivo FoldingStateActor para preencher o método checkFoldingState().

Ele é executado na fase RESUMED de Activity e aproveita WindowInfoTracker para detectar qualquer mudança de layout.

step1/FoldingStateActor.kt

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

Ao usar a interface WindowInfoTracker, você pode chamar windowLayoutInfo() para coletar um Flow (link em inglês) de WindowLayoutInfo que contém todas as informações disponíveis no DisplayFeature.

A última etapa é reagir a essas mudanças e mover o conteúdo. Você fará isso dentro do método updateLayoutByFoldingState(), uma etapa de cada vez.

  1. Confira se a activityLayoutInfo contém algumas propriedades do DisplayFeature e que pelo menos uma delas seja FoldingFeature. Caso contrário, você não precisa fazer nada:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. Calcule a posição da dobra para garantir que a posição do dispositivo afete o layout e não esteja fora dos limites de hierarquia:

step1/FoldingStateActor.kt

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

Agora, você tem um FoldingFeature que impacta o layout, então é necessário mover o conteúdo.

  1. Confira se FoldingFeature é HALF_OPEN. Se não for, restaure apenas a posição do conteúdo. Se for HALF_OPEN, é necessário executar outra verificação e agir de maneira diferente com base na orientação da dobra:

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()
}

Se a dobra for VERTICAL, mova o conteúdo para a direita. Caso contrário, mova-o para cima da posição da dobra.

  1. Crie e execute o app e, em seguida, desdobre o dispositivo e coloque-o no modo de mesa para que o conteúdo seja movido corretamente.

7. Parabéns!

Neste codelab, você aprendeu sobre algumas capabilities exclusivas de dispositivos dobráveis, como o modo de tela traseira e o modo de mesa, e como desbloqueá-los usando a biblioteca Jetpack WindowManager.

Você já pode implementar ótimas experiências do usuário no seu app de câmera.

Leia mais

Referência