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.
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
- 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
- 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
.
Atualize para a versão mais recente do Gradle se aparecer um aviso pedindo isso.
3. Executar e observar
- 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.
- 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:
state()
: retornaFLAT
se a dobra estiver aberta a 180 graus ouHALF_OPENED
de outra forma.orientation()
: retornaFoldingFeature.Orientation.HORIZONTAL
se a largura deFoldingFeature
for maior que a altura. Caso contrário, retornaFoldingFeature.Orientation.VERTICAL
.bounds()
: fornece os limites doFoldingFeature
em um formatoRect
.
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.
- 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
- 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()
}
}
}
- 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.
- Agora, implemente a função
toggleRearDisplayMode
, que vai fechar a sessão se a capability já estiver ativa, ou chame a funçãotransferActivityToWindowArea
:
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
.
- Modifique a declaração
MainActivity
para que ela implemente a interface doWindowAreaSessionCallback
:
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]")
}
- Crie e execute o app. Se você desdobrar o dispositivo e tocar no botão da tela traseira, uma mensagem como esta vai aparecer:
- 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.
- Detecte todas as mudanças de janela no método
onResume()
daActivity
:
step1/MainActivity.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
- Agora, abra o arquivo
FoldingStateActor
para preencher o métodocheckFoldingState()
.
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.
- Confira se a
activityLayoutInfo
contém algumas propriedades doDisplayFeature
e que pelo menos uma delas sejaFoldingFeature
. Caso contrário, você não precisa fazer nada:
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- 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.
- Confira se
FoldingFeature
éHALF_OPEN
. Se não for, restaure apenas a posição do conteúdo. Se forHALF_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.
- 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.