Edge-to-edge no Android: fazendo certo

O passado Google I / O 2019 trouxe muitas inovações sensacionais, muitas das quais afetarão o setor de desenvolvimento móvel nos próximos anos. Não foi menos interessante seguir as tendências emergentes. Primeiro, as teclas de controle mecânico caíram na história, as telas dos smartphones ficaram maiores e os quadros laterais ficaram mais discretos. Os gestos substituíram os botões do sistema na tela, deixando cada vez mais espaço para o consumo de conteúdo. Os aplicativos são exibidos em toda a superfície visível da tela, da parte inferior à moldura superior, sem se restringir aos limites condicionais da barra de status e do painel de navegação. Estamos à beira de uma era de ponta a ponta.



O que é borda a borda? Literalmente entendido, isso significa que seu aplicativo deve ser exibido em toda a superfície visível da tela, do quadro inferior ao quadro superior, sem se restringir à barra de status e aos botões de navegação inferiores.


Borda a borda no exemplo do shell do sistema Android.

Quando se trata do Android, uma idéia simples está longe de ser sempre fácil de implementar. Neste artigo, falaremos sobre como maximizar o uso de todo o espaço disponível na tela de qualquer dispositivo, independentemente do fabricante, versão do sistema e a variedade de configurações que os fabricantes de dispositivos do Reino Médio (e não apenas) gostam de agradar aos usuários. O código apresentado no artigo foi testado em mais de 30 dispositivos por nós pessoalmente e em 231 dispositivos diferentes por 100 mil usuários de nossos aplicativos.

O problema de criar uma interface borda a borda não é novo por si só e era relevante muito antes da E / S 2019. Certamente, cada um de vocês se lembrará de como primeiro pesquisou algo da categoria: "barra de status transparente do android" ou "gradiente da barra de status do android" "

Os principais critérios para o aplicativo corresponder ao título de borda a borda são:

  • barra de status transparente;
  • barra de navegação transparente.

Leia mais sobre eles no material.io .


O aplicativo Deezer não se preocupa com a conformidade de ponta a ponta

É importante notar que não estamos falando sobre removê-los completamente, como no " modo de tela cheia ". Deixamos ao usuário a oportunidade de ver informações importantes do sistema e usar a navegação familiar.

Um requisito igualmente importante para uma solução é escalabilidade e extensibilidade. Existem vários outros:

  • Desloque corretamente a tela acima do teclado sem interromper o suporte para os sinalizadores AdjustResize da Activity;
  • Evite sobrepor a barra de status e a barra de navegação nos elementos da interface do usuário do aplicativo, enquanto exibe o plano de fundo correspondente sob eles;
  • Trabalhe em todos os dispositivos com versões atuais do Android e pareça idêntico.

Pouco de teoria


Pode demorar muito tempo para encontrar uma solução para uma tarefa aparentemente simples, o que não será fácil para o gerente de projeto explicar. E quando o controle de qualidade ainda encontra o smartphone infeliz, no qual sua tela não parece "de acordo com os cânones" ...
Em nosso projeto, nos enganamos várias vezes. Apenas um mês depois, depois de passar por uma longa série de tentativas e erros, resolvemos o problema de uma vez por todas.

Primeiro de tudo, você precisa entender como o Android desenha os painéis do sistema. A partir do Android 5.0, foi fornecida uma API conveniente para trabalhar com recuos do sistema ao longo das bordas horizontais da tela. Eles são chamados WindowInsets e, na figura abaixo, são coloridos em vermelho:


Além disso, os desenvolvedores da equipe do Android adicionaram ouvintes que permitem que você assine alterações nesses recuos, por exemplo, quando o teclado aparecer. A rigor, WindowInsets são as margens do seu arquivo de layout a partir das bordas da tela. Ao redimensionar sua Atividade (modo de tela dividida, aparência do teclado), o Inset também será alterado. Portanto, para oferecer suporte de ponta a ponta, precisamos garantir que esses recuos não estejam lá. Uma tela com WindowInsets nulo terá a seguinte aparência:


Implementação


Em nossa implementação, operaremos ativamente no Window e em seus sinalizadores.
Todos os exemplos serão escritos em Kotlin, mas você pode implementá-los facilmente em Java, usando utilitários em vez de funções de extensão.

A primeira coisa a fazer com o elemento de layout raiz é definir explicitamente o sinalizador:

android:fitsSystemWindows="true" 

Isso é necessário para garantir que a Vista raiz seja desenhada sob os elementos do sistema, bem como para as medidas corretas do Inset ao assinar sua alteração.
Agora nos voltamos para a coisa mais importante - removemos as bordas da tela! No entanto, isso deve ser feito com muito cuidado. E aqui está o porquê:

  1. Zerando a inserção inferior, corremos o risco de perder a reação da janela à aparência do teclado: há dezenas de dicas no StackOverflow para redefinir a inserção superior, mas as inferiores são delicadamente silenciosas. Por esse motivo, o NavigationBar não se mostra completamente transparente. Ao redefinir a inserção inferior, o sinalizador AdjustResize para de funcionar.

    Solução: Cada vez que você altera o Inset, determine se a altura mais baixa do teclado está contida nele e somente restaure-a.
  2. Quando você redefine o Inset, as partes visíveis da View aparecem na barra de status e na barra de navegação. De acordo com o conceito de design de materiais (e senso comum), nenhum elemento ativo deve ser localizado nas áreas do sistema. Ou seja, nessa área não deve haver botões, campos para inserir texto, caixas de seleção etc.

    Solução: adicionaremos um ouvinte ao ouvinte para que, ao alterar WindowInsets, traduza os recuos do sistema para a Atividade e responda a eles internamente, definindo os preenchimentos e margens corretos para a Visualização.


Esse comportamento não deve ser permitido (a barra de ferramentas rastreia na barra de status).

A função removeSystemInsets () tem a seguinte aparência:

 fun removeSystemInsets(view: View, listener: OnSystemInsetsChangedListener) { ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets -> val desiredBottomInset = calculateDesiredBottomInset( view, insets.systemWindowInsetTop, insets.systemWindowInsetBottom, listener ) ViewCompat.onApplyWindowInsets( view, insets.replaceSystemWindowInsets(0, 0, 0, desiredBottomInset) ) } } 

A função modifyDesiredBottomInset () calcula a inserção inferior com ou sem o teclado, dependendo da configuração atual do dispositivo.

 fun calculateDesiredBottomInset( view: View, topInset: Int, bottomInset: Int, listener: OnSystemInsetsChangedListener ): Int { val hasKeyboard = isKeyboardAppeared(view, bottomInset) val desiredBottomInset = if (hasKeyboard) bottomInset else 0 listener(topInset, if (hasKeyboard) 0 else bottomInset) return desiredBottomInset } 

Use o método isKeyboardAppeared () para verificar a altura do teclado . Confiamos na hipótese de que o teclado não pode ocupar menos de um quarto da altura da tela. Se desejar, você pode modificar a lógica de verificação como desejar.

 private fun View.isKeyboardAppeared(bottomInset: Int) = bottomInset / resourdisplayMetrics.heightPixels.toDouble() > .25 

O método removeSystemInsets () usa um ouvinte. Na verdade, essas são apenas tipealias para uma expressão lambda. Seu código completo:

 typealias OnSystemBarsSizeChangedListener = (statusBarSize: Int, navigationBarSize: Int) -> Unit 

O próximo passo é definir a transparência para as barras do sistema:

 window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT 

Depois de compilar tudo o que foi dito acima, obtemos o seguinte método:

 fun Activity.setWindowTransparency( listener: OnSystemInsetsChangedListener = { _, _ -> } ) { InsetUtil.removeSystemInsets(window.decorView, listener) window.navigationBarColor = Color.TRANSPARENT window.statusBarColor = Color.TRANSPARENT } 

Agora, para ativar o modo borda a borda da Atividade desejada, basta chamar a seguinte função no método onCreate () :

 setWindowTransparency { statusBarSize, navigationBarSize -> //  } 

Assim, em menos de 30 linhas de código, alcançamos um efeito "de ponta a ponta", sem violar nenhum princípio de UX e sem privar o usuário dos controles usuais do sistema. Essa implementação pode parecer simples e trivial para alguém, mas é isso que garante a operação confiável do seu aplicativo em qualquer dispositivo.
Você pode obter o efeito "de ponta a ponta" de centenas de maneiras diferentes (o número de dicas sobre o StackOverflow é uma confirmação clara disso), mas muitas delas levam a comportamentos incorretos em diferentes versões do Android ou não levam em consideração parâmetros como a necessidade de exibição longa. listas ou quebre o redimensionamento da tela ao mostrar o teclado




Voar na pomada


A solução descrita neste artigo é adequada para todos os dispositivos atuais. Real significa dispositivos no Android Lollipop (5.0) e superior. Para eles, a solução acima funcionará perfeitamente. Mas para versões mais antigas do Android, você precisará de sua própria implementação, pois nada era conhecido sobre o WindowInsets naquela época.

A boa notícia é que no Android KitKat (4.4), a transparência dos painéis do sistema ainda é suportada. Mas versões mais antigas não suportam tanta beleza, você nem pode tentar.

Vamos nos concentrar na mudança de inserções no Android 4.4. Isso pode ser feito no método fitSystemWindows () . Portanto, o elemento principal em seu layout deve ser um contêiner com um método fitSystemWindows substituído, contendo exatamente a mesma implementação que nosso ouvinte no exemplo de versões atuais do Android.

 class KitkatTransparentSystemBarsFrame @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = -1 ) : FrameLayout(context, attrs, defStyleAttr), KitkatTransparentSystemBarsContainer { override var onSystemInsetsChangedListener: OnSystemInsetsChangedListener = { _, _ -> } override fun fitSystemWindows(insets: Rect?): Boolean { insets ?: return false val desiredBottomInset = InsetUtil.calculateDesiredBottomInset( this, insets.top, insets.bottom, onSystemInsetsChangedListener ) return super.fitSystemWindows(Rect(0, 0, 0, desiredBottomInset)) } 

Em dispositivos com Android 4.4, apenas a transparência parcial funciona através da configuração de sinalizadores translúcidos:

 window.addFlags( WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION ) 

Esses sinalizadores tornam as barras do sistema translúcidas, adicionando um leve gradiente a elas, que, infelizmente, não podem ser removidas. No entanto, o gradiente pode ser transformado em uma barra de cores translúcida usando esta biblioteca: https://github.com/jgilfelt/SystemBarTint . Ela nos resgatou mais de uma vez no passado. As alterações mais recentes foram feitas na biblioteca há 5 anos, portanto apenas revelam seu charme para verdadeiras cidades retrógradas.

Todo o processo de sinalização para Kitkat terá a seguinte aparência:

 fun Activity.setWindowTransparencyKitkat( rootView: KitkatTransparentSystemBarsContainer, listener: OnSystemBarsSizeChangedListener = { _, _ -> } ) { rootView.onSystemBarsSizeChangedListener = listener window.addFlags( WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION ) } 

Com isso em mente, estamos escrevendo um método universal que pode tornar as barras do sistema transparentes (ou pelo menos translúcidas), independentemente de qual aplicativo esteja sendo executado em um dispositivo com qual versão do Android:

 when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> setWindowTransparency(::updateMargins) Build.VERSION.SDK_INT == Build.VERSION_CODES.KITKAT -> setWindowTransparencyKitkat(root_container, ::updateMargins) else -> { /*do nothing*/ } } 

No spoiler abaixo, você pode ver como a amostra apresentada no artigo se parece com alguns dos dispositivos problemáticos:

Screenshots
Huawei Honor 8, Android 7.0



Xiaomi Redmi Note 4, Android 6.1



HTC Desire Dual Sim, Android 4.4.2



Samsung J3, Android 7.0



Meizu M3s, Android 5.1



Asus Zenfone 3 Max (Android 6.0)



Umi Roma, Android 5.0



Nexus 5X, Android 8.0



Samsung Galaxy S8, Android 9.0





Concluindo, quero dizer que a solução para uma tarefa aparentemente tão simples como definir transparência para elementos da interface do usuário do sistema pode arrastar você por toda a variedade de armadilhas e não levar ao resultado desejado, mas causará bugs desagradáveis. Ainda bem que você tem este artigo agora.

Você pode encontrar uma lista completa do programa e uma amostra do trabalho em nosso repositório git .

O material é inspirado no relatório de Chris Banes "Tornando-se um instalador de janela principal".


Agradeço ao Surf studio e a Evgeny Saturov pela ajuda na preparação do material.

Source: https://habr.com/ru/post/pt464373/


All Articles