Descubra a câmera em dispositivos dobráveis

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 seus 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 o Hilt

O que você vai criar

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

Captura de tela do app em execução

Você começa 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 melhor qualidade para tirar selfies aprimoradas. Você atualiza o código-fonte para mover a visualização para a tela menor quando o dispositivo for desdobrado e reage 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 levar em conta a 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 ou emulador dobráveis

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

Se for solicitado que você use a versão mais recente do Gradle, atualize-a.

3. Executar e observar

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

Como você pode perceber, 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 atualmente não faz nada, mas será o ponto de entrada para o modo selfie traseira.

Captura de tela do app com o ícone do modo selfie traseira em destaque

  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.

Como você pode perceber, 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 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 outras informações, como occlusionType() ou isSeparating(), mas este codelab não os explora em profundidade.

A partir da versão 1.1.0-beta01, 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 as seguintes dependências ao nível do módulo do arquivo build.gradle:

step1/build.gradle

def work_version = '1.1.0-beta01'
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 selfie traseira

Comece com o modo de tela traseira. A API que habilita esse modo é a WindowAreaControllerJavaAdapter, que requer Executor e retorna WindowAreaSession, que armazena o estado atual. Este WindowAreaSession deve ser mantido quando Activity for excluído e recriado, então você o armazena dentro de ViewModel para armazená-lo com segurança nas alterações de configuração.

  1. Declare essas variáveis em MainActivity:

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
  1. E inicialize-os no método onCreate():

step1/MainActivity.kt

windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())

Agora o Activity está pronto para mover o conteúdo na tela menor, mas você precisa armazenar a sessão.

  1. Para armazenar a sessão, abra CameraViewModel e declare esta variável dentro dela:

step1/CameraViewModel.kt

var rearDisplaySession: WindowAreaSession? = null
        private set

Você precisa que rearDisplaySession seja uma variável, já que ela muda toda vez que você cria uma, mas você quer ter certeza de que ela não seja atualizada de fora, porque agora você cria um método que a atualiza sempre que for necessário.

  1. Cole este código dentro de CameraViewModel:

step1/CameraViewModel.kt

fun updateSession(newSession: WindowAreaSession? = null) {
        rearDisplaySession = newSession
}

Esse método é invocado toda vez que o código precisa atualizar a sessão e convém encapsulá-lo em um único ponto de acesso.

A API Rear Display funciona com uma abordagem de listener: quando você move o conteúdo para a tela menor, uma sessão é iniciada e retornada usando o método onSessionStarted() do listener. Quando, em vez disso, você quer retornar à tela interna (e maior), você fecha a sessão e recebe uma confirmação no método onSessionEnded(). Você usa esses métodos para atualizar rearDisplaySession no interior de CameraViewModel. Para criar esse listener, você precisa implementar a interface WindowAreaSessionCallback.

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

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

Agora, implemente os métodos onSessionStarted e onSessionEnded dentro de MainActivity. Para o primeiro, você precisa salvar o WindowAreaSession e, no segundo, redefini-lo para null. Isso é particularmente útil, porque a presença de WindowAreaSession permite que você decida se quer iniciar uma sessão nova ou fechar uma existente.

step1/MainActivity.kt

override fun onSessionEnded() {
    viewModel.updateSession(null)
}

override fun onSessionStarted(session: WindowAreaSession) {
    viewModel.updateSession(session)
}
  1. No arquivo MainActivity.kt, escreva o último trecho de código necessário para essa API funcionar:

step1/MainActivity.kt

private fun startRearDisplayMode() {
   if (viewModel.rearDisplaySession != null) {
      viewModel.rearDisplaySession?.close()
   } else {
      windowAreaController.startRearDisplayModeSession(
         this,
         displayExecutor,
         this
      )
   }
}

Como mencionamos anteriormente, para entender qual ação tomar, você precisa verificar a presença de rearDisplaySession dentro de CameraViewModel: se não for null, a sessão já está ocorrendo, então ela fecha. Por outro lado, se for null, você usa windowAreaController para iniciar uma nova sessão, transmitindo Activity duas vezes. A primeira vez é usada como Context e a segunda como um listener WindowAreaSessionCallback.

  1. Agora, crie e execute o app. Se você abrir o dispositivo e tocar no botão da tela traseira, uma mensagem como esta será exibida:

Captura de tela do prompt do usuário mostrando quando o modo de exibição traseira foi iniciado.

  1. Clique em Troque de tela agora e seu conteúdo será movido para a tela externa!

6. Implementar o modo de mesa

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

A parte central desta API consiste na interface WindowInfoTracker, que é criada com um método estático que requer um 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 WindowInfoTracker é criado.

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

step1/MainActivity.kt

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

Como você já percebeu, ele é executado na fase RESUMED de Activity e aproveita WindowInfoTracker para detectar qualquer alteração 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 de WindowLayoutInfo que contém todas as informações disponíveis em DisplayFeature.

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

  1. Certifique-se que activityLayoutInfo contenha algumas propriedades 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 esteja afetando 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 certeza de que tem um FoldingFeature que impacta o layout, então você precisa mover o conteúdo.

  1. Verifique se FoldingFeature for HALF_OPEN ou outro, apenas restaure a posição do conteúdo. Se for HALF_OPEN, você precisa 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 se mova.

7. Parabéns

Neste codelab, você aprendeu o que há de especial nos dispositivos dobráveis, nas mudanças de posição e na API Rear Display.

Leia mais

Reference