Inserções do Android: lidando com medos e se preparando para o Android Q

O Android Q é a décima versão do Android com API nível 29. Uma das principais idéias da nova versão é o conceito de ponta a ponta, quando os aplicativos ocupam a tela inteira, de baixo para cima. Isso significa que a barra de status e a barra de navegação devem ser transparentes. Mas, se forem transparentes, não haverá interface do usuário do sistema - ela se sobrepõe aos componentes interativos do aplicativo. Este problema é resolvido com inserções.

Os desenvolvedores de dispositivos móveis evitam inserções, pois causam medo neles. Mas no Android Q, não será possível contornar as inserções - você precisará estudá-las e aplicá-las. De fato, não há nada complicado nas inserções: elas mostram quais elementos da tela se cruzam com a interface do sistema e sugerem como mover o elemento para que não entre em conflito com a interface do usuário do sistema. Konstantin Tskhovrebov falará sobre como as inserções funcionam e como são úteis.


Konstantin Tskhovrebov (terrakok) trabalha em Redmadrobot . Está envolvido no Android há 10 anos e acumulou muita experiência em vários projetos nos quais não havia lugar para inserções, eles sempre conseguiam se locomover de alguma forma. Konstantin vai contar sobre uma longa história de evitar o problema das inserções, sobre estudar e combater o Android. Ele considerará tarefas típicas de sua experiência nas quais as inserções poderiam ser aplicadas e mostrará como parar de ter medo do teclado, reconhecer seu tamanho e responder à aparência.

Nota. O artigo foi escrito com base em um relatório de Konstantin no Saint AppsConf 2019 . O relatório usou materiais de vários artigos sobre inserções. Link para esses materiais no final.

Tarefas típicas


Barra de status de cores. Em muitos projetos, o designer desenha uma barra de status colorida. Tornou-se moda quando o Android 5 veio junto com o novo Material Design.



Como pintar a barra de status? Elementar - adicione colorPrimaryDarke a cor é substituída.

 <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="colorPrimaryDark">@color/colorAccent</item>
    ...
</style>

Para Android acima da quinta versão (API de 21 e superior), você pode definir parâmetros especiais no tópico:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
    ...
    <item name="android:statusBarColor">@color/colorAccent</item>
    ...
</style>

Barra de status multicolorida . Às vezes, designers em telas diferentes desenham a barra de status em cores diferentes.



Tudo bem, a maneira mais fácil de trabalhar é com diferentes tópicos em diferentes atividades .

A maneira mais interessante é mudar as cores diretamente no tempo de execução .

override fun onCreateView(...): View {
    requireActivity().window.statusBarColor = requireContext().getColor(R.color.colorPrimary)
    ...
}

O parâmetro é alterado através de um sinalizador especial. Mas o principal é não esquecer de mudar a cor novamente quando o usuário sair da tela novamente.

Barra de status transparente. Isso é mais difícil. Na maioria das vezes, a transparência está associada aos mapas, porque é nos mapas que a transparência é melhor visualizada. Nesse caso, como antes, definimos um parâmetro especial:

<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">    
    ...
    <item name="android:windowTranslucentStatus">true</item>
    ...
</style>

Aqui, é claro, existe um truque bem conhecido - recuar mais alto, caso contrário, a Barra de Status será sobreposta ao ícone e será feia.


Mas em outras telas tudo quebra.



Como resolver o problema? O primeiro método que vem à mente são atividades diferentes: temos tópicos diferentes, parâmetros diferentes, eles funcionam de maneira diferente.

Trabalhe com o teclado. Evitamos inserções não apenas com a barra de status, mas também ao trabalhar com o teclado.



Ninguém gosta da opção à esquerda, mas para transformá-la em uma opção à direita, existe uma solução simples.

<activity
    ...
    android:windowSoftInputMode="adjustResize">
    ...
</activity>

A atividade agora pode ser redimensionada quando o teclado aparecer. Funciona de forma simples e severa. Mas não se esqueça de outro truque - envolva tudo ScrollView. De repente, o teclado ocupará a tela inteira e haverá uma pequena faixa no topo?

Há momentos mais difíceis quando queremos alterar o layout quando o teclado aparece. Por exemplo, organize os botões ou oculte o logotipo.



Treinamos tantas muletas com um teclado e uma barra de status transparente, agora é difícil nos impedir. Vá para StackOverflow e copie o código bonito.

boolean isOpened = false;

public void setListenerToRootView() {
    final View activityRootView = getWindow().getDecorView().findViewById(android.R.id.content);
    activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
    
            int heightDiff = activityRootView.getRootView().getHeight() - activityRootView.getHeight();      
            if (heightDiff > 100) { // 99% of the time the height diff will be due to a keyboard.       
                Toast.makeText(getApplicationContext(), "Gotcha!!! softKeyboardup", 0).show();

                if (isOpened == false) {
                   //Do two things, make the view top visible and the editText smaller
                }
                isOpened = true;
            } else if (isOpened == true) {
                Toast.makeText(getApplicationContext(), "softkeyborad Down!!!", 0).show();
                isOpened = false;
            }    
        }
    });
}

Ele até calculou a probabilidade de um teclado aparecer. O código funciona, em um dos projetos que até o usamos, mas por um longo tempo.

Muitas muletas nos exemplos estão associadas ao uso de diferentes atividades. Mas são ruins não apenas porque são muletas, mas também por outras razões: o problema do “arranque a frio”, assincronia. Descrevi os problemas com mais detalhes no artigo " Licença para dirigir um carro, ou por que os aplicativos devem ser de atividade única ". Além disso, a documentação do Google indica que a abordagem recomendada é um aplicativo de atividade única.

O que fizemos antes do Android 10


Nós (da Redmadrobot) estamos desenvolvendo aplicativos de alta qualidade, mas há muito que evitamos inserções. Como conseguimos evitá-los sem muletas grandes e em uma atividade?

Nota. Capturas de tela e código retirados do meu projeto de estimação, GitFox .

Imagine a tela do aplicativo. Quando desenvolvemos nossos aplicativos, nunca pensamos que poderia haver uma barra de navegação transparente abaixo. Existe uma barra preta abaixo? Então, o que os usuários estão acostumados?



No topo, inicialmente definimos o parâmetro de que a barra de status é preta com transparência. Como fica em termos de layout e código?



Na figura, abstração: o bloco vermelho é a atividade do aplicativo, o azul é um fragmento com um bot de navegação (com guias) e dentro dele são trocados fragmentos verdes com conteúdo. Pode ser visto que a barra de ferramentas não está na barra de status. Como conseguimos isso?

O Android tem uma bandeira complicada fitSystemWindow. Se você configurá-lo como verdadeiro, o contêiner adicionará preenchimento a si mesmo, para que nada dentro dele caia na barra de status. Acredito que esta bandeira é a muleta oficial do Google para quem tem medo de inserir. Usar tudo funcionará relativamente bem sem inserções.

O sinalizador FitSystemWindow=”true”adiciona preenchimento ao contêiner que você especificou. Mas a hierarquia é importante: se um dos pais definir esse sinalizador como "verdadeiro", sua distribuição não será levada em consideração, porque o contêiner já aplicou o recuo.

A bandeira funciona, mas outro problema aparece. Imagine uma tela com duas guias. Ao alternar, uma transação é iniciada, o que causa um replacefragmento um no outro e tudo quebra.



O segundo fragmento também possui um sinalizador definido FitSystemWindow=”true”, e isso não deve acontecer. Mas o que aconteceu, por que? A resposta é que isso é uma muleta e, às vezes, não funciona.

Mas encontramos uma solução no Stack Overflow : use root para não fragmentos FrameLayout, mas CoordinatorLayout. Foi criado para outros fins, mas funciona aqui.

Por que isso funciona?


Vamos ver na fonte o que está acontecendo CoordinatorLayout.

@Override
public void onAttachedToWindow() {   
    super.onAttachedToWindow();
    ...    
    if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {         
        // We're set to fitSystemWindows but we haven't had any insets yet...
        // We should request a new dispatch of window insets
        ViewCompat.requestApplyInsets(this);
    }     
    ...
}

Vemos um belo comentário de que inserções são necessárias aqui, mas não são. Precisamos pedir novamente quando solicitamos uma janela. Descobrimos que as inserções funcionam de alguma forma por dentro, mas não queremos trabalhar com elas e sair Coordinator.

Na primavera de 2019, desenvolvemos o aplicativo, momento em que o Google I / O acabou de passar. Ainda não descobrimos tudo, então continuamos com o preconceito. Mas percebemos que alternar as guias na parte inferior é de alguma forma lenta, porque temos um layout complexo, carregado com a interface do usuário. Encontramos uma maneira simples de resolver isso - mude replacepara show/hidepara que cada vez que você não recrie o layout.



Mudamos e, novamente, nada funciona - tudo quebrou! Aparentemente, não é possível espalhar muletas, é preciso entender por que elas funcionaram. Estudamos o código e verifica-se que qualquer ViewGroup também pode trabalhar com inserções.


@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {    
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {         
        final int count = getChildCount();        
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);             
            if (insets.isConsumed()) {          
                break;
            }
        }
    }
    return insets;
}

Existe uma lógica interna: se pelo menos alguém já processou inserções, todas as Views subseqüentes dentro do ViewGroup não as receberão. O que isso significa para nós? Deixe-me mostrar um exemplo do nosso FrameLayout, que alterna as guias.



Dentro, há o primeiro fragmento que tem o sinalizador definido fitSystemWindow=”true”. Isso significa que o primeiro fragmento processa inserções. Depois disso, chamamos o HIDEprimeiro fragmento e o SHOWsegundo. Mas o primeiro permanece no layout - sua visualização é deixada, apenas oculta.

O contêiner passa por sua Visualização: pega o primeiro fragmento, fornece inserções e a partir dele fitSystemWindow=”true”- ele os pegou e processou. Perfeitamente, o FrameLayout parecia que as inserções foram processadas e não a deram para o segundo fragmento. Tudo funciona como deveria. Mas isso não nos convém, o que devemos fazer?

Nós escrevemos nosso próprio ViewGroup


Nós viemos de Java, e lá OOP em toda a sua glória, então decidimos herdar. Nós escrevemos nosso próprio ViewGroup, a partir do qual redefinimos o método dispatchApplyWindowInsets. Funciona da maneira que precisamos: sempre devolve às crianças as inserções que vieram, independentemente de processá-las ou não.

class WindowInsetFrameLayout @JvmOverloads constructor(
    context: Context,      
    attrs: AttributeSet? = null,     
    defStyleRes: Int = 0
) : FrameLayout(context, attrs, defStyleRes) {

    override fun dispatchApplyWindowInsets(insets: WindowInsets): WindowInsets { 
        for (child in (0 until childCount)) {
            getChildAt(child).dispatchApplyWindowInsets(insets)
        }
        return insets.consumeSystemWindowInsets()
    }
}

O ViewGroup personalizado funciona, o projeto também é tudo o que precisamos. Mas ele ainda não chega a uma solução geral. Quando descobrimos o que eles nos disseram no Google IO, percebemos que não poderíamos mais agir assim.

Android 10


O Android 10 nos mostrou dois conceitos importantes da interface do usuário que são estritamente recomendados: borda a borda e navegação gestual.

Borda a borda. Esse conceito sugere que o conteúdo do aplicativo deve ocupar todo o espaço possível na tela. Para nós, como desenvolvedores, isso significa que os aplicativos devem ser colocados nos painéis do sistema Barra de status e Barra de navegação.



Anteriormente, poderíamos ignorar isso ou ser colocados apenas sob a barra de status.

Quanto às listas, elas devem rolar não apenas para o último elemento, mas também para não permanecer embaixo da Barra de Navegação.


Navegação Gestual. Este segundo conceito importante é a navegação por gestos . Permite controlar o aplicativo com os dedos a partir da borda da tela. O modo de gesto não é padrão, tudo parece diferente, mas agora você não pode alternar entre duas barras de navegação diferentes.

Nesse momento, percebemos que nem tudo é tão simples. Não será possível evitar ainda mais as inserções se quisermos desenvolver aplicativos de qualidade. É hora de estudar a documentação e entender o que são inserções.

Insets O que você precisa saber sobre eles?


Insets foram criados para assustar os desenvolvedores.
Eles estão fazendo isso perfeitamente desde o momento em que apareceu no Android 5.

Claro, nem tudo é tão assustador. O conceito de inserções é simples - eles relatam uma sobreposição da interface do usuário do sistema na tela do aplicativo. Pode ser uma barra de navegação, barra de status ou um teclado. O teclado também é uma interface do usuário regular do sistema, sobreposta à parte superior do aplicativo. Não é necessário tentar processá-lo com muletas, apenas inserções.

O objetivo das inserções é resolver conflitos . Por exemplo, se houver outro elemento acima do nosso botão, podemos movê-lo para que o usuário possa continuar usando o aplicativo.

Para manipular a inserção, use Windowlnsetspara o Android 10 e WindowInsetsCompatpara outras versões.

Existem 5 tipos diferentes de inserções no Android 10e um "bônus", que é chamado não inserido, mas de outra forma. Lidamos com todos os tipos, porque a maioria sabe apenas uma coisa - Inserções na janela do sistema.

Inserções da janela do sistema


Introduzido no Android 5.0 (API 21). Obteve-se pelo método getSystemWindowInsets().

Esse é o principal tipo de inserção com o qual você precisa aprender a trabalhar, porque o restante funciona da mesma maneira. Eles são necessários para processar a barra de status, primeiro e depois a barra de navegação e o teclado. Por exemplo, eles resolvem um problema quando a barra de navegação está acima do aplicativo. Como na imagem: o botão permaneceu sob a barra de navegação, o usuário não pode clicar nele e está muito infeliz.



Inserções de elementos ajustáveis


Apareceu apenas no Android 10. Obtido pelo método getTappableElementInsets().

Essas inserções são úteis apenas para lidar com vários modos da barra de navegação . Como o próprio Chris Bane diz , você pode esquecer esse tipo de inserção e contornar apenas as inserções da janela do sistema. Mas se você deseja que o aplicativo seja 100% legal, não 99,9% legal, você deve usá-lo.

Olha a foto.



No topo, a cor vermelha indica Inserções da janela do sistema, que serão exibidas em diferentes modos da barra de navegação. Pode-se observar que, à direita e à esquerda, são iguais à barra de navegação.

Lembre-se de como a navegação por gestos funciona: no modo certo, nunca clicamos em um novo painel, mas sempre arrastamos os dedos de baixo para cima. Isso significa que os botões não podem ser removidos. Podemos continuar pressionando o botão FAB (botão de ação flutuante), ninguém irá interferir. Portanto, ele TappableElementInsetsficará vazio porque o FAB não é necessário para se mover. Mas se subirmos um pouco mais, tudo bem.

A diferença aparece apenas na navegação por gestos e na barra de navegação transparente (adaptação de cores). Esta não é uma situação muito agradável.



Tudo vai funcionar, mas parece desagradável. O usuário pode ficar confuso com a proximidade dos elementos. Pode-se explicar que um é para gestos e o outro é para pressionar, mas ainda não é bonito. Portanto, levante o FAB mais alto ou deixe-o à direita.

Inserções de gesto do sistema


Apareceu apenas no Android 10. Obtido pelo método getSystemGestureInsets().

Essas inserções estão relacionadas à navegação por gestos. Inicialmente, assumiu-se que a interface do usuário do sistema é desenhada no topo do aplicativo, mas as Inserções de gestos do sistema dizem que não é a interface do usuário que é desenhada, mas o próprio sistema lida com os gestos. Eles descrevem onde o sistema manipulará os gestos por padrão.

As áreas dessas inserções são aproximadamente as marcadas em amarelo no diagrama.



Mas quero avisar você - eles nem sempre serão assim. Nunca saberemos quais novas telas a Samsung e outros fabricantes criarão. Já existem dispositivos nos quais a tela está em todos os lados. Talvez as inserções não estejam onde esperamos. Portanto, você precisa trabalhar com eles como em alguma abstração: existem inserções no System Gesture Insets nas quais o próprio sistema processa gestos.

Essas inserções podem ser substituídas. Por exemplo, você está desenvolvendo um editor de fotos. Em alguns lugares, você mesmo deseja lidar com gestos, mesmo que eles estejam próximos à borda da tela. Indique ao sistema que você processará o ponto na tela no canto da foto. Isso pode ser redefinido dizendo ao sistema: "Eu sempre processarei o quadrado em torno do ponto."

Inserções obrigatórias de gestos no sistema


Apareceu apenas no Android 10. Este é um subtipo de Inserções de gestos do sistema, mas elas não podem ser substituídas pelo aplicativo.

Usamos o método getMandatorySystemGestureInsets()para determinar a área em que eles não funcionarão. Isso foi feito intencionalmente, para que fosse impossível desenvolver um aplicativo “sem esperança”: redefina o gesto de navegação de baixo para cima, o que permite sair do aplicativo na tela principal. Aqui, o sistema sempre processa os próprios gestos .


Não necessariamente estará na parte inferior do dispositivo e não necessariamente desse tamanho. Trabalhe com isso como uma abstração.

Esses eram tipos relativamente novos de inserções. Mas existem aqueles que apareceram antes do Android 5 e foram chamados de forma diferente.

Inserções estáveis


Introduzido com o Android 5.0 (API 21). Obteve-se pelo método getStableInsets().

Mesmo desenvolvedores experientes nem sempre podem dizer por que são necessários. Vou lhe contar um segredo: essas inserções são úteis apenas para aplicativos em tela cheia: reprodutores de vídeo, jogos. Por exemplo, no modo de reprodução, qualquer interface do usuário do sistema está oculta, incluindo a Barra de Status, que se move para além da borda da tela. Mas vale a pena tocar na tela quando a barra de status aparecer na parte superior.

Se você colocar qualquer botão VOLTAR na parte superior da tela e ele processar as Inserções da janela do sistema corretamente, a cada guia uma UI aparecerá na tela de cima e o botão pulará. Se você não tocar na tela por um tempo, a barra de status desaparecerá, o botão saltará porque os botões da janela do sistema desapareceram.

Para esses casos, os insertos estáveis ​​estão certos. Eles dizem que agora nenhum elemento é desenhado no seu aplicativo, mas pode fazê-lo. Com essas inserções, você pode conhecer antecipadamente o valor da barra de status nesse modo, por exemplo, e posicionar o botão onde desejar.

Nota. O método getStableInsets()apareceu apenas com a API 21. Anteriormente, havia vários métodos para cada lado da tela. 

Examinamos 5 tipos de inserções, mas há mais uma que não se aplica diretamente às inserções. Ajuda a lidar com franja e recortes.

Franjas e decotes


As telas não são mais quadradas. Eles são oblongos, com um ou mais recortes para a câmera e franja por todos os lados.



Até 28 APIs, não conseguimos processá-las. Eu tive que adivinhar através do interior o que estava acontecendo lá. Mas com a API 28 e além (do Android anterior), a classe apareceu oficialmente DisplayCutout. Está nas mesmas inserções das quais você pode obter todos os outros tipos. A classe permite que você descubra a localização e o tamanho dos artefatos.

Além das informações de localização, um conjunto de sinalizadores y é fornecido para o desenvolvedor WindowManager.LayoutParams. Eles permitem incluir comportamentos diferentes em torno dos recortes. Seu conteúdo pode ser exibido ao redor deles ou não: nos modos paisagem e retrato de maneiras diferentes.

Bandeiraswindow.attributes.layoutInDisplayCutoutMode =

  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT. No retrato - existe e na paisagem - uma barra preta por padrão.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER. Nem um pouco - uma faixa preta.
  • LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES - sempre é.

A tela pode ser controlada, mas trabalhamos com eles da mesma maneira que com outros Insets.

insets.displayCutout
    .boundingRects
    .forEach { rect -> ... }

A chamada de retorno com inserções com uma matriz chega até nós displayCutout. Então podemos executá-lo, processar todos os recortes e franjas que estão no aplicativo. Você pode aprender mais sobre como trabalhar com eles no artigo .

Entendido com 6 tipos de inserções. Agora vamos falar sobre como isso funciona.

Como funciona


As inserções são necessárias, antes de tudo, quando algo é desenhado no topo do aplicativo, por exemplo, a Barra de Navegação.



Sem inserções, o aplicativo não terá uma barra de navegação e uma barra de status transparentes. Não se surpreenda que você não vem SystemWindowInsets, isso acontece.

Spread insets


Toda a hierarquia da interface do usuário se parece com uma árvore com uma visualização nas extremidades. Os nós geralmente são um grupo de exibição. Eles também herdam do View, de modo que os insets chegam a eles de uma maneira especial dispatchApplyWindowInsets. A partir da visualização raiz, o sistema envia inserções por toda a árvore. Vamos ver como o View funciona neste caso.



O sistema chama o método dispatchApplyWindowInsetspelo qual as inserções entram. Voltar a este modo de exibição deve retornar algo O que fazer para lidar com esse comportamento?

Para simplificar, consideramos apenas o WindowInsets.

Manipulação de inserções


Antes de tudo, a redefinição do método dispatchApplyWindowInsetse implementação dentro de sua própria lógica parece lógica : inserções vieram, implementamos tudo dentro.

Se você abrir a classe View e observar o Javadoc escrito sobre esse método, poderá ver: “Não substitua esse método!”
Lidamos com Insets por meio de delegação ou herança.
Use delegação . O Google ofereceu a oportunidade de expor seu próprio delegado, responsável por lidar com inserções. Você pode instalá-lo através do método setOnApplyWindowInsetsListener(listener). Definimos um retorno de chamada que lida com essas inserções.

Se, por algum motivo, isso não nos convém, você pode herdar do View e substituir outro método onApplyWindowInsets(WindowInsets insets) . Nós substituímos nossas próprias inserções nele.

O Google não escolheu entre delegação e herança, mas permitiu fazer tudo juntos. Isso é ótimo porque não precisamos redefinir toda a barra de ferramentas ou o ListView para lidar com inserções. Podemos pegar qualquer Visualização pronta, mesmo uma biblioteca, e definir o delegado lá, que manipulará inserções sem redefinição.

O que devemos retornar?

Por que devolver alguma coisa?


Para fazer isso, você precisa entender como a distribuição de inserções, ou seja, ViewGroup, funciona. O que ela está fazendo dentro de si mesma. Vamos dar uma olhada no comportamento padrão usando o exemplo que mostrei anteriormente.

As inserções do sistema vieram na parte superior e inferior, por exemplo, 100 pixels cada. O ViewGroup os envia para a primeira visualização que possui. Este modo de exibição os manipulou de algum modo e retorna inserções: no topo ele diminui para 0, mas no fundo ele sai. No topo, ela acrescentou preenchimento ou margem, mas no fundo ela não tocou e relatou isso. Em seguida, o ViewGroup passa inserções para a segunda View.



A próxima exibição processa e renderiza inserções. Agora o ViewGroup vê que as inserções são processadas acima e abaixo - não resta mais nada, todos os parâmetros são zero.

A terceira visão nem sabe que havia algumas inserções e algo estava acontecendo. O ViewGroup retornará inserções de volta para quem as expôs.

Quando começamos a lidar com esse tópico, escrevemos a idéia para nós mesmos - sempre retornamos os mesmos detalhes que vieram. Queríamos evitar essa situação quando algum View nem reconhecia que havia inserções. A ideia parecia lógica. Mas acabou que não. O Google não adicionou em vão nenhum comportamento às inserções, como no exemplo.

Isso é necessário para que as inserções sempre atinjam toda a hierarquia da Visualização e você sempre possa trabalhar com elas. Nesse caso, não haverá situação quando trocarmos dois fragmentos, um foi processado e o segundo ainda não recebido. Na seção prática, retornaremos a esse ponto.

Prática


Teoria é feita. Vamos ver como fica o código, porque, em teoria, tudo é sempre tranquilo. Vamos usar o AndroidX.

Nota. No GitFox, tudo já está implementado no código, suporte completo para todos os presentes do novo Android. Para não verificar as versões do Android e não procurar o tipo de Insets necessário, sempre use a view.setOnApplyWindowInsetsListener { v, insets -> ... }versão do AndroidX ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> ... }.

Há uma tela com uma barra de navegação escura. Não sobrepõe nenhum elemento na parte superior. Tudo é do jeito que estamos acostumados.


Mas acontece que agora precisamos torná-lo transparente. Quão?

Ativar transparência


O mais simples é indicar no assunto que a barra de status e a barra de navegação são transparentes.

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
</style>

Nesse momento, tudo começa a se sobrepor, tanto de cima como de baixo.

Curiosamente, existem dois sinalizadores e sempre há três opções: se você especificar transparência para a barra de navegação, a barra de status ficará transparente por si só - uma limitação. Você não pode escrever a primeira linha, mas eu sempre adoro a clareza, por isso escrevo duas linhas para que os seguidores possam entender o que está acontecendo e não cavar por dentro.

Se você escolher esse método, a barra de navegação e a barra de status ficarão pretas com transparência. Se o aplicativo for branco, você poderá adicionar cores apenas a partir do código. Como fazer isso?

Ative a transparência com cores


É bom termos um aplicativo de atividade única, por isso colocamos dois sinalizadores em uma atividade.

rootView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or  
    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

<style name="AppTheme" parent="Theme.MaterialComponents.Light">
    <!-- Set the navigation bar to 50% translucent white -->
    <item name="android:navigationBarColor">#80FFFFFF</item> 
</style>

Existem muitos sinalizadores lá, mas são esses dois que ajudarão a tornar a transparência na barra de navegação e na barra de status. A cor pode ser especificada através do tema.

Esse é o comportamento estranho do Android: algo através do tema e algo através das bandeiras. Mas podemos especificar parâmetros no código e no assunto. Esse Android não é tão ruim assim, apenas nas versões mais antigas do Android o sinalizador especificado no tópico será ignorado. navigationBarColorEle destacará que no Android 5 isso não é verdade, mas tudo vai se unir. No GitFox, foi a partir do código que defini a cor da barra de navegação.

Fizemos fraudes - indicamos que a barra de navegação é branca com transparência. Agora, o aplicativo fica assim.


O que poderia ser mais fácil do que lidar com inserções?


De fato, elementar.

ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->  
        view.updatePadding(bottom = insets.systemWindowInsetBottom) 
        insets
    }

Pegamos o método setOnApplyWindowInsetsListenere o passamos para ele bottomNav. Dizemos que, quando as inserções chegarem, instale o preenchimento inferior que entrou systemWindowInsetBottom. Queremos isso por baixo, mas todos os nossos elementos clicáveis ​​estão no topo. Retornamos totalmente inserções para que todas as outras visualizações em nossa hierarquia também as recebam.

Tudo parece ótimo.


Mas há um porém. Se no layout indicamos algum tipo de preenchimento na barra de navegação abaixo, então aqui foi excluído - definir updatePaddinginserções iguais. Nosso layout não parece como gostaríamos.

Para salvar os valores do layout, você deve primeiro salvar o que está no preenchimento inferior. Mais tarde, quando inserções chegarem, adicione e defina os valores resultantes.

val bottomNavBottomPadding = bottomNav.paddingBottom
ViewCompat
    .setOnApplyWindowInsetsListener(bottomNav) { view, insets ->                 
        view.updatePadding(
        bottom = bottomNavBottomPadding + insets.systemWindowInsetBottom
    )
    Insets
}

É inconveniente escrever desta maneira: em todo o código em que você usa inserções, é necessário salvar o valor do layout e adicioná-lo à interface do usuário. Mas existe o Kotlin, e isso é maravilhoso - você pode escrever sua própria extensão, o que fará tudo isso por nós.

Adicione Kotlin!


Lembramos initialPaddinge entregamos ao manipulador quando novas inserções vêm (junto com elas). Isso os ajudará de alguma forma a se juntar ou a construir algum tipo de lógica de cima.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {    

    val initialPadding = recordInitialPaddingForView(this)

    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
    
    requestApplyInsetsWhenAttached()
}

private fun recordInitialPaddingForView(view: View) =
   Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

  .

bottomNav.doOnApplyWindowInsets { view, insets, padding ->   
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    ) 
    insets
}

Temos que redefinir a lambda. Possui não apenas inserções, mas também almofadas. Podemos adicioná-los não apenas de baixo, mas também de cima, se for uma barra de ferramentas que processa a barra de status.

Algo esquecido!


Há uma chamada para um método incompreensível.

fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, Rect) -> WindowInsetsCompat) {

    val initialPadding = recordInitialPaddingForView(this)
     
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->        
        block(v, insets, initialPadding)
    }
      
    requestApplyInsetsWhenAttached() 
}

private fun recordInitialPaddingForView(view: View) =
      Rect(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)

Isso se deve ao fato de que você não pode apenas esperar que o sistema lhe envie inserções. Se criarmos uma View a partir do código ou instalarmos um InsetsListenerpouco mais tarde, o próprio sistema poderá não passar o último valor.

Devemos verificar se, quando a tela está na tela, informamos ao sistema: "Chegaram as inserções, queremos processá-las". Definimos o ouvinte e devemos fazer uma solicitação requestsApplyInsets".

fun View.requestApplyInsetsWhenAttached() {    
    if (isAttachedToWindow) {        
        requestApplyInsets()
    } else {
        addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {            
            override fun onViewAttachedToWindow(v: View) {
                v.removeOnAttachStateChangeListener(this)
                v.requestApplyInsets()           
            }

            override fun onViewDetachedFromWindow(v: View) = Unit
        })
    }
}

Se criamos um código de exibição e ainda não o anexamos ao nosso layout, devemos assinar o horário em que o fazemos. Somente então solicitamos inserções.

Mas, novamente, há Kotlin: eles escreveram uma extensão simples e não precisamos mais pensar nisso.

Refinando o RecyclerView


Agora vamos falar sobre a finalização do RecyclerView. O último elemento cai sob a barra de navegação e permanece abaixo - feio. Também é inconveniente clicar nele. Se este não é um painel novo, mas o antigo, um grande, geralmente o elemento inteiro pode desaparecer sob ele. O que fazer?



A primeira idéia é adicionar um elemento abaixo e definir a altura para ajustar as inserções. Mas se tivermos um aplicativo com centenas de listas, de alguma forma teremos que assinar cada lista para inserir, adicionar um novo elemento lá e definir a altura. Além disso, o RecyclerView é assíncrono, não se sabe quando funcionará. Existem muitos problemas.

Mas há outro truque oficial. Surpreendentemente, ele funciona com eficiência.

<androidx.recyclerview.widget.RecyclerView 
        android:id="@+id/recyclerView"        
    android:layout_width="match_parent"    
    android:layout_height="match_parent"             
    android:clipToPadding="false" />

recyclerView.doOnApplyWindowInsets { view, insets, padding ->    
    view.updatePadding(
        bottom = padding.bottom + insets.systemWindowInsetBottom
    )
}

Existe uma bandeira no layout clipToPadding. Por padrão, é sempre verdade. Isso sugere que você não precisa desenhar elementos que apareçam onde o preenchimento está exposto. Mas se definido clipToPadding="false", você pode desenhar.

Agora, se você definir o preenchimento na parte inferior, no RecyclerView funcionará assim: o preenchimento será definido na parte inferior e o elemento será desenhado em cima dele até rolar. Quando chegar ao fim, o RecyclerView rolará os elementos para a posição que precisamos.

Ao definir um desses sinalizadores, podemos trabalhar com o RecyclerView como em um modo de exibição normal. Não pense que existem elementos que rolam - basta definir o preenchimento abaixo, como, por exemplo, na barra de navegação inferior.

Agora sempre retornamos inserções como se não fossem processadas. As inserções inteiras chegaram até nós, fizemos algo com elas, ajustamos os forros e devolvemos todas as inserções inteiras novamente. Isso é necessário para que qualquer ViewGroup sempre passe essas inserções para todas as View. Funciona.

Bug do aplicativo


Em muitos aplicativos no Google Play que já processaram inserções, notei um pequeno bug. Agora vou falar sobre ele.

Há uma tela com navegação no porão. À direita está a mesma tela que mostra a hierarquia. O fragmento verde exibe o conteúdo na tela, dentro dele tem um RecyclerView.



Quem lidou com as inserções aqui? Barra de ferramentas: ele aplicou o preenchimento na parte superior para que seu conteúdo se mova sob a barra de status. Consequentemente, a barra de navegação inferior aplicava inserções de baixo e subia acima da barra de navegação. Mas o RecyclerView não manipula inserções de forma alguma, não se enquadra nelas, não precisa processar inserções - tudo é feito corretamente.



Mas aqui acontece que queremos usar o fragmento verde RecyclerView em outro local onde a barra de navegação inferior não está mais lá. Neste ponto, o RecyclerView já está começando a lidar com inserções a partir de baixo. Precisamos aplicar preenchimento a ele para rolar adequadamente sob a barra de navegação. Portanto, no RecyclerView, também adicionamos processamento de inserções.



Voltamos à nossa tela, onde todos processam inserções. Lembre-se, ninguém relata que ele os processou?



Vemos esta situação: O RecyclerView processou inserções a partir de baixo, embora não atinja a barra de navegação - um espaço apareceu na parte inferior. Notei isso em aplicativos, e bastante grande e popular.



Se for esse o caso, lembramos que retornamos todas as inserções para processar. Então (acontece!) É necessário relatar que as inserções são processadas: A Barra de Navegação deve relatar que as inserções foram processadas. Eles não devem acessar o RecyclerView.



Para fazer isso, instale a barra de navegação inferior InsetsListener. Subtraia dentro bottom + insetsque eles não são processados, retorne 0.

doOnApplyWindowInsets { view, insets, initialPadding ->    
    view.updatePadding(
        bottom = initialPadding.bottom + insets.systemWindowInsetBottom
    )
    insets.replaceSystemWindowInsets(
        Rect(
            insets.systemWindowInsetLeft,            
            insets.systemWindowInsetTop,            
            insets.systemWindowInsetRight,
            0
        )
    )
}

Retornamos novas inserções, nas quais todos os parâmetros antigos são iguais, mas bottom + insetsiguais a 0. Parece que está tudo bem. Começamos repetidamente sem sentido - o RecyclerView ainda lida com inserções por algum motivo.



Não entendi imediatamente que o problema é que o contêiner para esses três modos de exibição é LinearLayout. Dentro deles, há uma barra de ferramentas, um trecho com um RecyclerView e na parte inferior da barra de navegação inferior. O LinearLayout coloca seus filhos em ordem e aplica inserções neles.

Acontece que a barra de navegação inferior será a última. Ele disse a alguém que processou todas as inserções, mas era tarde demais. Todas as inserções foram processadas de cima para baixo, o RecyclerView já as recebeu e isso não nos salva.



O que fazer? O LinearLayout não funciona da maneira que eu quero, ele os transfere de cima para baixo e preciso obter o primeiro.

Redefinir tudo


O desenvolvedor Java jogou em mim - tudo precisa ser redefinido ! Bem, agora dispatchApplyWindowInsetsredefina, defina yLinearLayout, que sempre passará de baixo para cima. Ele primeiro envia inserções da barra de navegação inferior e depois para todos os outros.

@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {     
    insets = super.dispatchApplyWindowInsets(insets);     
    if (!insets.isConsumed()) {        
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            insets = getChildAt(i).dispatchApplyWindowInsets(insets);
            if (insets.isConsumed()) {
                break;
            }
        }
    }
    return insets;
}

Mas parei a tempo, lembrando do comentário de que não havia necessidade de redefinir esse método.

Aqui, um pouco maior é a salvação: o ViewGroup verifica se há um delegado que processa as inserções, depois chama o super método no View e ativa seu próprio processamento. Nesse código, obtemos inserções e, se ainda não foram processadas, iniciamos a lógica padrão para nossos filhos.

Eu escrevi uma extensão simples. Permite, aplicando InsetsListenera uma Visualização, dizer a quem essas inserções passam.

fun View.addSystemBottomPadding(    
    targetView: View = this
) {
    doOnApplyWindowInsets { _, insets, initialPadding ->           
        targetView.updatePadding(
            bottom = initialPadding.bottom + insets.systemWindowInsetBottom
        )
        insets.replaceSystemWindowInsets(
            Rect(
                insets.systemWindowInsetLeft,
                insets.systemWindowInsetTop,     
                insets.systemWindowInsetRight,
                0
            )
        )
    }
}

Aqui, targetViewpor padrão , é igual à mesma visualização na qual aplicamos addSystemBottomPadding. Nós podemos redefinir isso.

No meu LinearLayout, pendurei um manipulador, passando como targetView- esta é minha barra de navegação inferior.

  • Primeiro ele dará inserções na barra de navegação inferior. 
  • Isso processará inserções, retornará zero ao fundo.
  • Além disso, por padrão, eles vão de cima para baixo: Barra de ferramentas, fragmenta com o RecyclerView.
  • Então, talvez, ele volte a enviar inserções na barra de navegação inferior. Mas isso não é mais importante, tudo funcionará tão bem.

Consegui exatamente o que queria: todas as inserções são processadas na ordem em que são necessárias.



Importante


Algumas coisas importantes a ter em mente.

O teclado é a interface do usuário do sistema na parte superior do seu aplicativo. Não há necessidade de tratá-la de uma maneira especial. Se você olhar para o teclado do Google, não é apenas um Layout com botões nos quais você pode clicar. Existem centenas de modos para este teclado: pesquisando gifs e memes, entrada de voz, redimensionando de 10 pixels de altura para tamanhos de tela. Não pense no teclado, mas assine inserções.

A gaveta de navegação ainda não suporta a navegação por gestos. No Google IO, eles prometeram que tudo daria certo. Mas no Android 10, o Navigation Drawer ainda não suporta isso. Se você atualizar para o Android 10 e ativar a navegação com gestos, o Navigation Drawer cairá. Agora você precisa clicar no menu do hambúrguer para que ele apareça, ou uma combinação de circunstâncias aleatórias permite esticá-lo.

Na versão pré-alfa do Android, o Navigation Drawer funciona, mas não me atrevi a atualizar - isso é pré-alfa. Portanto, mesmo se você instalar a versão mais recente do GitFox a partir do repositório, existe uma Gaveta de Navegação, mas ela não pode ser extraída. Assim que o suporte oficial for lançado, eu atualizarei e tudo funcionará bem.

Lista de verificação de preparação do Android 10


Defina a transparência da barra de navegação e da barra de status desde o início do projeto . O Google recomenda enfaticamente a manutenção de uma barra de navegação transparente. Para nós, essa é uma parte prática importante. Se o projeto funcionar, mas você não o ativou, escolha a hora de oferecer suporte ao Android 10. Ligue-o primeiro no assunto, como translúcido, e corrija o layout onde ele quebrou.

Adicione extensões ao Kotlin - é mais fácil com elas.

Adicione a toda a barra de ferramentas em cima do preenchimento. As barras de ferramentas estão sempre no topo, e é isso que você precisa fazer.

Para todos os RecyclerViews, adicione preenchimento na parte inferior e a capacidade de rolar clipToPadding="false".

Pense em todos os botões nas bordas da tela (FAB) . Os FABs provavelmente não estarão onde você espera.

Não substitua ou faça suas próprias implementações para todos os LinearLayout e outros casos semelhantes. Faça o truque, como no GitFox, ou use minha extensão pronta para ajudar.

Verifique os gestos nas bordas da tela para ver a exibição personalizada . A Gaveta de Navegação não suporta isso. Mas não é tão difícil apoiá-lo com as mãos, redefinir inserções para gestos na tela com a Gaveta de Navegação. Talvez você tenha editores de imagens em que os gestos funcionam.

A rolagem no ViewPager não funciona a partir da borda, mas apenas do centro para a direita e esquerda . O Google diz que isso é normal. Se você puxar o ViewPager pela borda, ele será percebido como pressionando o botão "Voltar".

Em um novo projeto, inclua imediatamente toda a transparência. Você pode dizer aos designers que não possui uma barra de navegação e uma barra de status. O quadrado inteiro é o conteúdo do seu aplicativo. E os desenvolvedores já descobrirão onde e o que criar.

Links Úteis:

  • @rmr_spb em um telegrama - registros de mitaps internos do Redmadrobot SPb.
  • Todo o código-fonte deste artigo e ainda mais no GitFox . Há um ramo separado do Android 10, onde você pode ver por confirmar como cheguei a tudo isso e como apoiei o novo Android.
  • Biblioteca Insetter de Chris Bane. Ele contém 5-6 extensões que mostrei. Para usar no seu projeto, entre em contato com a biblioteca, provavelmente ela será movida para o Android Jetpack. Eu os desenvolvi no meu projeto e, ao que me parece, eles melhoraram.
  • Artigo de Chris Bane.
  • FunCorn, , .
  • «Why would I want to fitsSystemWindows?» .

AppConf, youtube- . , - . AppsConf , . , , stay tuned!

All Articles