Como Melhorar a Experiência do Usuário Utilizando API de Mapa
(2839)
CategoriasAndroid, Design, Protótipo
AutorVinícius Thiengo
Vídeo aulas186
Tempo15 horas
ExercíciosSim
CertificadoSim
CategoriaEngenharia de Software
Autor(es)Vlad Khononov
EditoraAlta Books
Edição1ª
Ano2024
Páginas320
Tudo bem?
Neste artigo nós vamos a uma nova versão de nosso Android "app framework" para YouTubers.
Quando digo "nova versão", na verdade estou dizendo:
Vamos configurar um novo canal no aplicativo.
Canal que vai exigir mudanças "severas" ao menos na interface com o usuário.
Vamos também adicionar ao app uma experiência de usuário mais tangível.
Inserindo nos códigos do projeto parte das APIs de Mapas Android de Alta Qualidade:
Se você já está ciente sobre este projeto de aplicativo framework e já tem planos com ele. Ideias de monetização ou apenas de ampliação de portfólio.
Então é importante que você leia este artigo até o final.
Pois iremos discutir alguns pontos que acabei não levando em consideração quando desenvolvemos por completo todo esse projeto de app framework.
Pontos que afetam diretamente a produção. Produção na criação de varias versões do aplicativo.
É isso.
Antes de continuar, saiba que a versão PDF deste artigo está disponível somente aos inscritos da lista de e-mails do Blog.
Se você ainda não faz parte desta lista. Então não deixe de se inscrever para ter acesso a este e a outros conteúdos completos do Blog.
A seguir os tópicos que estaremos abordando neste artigo:
- Cheguei somente agora no projeto;
- Já conheço tudo. Quero o novo!;
- Portfólio de apresentação;
- Canal que será utilizado;
- Não banalize o valor (R$) de seu trabalho e esforço;
- Como será a partir daqui;
- Download e inicialização do projeto:
- Renomeando todo o projeto:
- Gerando chaves e IDs de API:
- Estrutura global:
- Atualizações por tela:
- Circuitos:
- Parceiros:
- Loja:
- Atividade principal;
- Atualizações para notificações push:
- Banco de dados;
- Resultado;
- GitHub do projeto;
- Conclusão;
- Fontes.
Cheguei somente agora no projeto
Se você ainda não conhece o projeto Android de app framework que desenvolvemos aqui no Blog.
Então é possível que você fique um pouco perdido no conteúdo deste artigo.
Sendo assim eu recomendo que você acesse primeiro este projeto a partir do artigo a seguir:
Lembrando que o projeto está todo finalizado.
Então, dependendo de sua motivação, é possível termina-lo em pouco tempo.
Toda a série deste projeto está bem dividida:
- Primeiro nós vamos ter uma visão Estratégica de toda a ideia de aplicativo;
- Logo depois nós vamos à parte Tática, aos códigos.
É isso.
A partir daqui vamos já criar mais uma versão de app para canais no YouTube... partindo de nosso aplicativo framework.
Já conheço tudo. Quero o novo!
Se você já está ok com este nosso projeto de app framework. Já o conhece "tudo" e não tem dificuldades em criar uma nova versão dele com um novo canal.
Neste caso é bem provável que você queira ir direto para as partes inéditas deste conteúdo, certo?
Sendo assim, siga o roteiro:
Parte inicial:
Leia tudo até o fim da seção Não banalize o valor (R$) de seu trabalho e esforço.
Parte intermediária:
Leia todo o conteúdo sobre "background drawable em SVG" presente na seção Background.
Parte final:
Então volte a ler somente a partir da seção Circuitos.
Com isso você estará acessando os trechos inéditos sobre este app framework.
Se quiser ir direto aos comentários da implementação da API de Mapas de Alta Qualidade, então acesse a seção Experiência realista.
Portfólio de apresentação
Se você já conhece o projeto e está prestes a atender a algum canal ou até mesmo já enviando contatos.
Então eu acho completamente aceitável que você utilize qualquer um dos três projetos Android que temos aqui no Blog e que já fazem uso deste framework que desenvolvemos juntos:
Mas espero que sua narrativa seja honesta com o cliente.
Ou seja:
Informe que são três exemplos de canais YouTube que estão configurados com o framework que você está oferecendo ao YouTuber.
Com o paragrafo acima você é 100% honesto com o possível cliente.
Pois em nenhum momento você informou que são clientes reais (não são) e que você foi o desenvolvedor(a) dos projetos. Apesar de ser completamente possível você fazer até melhor.
Coloquei está seção, pois pode ser que você já queira monetizar esse projeto de aplicativo framework. Porém ainda não tem em mãos algum já finalizado.
Então esses três projetos já prontos vão certamente facilitar a negociação com possíveis YouTubers clientes. Pois com o um portfólio disponível tudo fica mais claro na mesa de negociação.
Canal que será utilizado
Desta vez optei por um canal que é de uma marca muito popular e é também um canal extremamente ativo no YouTube.
Ou seja, libera inúmeros vídeos por dia.
Vamos seguir com o canal da Formula 1.
Canal que na verdade tem conteúdos de:
- Formula 1;
- Formula 2 (está daqui é realmente divertida, alta competitividade);
- e Formula 3.
Todos os dias têm vídeos novos e em alguns dias têm até seis novos vídeos. Isso mesmo, seis vídeos em um dia.
Muitos desses vídeos batem mais de 100k de visualizações em menos de 24 horas. Ou seja, o público do canal é bem ativo
A configuração prudente aqui de acesso aos servidores do YouTube (invocada em WorkManager) seria pelo menos de 4 em 4 horas.
Este é um canal de mais de 5 milhões de seguidores e que está realmente preocupado em aumentar ainda mais o número de subscribers.
Isso é um aspecto muito importante caso você queira monetizar esse projeto de app com algum canal YouTube...
...seguindo a terceira estratégia que mostro no último artigo deste aplicativo framework.
Não banalize o valor (R$) de seu trabalho e esforço
Quando eu iniciei mais está versão de projeto com o canal da Formula 1...
... eu queria mesmo era poder mostrar as possibilidades de modificações em app ainda maiores do que as que realizamos na versão de aplicativo para o canal Emagrecer de Vez (artigo 14 do projeto framework).
Queria também mostrar como é fácil adicionar uma funcionalidade de "realidade mais tangível". Isso com poucas linhas de código quando utilizando as APIs Android de Mapas de Alta Qualidade.
Imaginei que seria algo que rapidamente ficaria pronto, até porque eu estaria em uma versão de app com menos telas do que as versões anteriores.
São apenas seis telas aqui.
Voilà la.
Me enganei. 😅
Algo que eu não tinha percebido é que a parte gráfica, imagens, pode ser bem crítica de conseguir com facilidade se o domínio de problema do canal YouTube do app for bem "exótico".
Ok, Thiengo. Mas automobilismo não é nem de perto algo exótico.
Correto.
Porém até o momento que você precisa dos gráficos dos circuitos (as pistas de corrida) da temporada 2020 da Formula 1...
... e descobre que a única fonte que fornece o desenho de todos os circuitos com um design padrão, fornece em formato rasterizado quando na verdade você precisa de formato vetorial (SVG).
Então você descobre que para cada circuito, em imagem rasterizada, você terá que aplicar um bocado de trabalho manual que inclui até mesmo otimização de arquivo SVG.
Em resumo:
Um único e simples projeto, que deveria ficar pronto em poucas horas... devido ao uso de um framework já testado.
Esse projeto levou mais de um dia por causa da parte gráfica.
Pois o preenchimento de informações (textos) especificas de canal em app é algo trivial.
Mas trabalhar imagens complexas quando você não tem muita expertise no assunto ou não tem um designer do seu lado... ao menos essa parte não é nada simples.
Então, não banalize seu trabalho e esforço somente porque você tem um framework em mãos.
Aqui eu não vou lhe dar valores (R$) específicos, pois ainda não fiz testes de vendas do app em mercado. Pretendo fazer um artigo somente sobre isso.
Mas achei prudente adicionar essa parte aqui no conteúdo, principalmente para aumentar a sua confiança e assim você cobrar o que entender ser justo.
Como será a partir daqui
Como eu sei que você pode sim querer fazer tudo passo a passo.
Então vamos praticamente seguir o mesmo roteiro utilizado no penúltimo artigo desta série de app framework.
Ou seja, vamos em artigo apresentar cada um dos passos que são necessários para o aplicativo final ser um aplicativo de notificações YouTube. Notificações de novos vídeos do canal da Formula 1.
E diferente do que fizemos no artigo 14 deste projeto. Aqui teremos algumas modificações a mais no framework. Deixando claro ao desenvolvedor que as possibilidades são infinitas.
É isso.
Vamos ao projeto.
Download e inicialização do projeto
Vamos iniciar baixando o .zip do projeto no GitHub e depois abri-lo no Android Studio.
Baixando do GitHub
Acesse o repositório GitHub do projeto Android em:
https://github.com/viniciusthiengo/aplicativo-android-para-canais-do-youtube.
Logo depois:
- Clique em "Code";
- Então clique em "Download ZIP".
Agora, na área de aplicativos em seu ambiente de desenvolvimento, descompacte o ZIP.
Abrindo no Android Studio
Com o Android Studio aberto, na tela inicial:
- Clique em "Open an existing Android Studio project";
- Selecione o projeto que você acabou de baixar e descompactar. Projeto de nome "aplicativo-android-para-canais-do-youtube-master".
Aguarde até o final do carregamento em IDE.
Erro com o Git
Assim que o projeto for carregado por completo, provavelmente aparecerá um alerta de erro como a seguir:
Não se preocupe com erro de Git. Pois este é um inteiro novo projeto que vai partir de nosso app framework.
Sendo assim, siga os passos:
- No pop-up de alerta de erro clique em "Configure";
- Na caixa de diálogo que se abrir selecione (clique) a única opção na lista "Version Control";
- Com a opção selecionada, agora clique no sinal de menos (remover) que há no fundo da lista;
- Agora clique em "Apply";
- Por fim clique em "Ok".
Pronto.
Problema de configuração Git resolvido.
Renomeando todo o projeto
Antes de entrar nos trechos mais delicados de renomeação de projeto. Vamos primeiro renomear a pasta do projeto na área de aplicativos de seu sistema operacional.
Primeiro feche o projeto e remova ele da lista de apps do Android Studio IDE.
Logo depois coloque como rótulo da pasta do projeto o nome "CanalFormula1":
Por fim, realize todo o processo de migração de projeto no Android Studio. Acessando ele via "Open an existing Android Studio project".
Ignore os erros que aparecerem.
Pois iremos agora iniciar as renomeações dentro de projeto e aos poucos, de acordo com as atualizações, eles vão sendo resolvidos.
Root name
Com o aplicativo aberto no IDE e com a visualização de pacotes em modo "Android".
Neste contexto acesse o arquivo settings.gradle:
- Em rootproject.name coloque o valor "Canal Formula 1";
- Por fim clique em "Sync Now" e aguarde a sincronização do projeto.
Rótulos de pacotes
Para os pacotes, primeiro:
- Coloque a visualização de pacotes em modo "Project";
- Expanda tudo até chegar ao primeiro filho direto do pacote "java";
- Selecione esse pacote raiz (o primeiro filho direto do pacote "java");
- Clique em "Show Options Menu" - aquela engrenagem ⚙ que fica ao topo direito da estrutura de pacotes;
- Então clique (desmarque) a opção "Compact Middle Packages".
Você vai perceber que a estrutura de pacotes será dividida por completo.
Assim podemos partir para os pacotes raizes:
- Clique com o botão direito do mouse no pacote "canalviniciusthiengo";
- Acesse "Refactor" e clique em "Rename";
- No pop-up que se abrir clique em "Rename package";
- No novo pop-up que se abrir coloque em "Rename package (...)" o valor "youtubechannel";
- Então clique em "Refactor" e aguarde o processamento.
Muito bom.
Agora o pacote "thiengo":
- Clique com o botão direito do mouse no pacote "thiengo";
- Acesse "Refactor" e clique em "Rename";
- No pop-up que se abrir clique em "Rename package";
- No novo pop-up que se abrir coloque em "Rename package (...)" o valor "formula1";
- Então clique em "Refactor" e aguarde o processamento.
Excelente.
Ainda temos um pacote "br" perdido em projeto. Pois o site oficial do canal não tem o ".br". O domínio é apenas ".com": Formula1.com.
Sendo assim:
- Miniminize o pacote "youtubechannel";
- Clique neste pacote e arraste-o para dentro do pacote "com". Ele ficará como um pacote irmão de "br". O pacote "br" ficará vazio;
- No pop-up que se abrir, mantenha a opção "Move package (...)" marcada e clique em "Ok";
- No novo pop-up de "Warning" clique em "Yes";
- Agora, na caixa de diálogo de configuração de migração de pacote, clique em "Refactor";
- A caixa "Find" será acionada. Então clique em "Do Refactor" e aguarde o processamento;
- Por fim, clique com o botão direito do mouse no pacote "br" (agora vazio) e clique em "Delete";
- No pop-up que aparecer, apenas clique em "Delete".
Quase tudo finalizado!
Atualizando o Gradle
Abra o Gradle Nível de Aplicativo, ou build.gradle (Module: app).
No bloco defaultConfig modifique o valor de applicationId para formula1.com.youtubechannel como a seguir:
...
android {
...
defaultConfig {
applicationId "formula1.com.youtubechannel"
...
}
...
}
...
Sincronize o projeto.
Reconstrua o projeto
Com o projeto ainda aberto em IDE:
- Acesse o menu de topo e clique em "Build";
- Logo depois clique em "Clean Project";
- Ao final do processamento do clean acesse novamente "Build" e clique em "Rebuild Project".
Ainda no menu de topo:
- Clique em "File";
- Logo depois clique em "Invalidate Caches / Restart";
- No pop-up que se abrir clique em "Invalidate and Restart".
Ao final do processamento feche o projeto e remova ele da lista de apps da tela inicial do Android Studio.
Deletando arquivo
Entre na pasta do projeto e delete o folder oculto .idea.
É possível que dentro de CanalFormula1 ainda tenha arquivos e folders com o nome antigo do projeto.
Isso depende da versão do Android Studio que você estiver usando.
Se houverem arquivos e folders com o nome antigo do projeto, já neste ponto de renomeação de tudo. Então delete-os.
Agora acesse o Android Studio e abra o projeto novamente via "Open an existing Android Studio project".
Nova estrutura física
Agora temos um novo projeto com um nome de pacote exclusivo, exatamente como solicita a Google Play Store.
Nossa configuração final de pacotes renomeados ficou:
Vamos compactar os pacotes em IDE:
- Clicando em "Show Options Menus";
- Depois clicando em "Compact Middle Packages".
Temos:
Neste ponto o projeto não mais deverá estar apresentando erros.
Gerando chaves e IDs de API
Agora vamos às chaves de APIs e IDs que teremos que configurar em projeto para que seja possível:
- Obter dados do canal direto dos servidores do YouTube;
- e Enviar notificações push.
Dica importante:
Crie um novo Gmail para cada nova versão do app framework que você estiver desenvolvendo.
Mesmo que o YouTuber cliente já tenha um Gmail.
Crie um exclusivo para gerar todas as chaves e IDs do projeto e então entregue esse e-mail e dados de acesso a ele.
Identificador do canal
A URL do canal no YouTube é a seguinte: https://www.youtube.com/user/Formula1.
Assim:
- Entre em YouTube Channel ID;
- No campo "Youtube Channel URL, Video URL or username" coloque a URL do canal (que foi apresentada anteriormente);
- Clique em "Get YouTube Channel ID";
- Em "YOUTUBE CHANNEL ID, INFO & STATISTICS" você terá também o ID do canal:
- Copie o ID "UCB_qr75-ydFVKSF9Dmo6izg".
Classe de configuração
Em projeto, no Android Studio, acesse a classe de configuração YouTubeConfig.
Então atualize a constante CHANNEL_ID como a seguir:
...
abstract class Channel {
companion object {
...
const val CHANNEL_ID = "UCB_qr75-ydFVKSF9Dmo6izg"
...
}
}
...
OneSignal
Para gerar a chave e o ID do Google Firebase e também o ID do OneSignal.
Para isso siga o passo a passo em Dados que são pré-requisito da parte 10 do projeto.
Siga o passo a passo até ao fim desta seção da Parte 10.
Porque ao final dela você terá em mãos:
- SERVER KEY do Firebase Cloud Messaging;
- SENDER ID do Firebase Cloud Messaging;
- ID de projeto OneSignal.
Classe de configuração
Agora acesse a classe de configuração OneSignalConfig.
Atualize as constantes SERVER_KEY e SENDER_ID da classe interna Firebase e também a constante ID da classe interna App.
Atualize todas essas constantes com a chave e os IDs gerados na seção anterior:
...
abstract class Firebase {
companion object CloudMessage {
...
const val SERVER_KEY = "SDHsdNA..."
const val SENDER_ID = "okjuJIS..."
}
}
abstract class App {
companion object {
...
const val ID = "IgthhS..."
}
}
...
Gradle
Ainda é preciso atualizar o Gradle Nível de Aplicativo, ou build.gradle (Module: app), com o ID OneSignal.
Sendo assim, em manifestPlaceholders, coloque o ID de projeto OneSignal em onesignal_app_id como a seguir:
...
android {
...
defaultConfig {
...
manifestPlaceholders = [
onesignal_app_id: 'IgthhS...',
...
]
}
...
}
...
Sincronize o projeto.
YouTube Data API
Aqui nós vamos precisar somente da chave de desenvolvedor Google.
Para isso siga o passo a passo em Geração de chave no Google Console da parte 11 do projeto.
Ao final desse passo a passo você terá em mãos uma Google Dev Key.
Classe de configuração
Agora acesse a classe de configuração YouTubeConfig.
Então atualize a constante GOOGLE_DEV com a chave de desenvolvedor Google que você gerou na seção anterior:
...
abstract class Key {
companion object {
...
const val GOOGLE_DEV = "GtHYtydfO..."
}
}
...
Assim, com as configurações de chaves e IDs finalizadas, nós estamos prontos para atualizarmos os dados de canal, e parte da estrutura de código, no projeto de aplicativo Android.
Estrutura global
Nesta seção estaremos atualizando os pontos de projeto que atendem à todas as telas ou que não têm vinculo direto com somente uma tela.
Um ponto importante a ressaltar aqui é:
As propriedades que não forem citadas nas posteriores atualizações do app neste conteúdo...
... essas propriedades continuarão com os exatos mesmos valores que já tinham quando o projeto foi baixado do GitHub.
Quando for possível remover qualquer entidade do projeto, eu vou deixar explícito em texto.
Então é isso.
Vamos às atualizações.
Ícones de abertura
Os nossos ícones de abertura de aplicativo serão os seguintes:
Ícone retangular:
- /res/mipmap-mdpi/ic_launcher.png;
- /res/mipmap-hdpi/ic_launcher.png;
- /res/mipmap-xhdpi/ic_launcher.png;
- /res/mipmap-xxhdpi/ic_launcher.png;
- /res/mipmap-xxxhdpi/ic_launcher.png.
Ícone circular:
- /res/mipmap-mdpi/ic_launcher_round.png;
- /res/mipmap-hdpi/ic_launcher_round.png;
- /res/mipmap-xhdpi/ic_launcher_round.png;
- /res/mipmap-xxhdpi/ic_launcher_round.png;
- /res/mipmap-xxxhdpi/ic_launcher_round.png.
Realize o download de cada uma das versões rasterizadas de ícone e coloque-as em seus respectivos folders mipmap em projeto.
Splash Screen
Para a tela de abertura:
Vamos mudar as imagens e a cor de fundo.
Imagens
Imagens vetoriais não funcionam no modelo de tela de abertura que estamos utilizando. Então vamos manter o uso de imagens rasterizadas neste ponto do projeto.
Segue:
Imagem central:
- /res/drawable-mdpi/launcher_screen_logo.png;
- /res/drawable-hdpi/launcher_screen_logo.png;
- /res/drawable-xhdpi/launcher_screen_logo.png;
- /res/drawable-xxhdpi/launcher_screen_logo.png;
- /res/drawable-xxxhdpi/launcher_screen_logo.png.
Imagem de Powered by (desenvolvido por):
- /res/drawable-mdpi/powered_by.png;
- /res/drawable-hdpi/powered_by.png;
- /res/drawable-xhdpi/powered_by.png;
- /res/drawable-xxhdpi/powered_by.png;
- /res/drawable-xxxhdpi/powered_by.png.
Realize o download das imagens e coloque-as em seus respectivos folders drawable.
Cor de fundo
Vamos utilizar para cor de background a mesma cor da StatusBar (preto, #000000).
Assim, em /res/launcher_screen.xml, atualize a cor de background como a seguir:
...
<item android:drawable="@color/colorStatusBar" />
...
Atualizações de Splash Screen finalizadas.
Nome do app
No arquivo /res/values/strings.xml atualize o nome do aplicativo como a seguir:
...
<string name="app_name">
Canal Formula 1
</string>
...
Primary e accent colors
No arquivo /res/values/colors.xml atualize as cores das propriedades a seguir:
...
<color name="colorPrimary">#E20600</color>
<color name="colorPrimaryDark">#E20600</color>
<color name="colorAccent">#FFFFFF</color>
...
Background de conteúdo
Ainda em /res/values/colors.xml atualize a propriedade de cor de background de conteúdo, atualize como a seguir:
...
<color name="colorScreenBackground">#E20600</color>
...
Família de fontes
No projeto original nós temos, para títulos, uma família de fontes que é diferente da família de fontes padrão Android, Roboto.
A fonte "Quicksand Variable":
Para o canal Formula 1 vamos utilizar uma fonte que "casa" mais com um conteúdo de automobilismo:
Vamos utilizar a fonte "Racing Sans One".
Faça o download dela:
Depois coloque-a no folder /res/font do projeto.
Essa fonte foi baixada no Google Fonts.
Tema de título
Agora precisamos atualizar o tema de títulos de telas para utilizar a nova família de fontes.
Em /res/values/styles.xml atualize o valor de android:fontFamily no tema AppTheme.Title como a seguir:
...
<style name="AppTheme.Title">
...
<item name="android:fontFamily">@font/racing_sans_one_regular</item>
...
</style>
...
Topo
Para o topo:
Temos novas cores, rótulos, background e imagem.
Cores
No arquivo estático /res/values/colors.xml atualize as cores das propriedades a seguir:
...
<!-- Topo -->
<color name="colorStatusBar">#000000</color>
<color name="colorTopIcon">#FFFFFF</color>
<color name="colorTopText">#FFFFFF</color>
...
Você vai perceber que estaremos trabalhando em maioria com tons de vermelho.
Isso, pois o vermelho é uma das principais cores do site oficial da Formula 1. Vermelho, preto e branco.
Rótulos
No arquivo XML /res/values/strings.xml atualize as propriedades a seguir com os valores definidos:
...
<string name="channel_name">
FORMULA 1
</string>
<string name="channel_desc">
Mantenha-se atualizado com as últimas notícias e
conteúdos sobre a Fórmula 1
</string>
<string name="channel_thumb_content_desc">
Thumb do canal YouTube Formula 1.
</string>
<string name="channel_toast_alert">
É preciso ter o aplicativo do YouTube instalado
para acessar o canal da Formula 1.
</string>
...
Família de fontes
Com a nova família de fontes para títulos já definida em projeto. Podemos então atualizar o TextView tv_channel_name do layout de topo.
Em /res/layout/top_signature.xml atualize a família de fontes do TextView de título como a seguir:
...
<TextView
android:id="@+id/tv_channel_name"
...
android:fontFamily="@font/racing_sans_one_regular"
... />
...
Logo
Agora temos a logo do canal:
Imagem que deverá ter as suas cinco versões respeitando os 65dp x 65dp:
- /res/drawable-mdpi/channel_logo.png (65px x 65px);
- /res/drawable-hdpi/channel_logo.png (97.5px x 97.5px);
- /res/drawable-xhdpi/channel_logo.png (130px x 130px);
- /res/drawable-xxhdpi/channel_logo.png (195px x 195px);
- /res/drawable-xxxhdpi/channel_logo.png (260px x 260px).
Realize o download das imagens e coloque-as em seus respectivos folders drawable.
Ainda sobre a "imagem logo" de topo. Precisamos adicionar duas configurações ao CardView de top_signature.xml para que a logo fique como esperado em tela:
Em /res/layout/top_signature.xml atualize o único CardView do layout como a seguir:
...
<androidx.cardview.widget.CardView
android:id="@+id/cv_channel_logo"
...
android:background="@android:color/transparent"
android:backgroundTint="@android:color/transparent"
app:cardElevation="0dp">
...
Somente os atributos background, backgroundTint e cardElevation devem ser atualizados no CardView.
Background
Você já deve ter notado que nosso background de topo também está bem diferente do então comum background de waves, ondas.
Agora temos listras:
Sendo assim, nosso /res/drawable/bg_top.xml tem então o seguinte novo código fonte:
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="200dp"
android:height="100dp"
android:viewportWidth="1600"
android:viewportHeight="800">
<path
android:fillColor="#000000"
android:pathData="M0,0h1600v800h-1600z" />
<path
android:fillColor="#2d0100"
android:pathData="M1600,160l-1600,300l0,-110l1600,-300z" />
<path
android:fillColor="#5a0200"
android:pathData="M1600,260l-1600,300l0,-110l1600,-300z" />
<path
android:fillColor="#880400"
android:pathData="M1600,360l-1600,300l0,-110l1600,-300z" />
<path
android:fillColor="#b50500"
android:pathData="M1600,460l-1600,300l0,-110l1600,-300z" />
<path
android:fillColor="#e20600"
android:pathData="M1600,800l-1600,0l0,-50l1600,-300z" />
</vector>
Sim. Diferente do arquivo de waves, onde as ondas eram externas e referenciadas uma a uma. Aqui temos toda uma estrutura vetorial desenvolvida diretamente em arquivo.
Acredite: não precisei criar essa nova estrutura. Digo, não na "unha".
O background de topo anterior foi gerado de maneira trivial no seguinte site de criação de backgrounds em SVG:
Há inúmeros outros sites para a geração não somente de backgrounds em SVG, mas também de qualquer tipo de drawable.
Um outro também popular é o:
Hero Patterns - A collection of repeatable SVG background patterns for you to use on your web projects.
Uma opinião sincera:
Caso você ainda não tenha bons backgrounds de topo para seus aplicativos.
Então não deixe de salvar em sua caixa de ferramentas Android ao menos os dois sites anteriores de background SVG.
Pois esses backgrounds dão mais modernidade ao design de qualquer aplicativo... com triviais linhas de código que basicamente temos que "copiar e colar".
Thiengo, ok. E o problema de imagens vetoriais com mais de 200dp x 200dp?
Excelente colocação.
Confesso que foi a primeira coisa que pensei assim que trabalhei com backgrounds em formato vetorial.
Como resultado obtive:
Nenhum problema de sobrecarga de memória ou demora na renderização do layout em tela.
Tendo em mente que todos os arquivos vetoriais de background utilizados até o momento em projeto. Todos esses são arquivos pequenos em bytes.
Sendo assim... mesmo com a ressalva na documentação oficial sobre vetoriais acima dos 200dp x 200dp...
... mesmo com isso, vamos prosseguir com nossa configuração atual em app, pois está funcionando "redondo".
Por fim, você pode seguramente deletar de projeto todos os arquivos vetoriais de waves que não são mais utilizados.
Menu
Para o bottom menu principal:
Temos novas cores, rótulos e ícones vetoriais.
Cores
No arquivo estático /res/values/colors.xml atualize as cores das propriedades a seguir:
...
<!-- Bottom Menu -->
<color name="colorMenuSeparatorLine">#E20600</color>
<color name="colorMenuBackground">@android:color/transparent</color>
<color name="colorMenuItemBackground">@android:color/transparent</color>
<color name="colorMenuItemSelected">#FFFFFF</color>
<color name="colorMenuItemNotSelected">#A00000</color>
...
É isso mesmo que você está vendo, @android:color/transparent.
Quando for necessário para conseguirmos ao final do projeto uma versão de app framework que melhor representa o canal YouTube cliente...
... vamos sim utilizar a "cor" transparente.
Dessa forma não precisaremos, por exemplo, ter que atualizar:
- Ou código dinâmico de lógica (Kotlin);
- Ou o código estático de layout (XML).
Basta colocar a visualização com a "cor" transparente que assim vai aparentar que o componente visual "não esta presente em layout".
Rótulos
Em /res/values/strings.xml atualize as propriedades a seguir com os respectivos valores definidos:
...
<!-- Menu -->
<string name="item_menu_last_video">
Vídeo
</string>
<string name="item_menu_play_lists">
PlayLists
</string>
<string name="item_menu_social_networks">
Redes
</string>
<string name="item_menu_circuits">
Circuitos
</string>
<string name="item_menu_partners">
Parceiros
</string>
<string name="item_menu_store">
Loja
</string>
...
Todas as propriedades estáticas de rótulos de menu que não estão na definição anterior. Essas propriedades podem ser removidas do arquivo de Strings.
Lembrando que quando alguma entidade puder ser removida, eu vou de maneira explícita lhe informar.
Ícones
Agora os ícones vetoriais de menu. Já lhe adianto que todos foram alterados.
Assim temos:
Para a opção Vídeo, ic_helmet.xml:
Para a opção Redes, ic_social_networks.xml:
Para a opção PlayLists, ic_play_lists.xml:
Para a opção Circuitos (pistas de corrida), ic_circuits.xml:
Para a opção Parceiros, ic_partners.xml:
Para a opção Loja, ic_store.xml:
Realize o download de cada um dos ícones e coloque-os no folder /res/drawable.
Outro ponto onde também é possível remover recursos:
Os ícones de menu não mais utilizados. Todos esses podem ser removidos.
Identificadores de opções
Outro arquivo estático que estará passando por atualizações é o arquivo de IDs.
Mais precisamente o arquivo /res/values/ids.xml.
Este ficará exatamente como a seguir:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="last_video" />
<item type="id" name="play_lists" />
<item type="id" name="social_networks" />
<item type="id" name="circuits" />
<item type="id" name="partners" />
<item type="id" name="store" />
</resources>
Nós poderíamos ter mantido a configuração anterior de IDs. O problema é que os rótulos anteriores, alguns deles, não representavam mais a responsabilidade da entidade ID utilizada em código.
Exemplo:
Ou o ID antigo courses seria utilizado no lugar do ID partners, somente para reaproveitarmos a definição de ID e não ser necessário criar um novo.
Ou o ID courses ficaria em código sem ser utilizado. Ou seja, mesmo assim criaríamos o ID partners.
O que foi informado acima é, nada mais nada menos, que o princípio de um código mal escrito.
Se preocupe, mesmo que seja um detalhe pequeno, em utilizar rótulos que indicam o que a entidade faz. Mesmo sendo algo simples: um ID.
Dados fixos
Por fim os dados fixos que preencherão a lista de itens que esta vinculada ao RecyclerView do menu.
Acesse a classe persistência MenuItemsData e coloque no método getItems() a seguinte nova configuração de dados:
...
fun getItems( res: Resources )
= listOf(
MenuItem(
id = R.id.last_video,
label = res.getString( R.string.item_menu_last_video ),
icon = R.drawable.ic_helmet,
isSelected = MenuItemStatus.SELECTED
),
MenuItem(
id = R.id.play_lists,
label = res.getString( R.string.item_menu_play_lists ),
icon = R.drawable.ic_play_lists
),
MenuItem(
id = R.id.social_networks,
label = res.getString( R.string.item_menu_social_networks ),
icon = R.drawable.ic_social_networks
),
MenuItem(
id = R.id.circuits,
label = res.getString( R.string.item_menu_circuits ),
icon = R.drawable.ic_circuits
),
MenuItem(
id = R.id.partners,
label = res.getString( R.string.item_menu_partners ),
icon = R.drawable.ic_partners
),
MenuItem(
id = R.id.store,
label = res.getString( R.string.item_menu_store ),
icon = R.drawable.ic_store
)
)
...
Pronto.
Podemos ir às telas de projeto.
Atualizações por tela
Aqui vamos às atualizações específicas de cada uma das telas do canal que teremos em app.
Desta vez não temos tanta "coincidência" como tivemos em Nós Temos Um Framework Em Mãos - YouTuber Android App - Parte 14 quando colocamos em projeto de app um canal que tinha as exatas mesmas telas que a versão inicial do framework.
Desta vez estaremos adicionando algumas telas e todos os recursos necessários a elas.
Então, vamos aos códigos.
Último vídeo
A tela de "último vídeo" disponibilizado em canal terá cores, rótulo de título, ícones e dados fixos diferentes da versão inicial de framework:
Cores
No arquivo XML /res/values/colors.xml atualize as propriedades a seguir com os valores definidos:
...
<color name="colorContentTitle">#FFFFFF</color>
<color name="colorContentText">#FFFFFF</color>
<!-- Video player -->
<color name="colorVideoPlayerBackground">#000000</color>
<color name="colorVideoPlayerForeground">#BB000000</color>
<color name="colorVideoPlayerIcon">#FFFFFF</color>
<!-- YouTube block -->
<color name="colorVideoPlayerYBBackground">#D20000</color>
<color name="colorVideoPlayerYBText">#FFFFFF</color>
<color name="colorVideoPlayerYBIcon">#940000</color>
<!-- Comment block -->
<color name="colorVideoPlayerCBBackground">#D20000</color>
<color name="colorVideoPlayerCBText">#FFFFFF</color>
<color name="colorVideoPlayerCBIcon">#940000</color>
...
Rótulo de título
Para os rótulos da tela de último vídeo teremos atualização somente no rótulo de título.
Assim, em /res/values/strings.xml, atualize a propriedade a seguir:
...
<string name="last_video_content_title">
Assista agora
</string>
...
Ícones
Se você reparar bem nós temos dois novos ícones nas pequenas caixas abaixo do player de vídeo:
Na verdade um desses dois ícones já está em projeto. É o mesmo ícone YouTube à direita no topo do app:
Ícone da logo do YouTube, ic_youtube_logo.xml:
E tem o novo ícone de comentário:
Ícone de "deixe o seu comentário", ic_comment_color.xml:
Este último ícone ainda não está presente em projeto.
Sendo assim, por segurança, realize o download de ambos os ícones e coloque-os no folder drawable.
Dados fixos
Na classe persistência LastVideoData coloque a seguinte nova definição de dados iniciais:
...
fun getInitialVideo()
= LastVideo(
uid = "yWfUg5ja90k",
title = "Top 10 Moments of Pit Lane Drama",
description = "The quest to shave off precious " +
"milliseconds in the pits is always a " +
"dangerous game. Get it right, and you " +
"might prosper. Get it wrong, and, well, " +
"here are ten examples..."
)
.apply {
thumbUrl = ""
}
...
É isso mesmo que você notou: os dados são todos em inglês.
Aparentemente ao menos no canal YouTube da Formula 1 é sempre assim, tudo em inglês.
Agora podemos ir para a configuração de outra tela.
Redes
A tela de Redes sociais e sites disponibilizados em canal terá cores, rótulo e dados fixos diferentes da versão inicial de framework:
Cores
No arquivo estático /res/values/colors.xml atualize as propriedades a seguir com os valores definidos:
...
<!-- Item de lista -->
<color name="colorListItemBackground_RippleStart">#D20000</color>
<color name="colorListItemBackground_RippleEnd">@android:color/transparent</color>
<color name="colorListItemBackgroundInfoSide">#940000</color>
<color name="colorListItemStroke">#E20600</color>
<color name="colorListItemText">#FFFFFF</color>
<color name="colorListItemLinealIcon">#940000</color>
<color name="colorListItemNoDataIcon">#940000</color>
...
Note que adicionamos duas novas propriedades de cor:
- colorListItemBackgroundInfoSide;
- colorListItemLinealIcon.
A propriedade colorListItemBackgroundInfoSide é para uma parte do layout de itens de circuito, pista de corrida, que vamos desenvolver.
Esse layout será diferente do layout comum para itens já disponível em framework:
A propriedade colorListItemLinealIcon é para manter os ícones de itens de listas exatamente com a mesma cor:
Sendo assim, já para a propriedade colorListItemLinealIcon, ainda falta atualizar o estilo de ícones de lista para que todos tenham a mesma cor.
Em /res/values/styles.xml atualize o tema AppTheme.ListItemIcon adicionando o atributo android:tint exatamente como a seguir:
...
<style name="AppTheme.ListItemIcon">
...
<item name="android:tint">@color/colorListItemLinealIcon</item>
</style>
...
Rótulo de título
Aqui também teremos um novo rótulo de título.
Em /res/values/strings.xml atualize a propriedade a seguir com o valor definido:
...
<string name="social_networks_content_title">
Acompanhe a Formula 1 em todas as redes
</string>
...
Ícones
Aqui vamos utilizar ícones que realmente são a logo da rede ou site em lista.
Para o Blog, ic_f1_blog_color.xml:
Para a conta Facebook, ic_facebook_color.xml:
Para a conta Twitter, ic_twitter_color.xml:
Para a conta Instagram, ic_instagram_color.xml:
Para a conta YouTube, ic_youtube_color.xml:
Realize o download dos ícones e coloque-os no folder /res/drawable.
Dados fixos
Na classe persistência SocialNetworksData coloque a seguinte nova definição de dados no método getNetworks():
...
fun getNetworks()
= listOf(
SocialNetwork(
network = "Site oficial",
accountName = "Formula1.com",
webUri = "https://www.formula1.com/",
logo = R.drawable.ic_f1_blog_color
),
SocialNetwork(
network = "Facebook",
accountName = "/Formula1",
webUri = "https://www.facebook.com/Formula1",
logo = R.drawable.ic_facebook_color
),
SocialNetwork(
network = "Twitter",
accountName = "/F1",
webUri = "https://twitter.com/f1",
logo = R.drawable.ic_twitter_color
),
SocialNetwork(
network = "Instagram",
accountName = "/F1",
webUri = "https://www.instagram.com/f1/",
logo = R.drawable.ic_instagram_color
),
SocialNetwork(
network = "YouTube",
accountName = "/F1",
webUri = "https://www.youtube.com/F1",
logo = R.drawable.ic_youtube_color
)
)
...
Classe de domínio
Como não mais estaremos utilizando a propriedade appUri da classe SocialNetwork. Então podemos remover essa propriedade e o método getAppUri() desta classe.
Ela ficará como a seguir:
...
class SocialNetwork(
val network: String,
private val accountName: String,
private val webUri: String,
private val logo: Int ) : ListItem {
override fun getMainText()
= String.format(
"%s: %s",
network,
accountName
)
override fun getWebUri()
= Uri.parse( webUri )
override fun getIcon()
= logo
}
Vamos então à próxima tela.
PlayLists
Agora a tela de PlayLists. Aliás com muitas PlayLists. Pois o canal alvo tem inúmeras delas:
Rótulo de título
Em /res/values/strings.xml atualize a propriedade de título a seguir:
...
<string name="playlists_content_title">
PlayLists
</string>
...
Ícone
O ícone vetorial de cada PlayList será exatamente o mesmo para todas:
Ícone de cada PlayList do canal, ic_playlist_color.xml:
Realize o download do ícone e coloque-o no folder /res/drawable de sua versão de projeto.
Dados fixos
Coloque no método getInitialPlayLists(), da classe persistência PlayListsData, cada uma das PlayLists já disponíveis em canal.
Como a seguir:
...
fun getInitialPlayLists()
= mutableListOf(
PlayList(
title = "F1 Esports 2020",
uid = "PLfoNZDHitwjXAEbWFSL4hJsUnkF14y_rw"
),
PlayList(
title = "2020 F1 Pole Positions | Onboard Hot Laps",
uid = "PLfoNZDHitwjUG6Nq8W0XLC_ke3s90wb3M"
),
PlayList(
title = "Fernando Alonso Returns to F1!",
uid = "PLfoNZDHitwjVHvU4_yRzxojvLKbsKWDxZ"
),
PlayList(
title = "Best Team Radio 2020",
uid = "PLfoNZDHitwjWck2ndC4PdIN3-c9ccbIgB"
),
PlayList(
title = "Formula 1 Rolex Belgian Grand Prix 2020",
uid = "PLfoNZDHitwjV0jtnJr3410xKMT8It8_HZ"
),
PlayList(
title = "Formula 1 Aramco Gran Premio De España 2020",
uid = "PLfoNZDHitwjWFvaGTH2tZLHlUfqN8t90k"
),
PlayList(
title = "Emirates Formula 1 70th Anniversary Grand Prix 2020",
uid = "PLfoNZDHitwjX6EY2Sfdn6tHy9zFbnCnrd"
),
PlayList(
title = "What F1 Feels Like",
uid = "PLfoNZDHitwjXvx_7bXfxatL8vE7dfJMRN"
),
PlayList(
title = "The Legends",
uid = "PLfoNZDHitwjWnlZdS3mk5ttOZlqkLr9Nz"
),
PlayList(
title = "F1 70th Anniversary",
uid = "PLfoNZDHitwjUynZnW8XMmbhvnK1Qi2nR7"
),
PlayList(
title = "2021 Driver Moves",
uid = "PLfoNZDHitwjUnV6lmfFRHUOztgyXrVbd1"
),
PlayList(
title = "F1 Challenge Series 2020",
uid = "PLfoNZDHitwjVgUCeBbSOaLihLI3Jvh-B7"
),
PlayList(
title = "F1 Classics",
uid = "PLfoNZDHitwjUlydEe_RAZ2IgaKCIDxoUd"
),
PlayList(
title = "F1 Virtual GPs 2020",
uid = "PLfoNZDHitwjU1moRPhV163T1QHTpkui89"
),
PlayList(
title = "F1 Rewind!",
uid = "PLfoNZDHitwjXSniIQHvZPchn056vJq_Dn"
),
PlayList(
title = "2020 FIA Formula 3 Championship",
uid = "PLfoNZDHitwjXjQ9cvNSinbW6ezSKjxM8y"
),
PlayList(
title = "2020 FIA Formula 2 Championship",
uid = "PLfoNZDHitwjVGnDvKlYNZUIK4f7UK1iLC"
),
PlayList(
title = "Formula 1 Rolex Grosser Preis Von Österreich 2020",
uid = "PLfoNZDHitwjXMBeShm_2iOq34Q7dlXmBA"
),
PlayList(
title = "Formula 1 Pirelli Grosser Preis Der Steiermark 2020",
uid = "PLfoNZDHitwjWvN5xRm8-xTyrScl2ybuAZ"
),
PlayList(
title = "Formula 1 Rolex Australian Grand Prix 2020",
uid = "PLfoNZDHitwjW_JfXMrZUDlp8lwrjPkcWa"
),
PlayList(
title = "F1 2020 Race Highlights",
uid = "PLfoNZDHitwjXRANMnqmL0BzNGianZ2eX_"
),
PlayList(
title = "2020 F1 Season Preview",
uid = "PLfoNZDHitwjWx_vptWVzoyuL8GVGglV0h"
),
PlayList(
title = "Team Guides",
uid = "PLfoNZDHitwjVoXLlvVcfG1MbZn80BOGj6"
),
PlayList(
title = "F1 Testing 2020",
uid = "PLfoNZDHitwjXh_17yB_bI-hHXt3t1lgQb"
),
PlayList(
title = "F1 2020 Car Launches!",
uid = "PLfoNZDHitwjXbQQB7Rh43PX8kVoueMkcR"
),
PlayList(
title = "Best of 2019!",
uid = "PLfoNZDHitwjXID63aL08SfW3TqunE649p"
),
PlayList(
title = "F1 x Complex",
uid = "PLfoNZDHitwjXWM6U-UTowz2gO2m7Oi4oS"
),
PlayList(
title = "Grill The Grid 2019",
uid = "PLfoNZDHitwjUAibL7oIg4TCX1QkWdTcak"
),
PlayList(
title = "F1 Esports 2019",
uid = "PLfoNZDHitwjXw9KWL4nIoTGhogXtztEhe"
),
PlayList(
title = "The Best Of Team Radio 2019!",
uid = "PLfoNZDHitwjU7xYE4FdrweH8aGaT2z7IT"
),
PlayList(
title = "Niki Lauda, 1949-2019",
uid = "PLfoNZDHitwjVeh3xLJtXqup7iTAwdV4e3"
),
PlayList(
title = "FIA Formula 3 Championship",
uid = "PLfoNZDHitwjUzp1jZH8p8PMXpVVC-Aj-K"
),
PlayList(
title = "2019 F1 Pole Positions | Onboard Hot Laps",
uid = "PLfoNZDHitwjUA9aqbPGKw1l4SIz2bACi_"
),
PlayList(
title = "Formula 1 Etihad Airways Abu Dhabi Grand Prix 2019",
uid = "PLfoNZDHitwjUjlVjVvtXTtIhct2_1AhZy"
),
PlayList(
title = "Formula 1 Grande Premio Heineken do Brasil 2019",
uid = "PLfoNZDHitwjVf0YMtgh1es9I4oDORsmS5"
),
PlayList(
title = "Formula 1 United States Grand Prix 2019",
uid = "PLfoNZDHitwjWhH_FA5GArjph8k992Sxrt"
),
PlayList(
title = "Formula 1 Gran Premio de Mexico 2019",
uid = "PLfoNZDHitwjXI1gchbZgf-hlrKXHdXvlw"
),
PlayList(
title = "Formula 1 Japanese Grand Prix 2019",
uid = "PLfoNZDHitwjXlgY0rzRl-UomhV8vz6glb"
)
)
...
São muitas, certo?
Em caso de cliente, faça a mesma coisa:
Não economize, coloque todas as PlayLists já disponíveis em canal.
Assim podemos ir à próxima tela.
Circuitos
Para está tela teremos todo um combo de novas entidades, até mesmo o fragmento será um novo.
Não somente está tela, mas também as telas de Parceiros e Loja terão todo um combo de novas entidades.
Uma outra opção é renomear as entidades que não mais serão utilizadas.
Aqui a nossa abordagem será como se estivéssemos literalmente adicionando novas telas por completo em projeto.
Então vamos aos códigos... começando pelos estáticos.
Estáticos de interface
Rótulos
Em /res/values/strings.xml adicione as seguintes novas Strings:
...
<!-- CircuitsFragment -->
<string name="circuits_content_title">
Circuitos
</string>
<string name="circuits_desc">
Toque no circuito para ter acesso a ele com
realidade de mapa (incluindo Street View).
</string>
<string name="circuits_toast_alert">
É preciso o aplicativo do \"%s\" para ter uma
realidade física do circuito \"%s\".
</string>
<!-- Circuitos -->
<string name="grand_prix_spain_circuit_name">
Circuito de Barcelona (Espanha)
</string>
<string name="grand_prix_spain_circuit_km_length">
Percurso: 4.655 km
</string>
<string name="grand_prix_spain_circuit_laps">
Voltas: 64
</string>
<string name="grand_prix_british_circuit_name">
Circuito de Silverstone (Inglaterra)
</string>
<string name="grand_prix_british_circuit_km_length">
Percurso: 5.891 km
</string>
<string name="grand_prix_british_circuit_laps">
Voltas: 52
</string>
<string name="grand_prix_austria_circuit_name">
Circuito Red Bull Ring (Áustria)
</string>
<string name="grand_prix_austria_circuit_km_length">
Percurso: 4.318 km
</string>
<string name="grand_prix_austria_circuit_laps">
Voltas: 71
</string>
<string name="grand_prix_hungary_circuit_name">
Circuito Hungaroring (Hungria)
</string>
<string name="grand_prix_hungary_circuit_km_length">
Percurso: 4.381 km
</string>
<string name="grand_prix_hungary_circuit_laps">
Voltas: 70
</string>
<string name="grand_prix_belgium_circuit_name">
Circuito Spa-Francorchamps (Bélgica)
</string>
<string name="grand_prix_belgium_circuit_km_length">
Percurso: 7.004 km
</string>
<string name="grand_prix_belgium_circuit_laps">
Voltas: 44
</string>
<string name="grand_prix_italy_circuit_name">
Circuito de Monza (Itália)
</string>
<string name="grand_prix_italy_circuit_km_length">
Percurso: 5.793 km
</string>
<string name="grand_prix_italy_circuit_laps">
Voltas: 53
</string>
<string name="grand_prix_tuscan_circuit_name">
Circuito Internacional de Mugello (Itália)
</string>
<string name="grand_prix_tuscan_circuit_km_length">
Percurso: 5.245 km
</string>
<string name="grand_prix_tuscan_circuit_laps">
Voltas: 59
</string>
<string name="grand_prix_russian_circuit_name">
Circuito de Sochi (Rússia)
</string>
<string name="grand_prix_russian_circuit_km_length">
Percurso: 5.848 km
</string>
<string name="grand_prix_russian_circuit_laps">
Voltas: 53
</string>
<string name="grand_prix_germany_circuit_name">
Circuito de Nurburgring (Alemanha)
</string>
<string name="grand_prix_germany_circuit_km_length">
Percurso: 5.148 km
</string>
<string name="grand_prix_germany_circuit_laps">
Voltas: 60
</string>
<string name="grand_prix_portugal_circuit_name">
Circuito Internacional do Algarve (Portugal)
</string>
<string name="grand_prix_portugal_circuit_km_length">
Percurso: 4.684 km
</string>
<string name="grand_prix_portugal_circuit_laps">
Voltas: 66
</string>
<string name="grand_prix_emilia_romagna_circuit_name">
Circuito Enzo e Dino Ferrari (Itália)
</string>
<string name="grand_prix_emilia_romagna_circuit_km_length">
Percurso: 4.959 km
</string>
<string name="grand_prix_emilia_romagna_circuit_laps">
Voltas: 63
</string>
<string name="grand_prix_turkey_circuit_name">
Circuito Intercity Istanbul Park (Turquia)
</string>
<string name="grand_prix_turkey_circuit_km_length">
Percurso: 5.338 km
</string>
<string name="grand_prix_turkey_circuit_laps">
Voltas: 58
</string>
<string name="grand_prix_bahrain_circuit_name">
Circuito Internacional em Sakhir (Bahrain)
</string>
<string name="grand_prix_bahrain_circuit_km_length">
Percurso: 5.412 km
</string>
<string name="grand_prix_bahrain_circuit_laps">
Voltas: 57
</string>
<string name="grand_prix_bahrain_rolex_circuit_name">
Circuito Internacional em Sakhir - Rolex (Bahrain)
</string>
<string name="grand_prix_bahrain_rolex_circuit_km_length">
Percurso: 3.543 km
</string>
<string name="grand_prix_bahrain_rolex_circuit_laps">
Voltas: 87
</string>
<string name="grand_prix_abu_dhabi_circuit_name">
Circuito Yas Marina (Abu Dhabi)
</string>
<string name="grand_prix_abu_dhabi_circuit_km_length">
Percurso: 5.554 km
</string>
<string name="grand_prix_abu_dhabi_circuit_laps">
Voltas: 55
</string>
...
Todos os circuitos da temporada de 2020 foram colocados em projeto.
Incluindo os circuitos de pré-temporada e os circuitos que não foram utilizados em 2020 devido ao problema de pandemia.
Imagens das pistas
Este é o ponto crítico para está versão de app framework.
Digo crítico, pois não é nada fácil encontrar a imagem de cada circuito em formato SVG. Não até o momento em que este artigo é construído.
Sendo assim a melhor escolha é:
- Baixar as imagens PNG do site oficial;
- Converter cada PNG em sua versão SVG;
- Carregar cada SVG em um editor SVG de otimização. Isso para deixar cada arquivo SVG com poucos bytes, porém sem perder em qualidade;
- Então carregar os SVGs de circuitos em projeto.
Sim, é isso mesmo.
Quando você optar por trabalhar com versões SVG de imagens que não têm suas versões SVG com fácil acesso...
... então muito provavelmente o roteiro de conversão será como na lista anterior.
É isso.
Segue cada ícone dos circuitos da temporada 2020 da Formula 1:
Circuito de Barcelona (Espanha), ic_grand_prix_spain.xml:
Circuito de Silverstone (Inglaterra), ic_grand_prix_british.xml:
Circuito Red Bull Ring (Áustria), ic_grand_prix_austria.xml:
Circuito Hungaroring (Hungria), ic_grand_prix_hungary.xml:
Circuito Spa-Francorchamps (Bélgica), ic_grand_prix_belgium.xml:
Circuito de Monza (Itália), ic_grand_prix_italy.xml:
Circuito Internacional de Mugello (Itália), ic_grand_prix_tuscan.xml:
Circuito de Sochi (Rússia), ic_grand_prix_russian.xml:
Circuito de Nurburgring (Alemanha), ic_grand_prix_germany.xml:
Circuito Internacional do Algarve (Portugal), ic_grand_prix_portugal.xml:
Circuito Enzo e Dino Ferrari (Itália), ic_grand_prix_emilia_romagna.xml:
Circuito Intercity Istanbul Park (Turquia), ic_grand_prix_turkey.xml:
Circuito Internacional em Sakhir (Bahrain), ic_grand_prix_bahrain.xml:
Circuito Internacional em Sakhir - Rolex (Bahrain), ic_grand_prix_bahrain_rolex.xml:
Circuito Yas Marina (Abu Dhabi), ic_grand_prix_abu_dhabi.xml:
Assim, realize o download das imagens vetoriais de cada circuito e coloque-as no folder /res/drawable.
Ícones de itens
Ainda temos dois ícones para carregar:
São os ícones de "Percurso" e de "Voltas":
Ícone de percurso (tamanho da pista), ic_extension.xml:
Ícone de número de voltas em circuito, ic_lap.xml:
Novamente, realize o download dos ícones e coloque-os no folder /res/drawable.
Bloco de informações de circuito
Note novamente no design de item de lista da tela de circuitos que há um conjunto de informações ao lado de cada circuito:
E para está área também temos um arquivo de background específico.
Justamente devido à cor e às bordas arredondadas (com radius aplicado).
Sendo assim, em /res/drawable coloque o novo arquivo XML bg_item_list_info_side.xml com a seguinte definição:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:color="@color/colorListItemBackgroundInfoSide" />
<corners
android:bottomLeftRadius="0dp"
android:radius="8dp" />
</shape>
Enfim a nossa nova propriedade de cor colorListItemBackgroundInfoSide sendo utilizada.
Layout de item
Agora o novo layout para os itens de lista desta tela de circuitos.
Primeiro o diagrama deste novo layout:
Agora, em /res/layout, adicione o layout list_item_big_image.xml com a configuração de código a seguir:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/AppTheme.ListItemContainer"
android:background="@drawable/bg_item_list">
<ImageView
android:id="@+id/iv_icon"
style="@style/AppTheme.ListItemIcon"
android:layout_width="126dp"
android:layout_height="105dp"
android:layout_above="@+id/vv_vertical_line"
android:layout_alignParentStart="true"
android:layout_alignParentTop="false"
android:adjustViewBounds="false"
android:padding="5dp"
android:scaleType="centerInside" />
<View
android:id="@+id/vv_vertical_line"
android:layout_width="match_parent"
android:layout_height="3dp"
android:layout_alignParentBottom="true"
android:layout_toStartOf="@+id/ll_text_container"
android:background="@color/colorListItemBackgroundInfoSide" />
<LinearLayout
android:id="@+id/ll_text_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@+id/iv_icon"
android:background="@drawable/bg_item_list_info_side"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingTop="1dp"
android:paddingEnd="8dp"
android:paddingBottom="1dp">
<TextView
android:id="@+id/tv_main_text"
style="@style/AppTheme.ListItemMainText"
android:layout_marginTop="8dp"
android:layout_marginBottom="14dp"
android:ellipsize="end"
android:maxLines="2"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_first_aux_text"
style="@style/AppTheme.ListItemAuxText"
android:layout_marginBottom="0dp"
android:drawableStart="@drawable/ic_extension"
android:drawablePadding="7dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp"
android:textStyle="normal" />
<TextView
android:id="@+id/tv_second_aux_text"
style="@style/AppTheme.ListItemAuxText"
android:drawableStart="@drawable/ic_lap"
android:drawablePadding="7dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="14sp"
android:textStyle="normal" />
</LinearLayout>
</RelativeLayout>
Somente está tela terá um novo layout de item de lista.
Vamos aos códigos dinâmicos (Kotlin).
Códigos dinâmicos
Atualizando adapter de lista
Ok, Thiengo. Temos um novo layout de item de lista. Sendo assim teremos que ter também novas classes Adapter e ViewHolder?
Não.
Porém será preciso uma pequena atualização na classe ListItemAdapter.
É sério: é uma pequena atualização apenas.
Acesse a classe ListItemAdapter e na assinatura dela adicione a propriedade layout como a seguir:
...
class ListItemAdapter(
val context: Context,
val items: List<ListItem>,
private val callExternalAppCallback: (ListItem)->Unit,
private val layout: Int = R.layout.list_item
) : RecyclerView.Adapter<ListItemViewHolder>() {
...
E por fim, ainda em ListItemAdapter, porém no método onCreateViewHolder(). Atualize a invocação de inflate() para passar a referenciar também a nova propriedade layout.
Exatamente como a seguir:
...
override fun onCreateViewHolder(...) : ListItemViewHolder {
val layout = LayoutInflater
.from( context )
.inflate(
layout,
parent,
false
)
...
}
...
Pronto.
Assim nosso novo fragmento de circuitos também poderá aproveitar toda a configuração do fragmento ancestral FrameworkListFragment.
Classe de domínio
Sim, teremos um nova classe de domínio.
No pacote /model adicione a classe Circuit com o código a seguir:
package formula1.com.youtubechannel.model
import android.content.res.Resources
import android.net.Uri
/**
* Um circuito da temporada da Fórmula 1.
*
* O objetivo desta classe (objetos desta classe)
* é manter os principais dados do circuito para que
* o seguidor tenha mais entretenimento no app.
*
* @property place app que abrirá o circuito em tela.
* @property name nome do circuito.
* @property kmLength tamanho, km, do circuito.
* @property laps número de voltas no circuito.
* @property appUri maps URI API para abrir o mapa
* ou o StreetView.
* @property logo imagem do circuito.
* @constructor cria um objeto completo do tipo
* [Circuit].
*/
class Circuit(
val place: String = "Google Maps",
val name: String,
val kmLength: String,
val laps: String,
private val appUri: String,
private val logo: Int ) : ListItem {
override fun getMainText()
= name
override fun getFirstAuxText()
= kmLength
override fun getSecondAuxText( resource: Resources )
= laps
override fun getAppUri()
= Uri.parse( appUri )
override fun getIcon()
= logo
}
Podemos ir então à persistência estática.
Persistência estática
No pacote /data/fixed crie a classe CircuitsData com os códigos a seguir:
package formula1.com.youtubechannel.data.fixed
import android.content.res.Resources
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.model.Circuit
/**
* Contém os dados dos circuitos da temporada
* da Formula 1.
*
* O objetivo desta classe é trabalhar como uma
* persistência local estática, fixa, que contém
* os dados dos circuitos.
*
* Como esses dados tendem a sofrer poucas
* alterações (incluindo a inserção de novos grupos)
* e com espaços de tempo longos entre as alterações,
* então a melhor escolha foi o trabalho deles em
* uma classe estática (companion object) que
* trabalha como se fosse uma persistência de dados
* estáticos.
*/
abstract class CircuitsData {
companion object{
/**
* Retorna os circuitos Formula 1.
*
* @return lista não mutável de objetos
* [Circuit].
*/
fun getCircuits( res: Resources )
= listOf(
Circuit(
name = res.getString( R.string.grand_prix_spain_circuit_name ),
kmLength = res.getString( R.string.grand_prix_spain_circuit_km_length ),
laps = res.getString( R.string.grand_prix_spain_circuit_laps ),
appUri = "geo:41.568386,2.257148?z=14",
logo = R.drawable.ic_grand_prix_spain
),
Circuit(
name = res.getString( R.string.grand_prix_british_circuit_name ),
kmLength = res.getString( R.string.grand_prix_british_circuit_km_length ),
laps = res.getString( R.string.grand_prix_british_circuit_laps ),
appUri = "google.streetview:panoid=uuyC1meP9ZmbqQd2Ja_pdQ",
logo = R.drawable.ic_grand_prix_british
),
Circuit(
name = res.getString( R.string.grand_prix_austria_circuit_name ),
kmLength = res.getString( R.string.grand_prix_austria_circuit_km_length ),
laps = res.getString( R.string.grand_prix_austria_circuit_laps ),
appUri = "google.streetview:panoid=8iy5DWuMERRI4GLOHzPrAQ",
logo = R.drawable.ic_grand_prix_austria
),
Circuit(
name = res.getString( R.string.grand_prix_hungary_circuit_name ),
kmLength = res.getString( R.string.grand_prix_hungary_circuit_km_length ),
laps = res.getString( R.string.grand_prix_hungary_circuit_laps ),
appUri = "geo:47.582093,19.250562?z=14",
logo = R.drawable.ic_grand_prix_hungary
),
Circuit(
name = res.getString( R.string.grand_prix_belgium_circuit_name ),
kmLength = res.getString( R.string.grand_prix_belgium_circuit_km_length ),
laps = res.getString( R.string.grand_prix_belgium_circuit_laps ),
appUri = "geo:50.437067,5.972030?z=14",
logo = R.drawable.ic_grand_prix_belgium
),
Circuit(
name = res.getString( R.string.grand_prix_italy_circuit_name ),
kmLength = res.getString( R.string.grand_prix_italy_circuit_km_length ),
laps = res.getString( R.string.grand_prix_italy_circuit_laps ),
appUri = "google.streetview:panoid=7XPiTT1Bb-cPqRABXTdToQ",
logo = R.drawable.ic_grand_prix_italy
),
Circuit(
name = res.getString( R.string.grand_prix_tuscan_circuit_name ),
kmLength = res.getString( R.string.grand_prix_tuscan_circuit_km_length ),
laps = res.getString( R.string.grand_prix_tuscan_circuit_laps ),
appUri = "geo:43.996221,11.371082?z=14",
logo = R.drawable.ic_grand_prix_tuscan
),
Circuit(
name = res.getString( R.string.grand_prix_russian_circuit_name ),
kmLength = res.getString( R.string.grand_prix_russian_circuit_km_length ),
laps = res.getString( R.string.grand_prix_russian_circuit_laps ),
appUri = "google.streetview:panoid=wuATMoMcQUaAg2tZtd5l0w",
logo = R.drawable.ic_grand_prix_russian
),
Circuit(
name = res.getString( R.string.grand_prix_germany_circuit_name ),
kmLength = res.getString( R.string.grand_prix_germany_circuit_km_length ),
laps = res.getString( R.string.grand_prix_germany_circuit_laps ),
appUri = "geo:50.334290,6.942610?z=14",
logo = R.drawable.ic_grand_prix_germany
),
Circuit(
name = res.getString( R.string.grand_prix_portugal_circuit_name ),
kmLength = res.getString( R.string.grand_prix_portugal_circuit_km_length ),
laps = res.getString( R.string.grand_prix_portugal_circuit_laps ),
appUri = "geo:37.231632,-8.628392?z=14",
logo = R.drawable.ic_grand_prix_portugal
),
Circuit(
name = res.getString( R.string.grand_prix_emilia_romagna_circuit_name ),
kmLength = res.getString( R.string.grand_prix_emilia_romagna_circuit_km_length ),
laps = res.getString( R.string.grand_prix_emilia_romagna_circuit_laps ),
appUri = "google.streetview:panoid=BQMXpbyPOUavEzjU1f4wbQ",
logo = R.drawable.ic_grand_prix_emilia_romagna
),
Circuit(
name = res.getString( R.string.grand_prix_turkey_circuit_name ),
kmLength = res.getString( R.string.grand_prix_turkey_circuit_km_length ),
laps = res.getString( R.string.grand_prix_turkey_circuit_laps ),
appUri = "geo:40.958092,29.411653?z=14",
logo = R.drawable.ic_grand_prix_turkey
),
Circuit(
name = res.getString( R.string.grand_prix_bahrain_circuit_name ),
kmLength = res.getString( R.string.grand_prix_bahrain_circuit_km_length ),
laps = res.getString( R.string.grand_prix_bahrain_circuit_laps ),
appUri = "geo:26.032197,50.512853?z=14",
logo = R.drawable.ic_grand_prix_bahrain
),
Circuit(
name = res.getString( R.string.grand_prix_bahrain_rolex_circuit_name ),
kmLength = res.getString( R.string.grand_prix_bahrain_rolex_circuit_km_length ),
laps = res.getString( R.string.grand_prix_bahrain_rolex_circuit_laps ),
appUri = "geo:26.032197,50.512853?z=14",
logo = R.drawable.ic_grand_prix_bahrain_rolex
),
Circuit(
name = res.getString( R.string.grand_prix_abu_dhabi_circuit_name ),
kmLength = res.getString( R.string.grand_prix_abu_dhabi_circuit_km_length ),
laps = res.getString( R.string.grand_prix_abu_dhabi_circuit_laps ),
appUri = "geo:24.481397,54.615535?z=14",
logo = R.drawable.ic_grand_prix_abu_dhabi
)
)
}
}
Muito código, certo?
Isso devido à quantidade de circuitos na temporada.
Experiência realista
Com o uso de parte das APIs Android de Mapas de Alta Qualidade:
...
Circuit(
...,
appUri = "google.streetview:panoid=8iy5DWuMERRI4GLOHzPrAQ",
...
)
...
Nós vamos aumentar o valor agregado do app permitindo que o usuário entre dentro do circuito:
Note que foi preciso apenas uma linha de API para cada circuito.
Importante: é possível que você somente consiga simular com sucesso a abertura do Street View em um aparelho Android real. As versões de emulador Android têm certas limitações quanto a isso.
Você vai perceber que alguns circuitos têm o seguinte formato de API URI de acionamento de mapa:
google.streetview:panoid=IDENTIFICADOR_UNICO_PAINEL
Onde o usuário literalmente entra no circuito.
E outros circuitos têm o formato a seguir:
geo:LATITUDE,LONGITUDE?z=ZOOM
Onde o usuário tem na verdade acesso ao mapa do circuito.
Isso ocorre, pois não são todos os circuitos que têm "navegação" (StreetView completo) liberada.
Sendo assim, para este nosso projeto, optamos por deixar com a URI de acesso dentro do circuito somente aqueles que permitem navegação.
CircuitsFragment
Enfim o fragmento de circuitos.
No pacote /ui/fragment adicione o fragmento CircuitsFragment como a seguir:
package formula1.com.youtubechannel.ui.fragment
import android.os.Bundle
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.data.fixed.CircuitsData
import formula1.com.youtubechannel.model.Circuit
import formula1.com.youtubechannel.ui.adapter.ListItemAdapter
/**
* Contém a listagem de circuitos da Formula 1.
*
* @constructor cria um objeto completo do tipo
* [CircuitsFragment].
*/
class CircuitsFragment : FrameworkListFragment() {
companion object {
/**
* Constante com o identificador único do
* fragmento [CircuitsFragment] para que
* ele seja encontrado na pilha de fragmentos
* e assim não seja necessária a construção
* de mais de um objeto deste fragmento em
* memória enquanto o aplicativo estiver em
* execução.
*/
const val KEY = "CircuitsFragment_key"
}
override fun onActivityCreated(
savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
setUiModel(
titleText = getString( R.string.circuits_content_title ),
subTitleText = getString( R.string.circuits_desc )
)
val adapter = ListItemAdapter(
context = activity!!,
items = CircuitsData.getCircuits( resources ),
callExternalAppCallback = {
item -> callExternalApp(
webUri = item.getWebUri(),
failMessage = String.format(
getString( R.string.circuits_toast_alert ),
(item as Circuit).place,
item.name
)
)
},
R.layout.list_item_big_image
)
initList( adapter = adapter )
}
}
Note que na configuração da propriedade adapter, em onActivityCreated(), já estamos referenciando o novo layout de item:
...
val adapter = ListItemAdapter(
...,
R.layout.list_item_big_image
)
...
É isso. Tela de circuito finalizada.
Faltam apenas duas telas agora.
Parceiros
Essa é outra tela que tem um combo de novas entidades entrando em projeto:
Mas o layout de itens de lista será o mesmo já definido em versão inicial de app framework.
Vamos iniciar com a parte estática.
Estáticos de interface
Rótulos
No arquivo /res/values/strings.xml adicione as seguintes novas Strings:
...
<!-- PartnersFragment -->
<string name="partners_content_title">
Nossos parceiros
</string>
<string name="partners_toast_alert">
É preciso ao menos um navegador Web para abrir a
página do parceiro F1 \"%s\".
</string>
<string name="partner_rolex">
Rolex - Os relógios rolex são feitos com atenção
diferente dos detalhes.
</string>
<string name="partner_dhl">
DHL - Entregamos com velocidade!
</string>
<string name="partner_aramco">
Saudi Aramco - Onde a energia é oportunidade.
</string>
<string name="partner_pirelli">
Pirelli - Uma empresa global de pneus de alto
valor.
</string>
<string name="partner_emirates">
Emirates Airlines - Levando você a mais de 80
destinos.
</string>
<string name="partner_heineken">
Heineken - Abra seu mundo.
</string>
<string name="partner_aws">
Amazon Web Services (AWS) - Serviços de computação
em nuvem.
</string>
<string name="partner_expo_2020">
Expo 2020 Dubai Uae
</string>
<string name="partner_amg">
AMG - Desempenho de condução.
</string>
<string name="partner_caterpillar">
CAT - Construir um mundo melhor.
</string>
<string name="partner_188_bet">
188 Bet - 100% de bônus em seu primeiro depósito.
</string>
<string name="partner_citibank">
CitiBank - Simplificar a experiência do cliente,
por meio da tecnologia.
</string>
<string name="partner_petronas">
Petronas - Fornecendo energia ao mundo com energia
mais limpa.
</string>
...
É aquilo que já venho falando sobre em vários projetos aqui do Blog:
Toda String que é passível se ser traduzida em novas versões de app... entra então em strings.xml.
As Strings de marcas entraram em strings.xml devido ao moti de cada uma delas. Esses que podem sim passar por tradução.
Ícones
Para os ícones de marcas, aqui o trabalho foi exatamente o mesmo realizado para as imagens vetoriais de circuitos.
Em resumo:
- Encontrar a logo da marca em formato PNG (com transparência);
- Converter para SVG;
- Adicionar em projeto.
Confesso que desta vez, para nenhuma das imagens de brand, foi necessário o passo de otimização de SVG.
Existem alguns sites que têm, gratuito, os ícones vetoriais de algumas marcas.
Para as marcas parceiras da Formula 1 eu praticamente não encontrei nenhum ícone que fosse aproveitável em projeto.
É isso, vamos aos ícones das empresas parceiras:
Ícone Rolex, ic_logo_rolex.xml:
Ícone DHL, ic_logo_dhl.xml:
Ícone Saudi Aramco, ic_logo_aramco.xml:
Ícone Pirelli, ic_logo_pirelli.xml:
Ícone Emirates Airlines, ic_logo_emirates.xml:
Ícone Heineken, ic_logo_heineken.xml:
Ícone Amazon Web Services (AWS), ic_logo_aws.xml:
Ícone Expo 2020 Dubai Uae, ic_logo_expo_2020_dubai_uae.xml:
Ícone AMG, ic_logo_amg.xml:
Ícone Caterpillar, ic_logo_caterpillar.xml:
Ícone 188 Bet, ic_logo_188_bet.xml:
Ícone CitiBank, ic_logo_citibank.xml:
Ícone Petronas, ic_logo_petronas.xml:
Realize o download dos ícones anteriores e coloque-os no folder /res/drawable.
Códigos dinâmicos
Classe de domínio
Agora, no pacote /model, adicione a nova classe Partner com os fontes a seguir:
package formula1.com.youtubechannel.model
import android.net.Uri
/**
* Empresa parceira da Formula 1.
*
* O objetivo desta classe (objetos desta classe)
* é apenas manter os dados importantes para a
* apresentação da empresa parceira à Formula 1.
*
* @property name nome da empresa / marca.
* @property webPage URL da página Web oficial
* da empresa.
* @property logo identificador único do logo
* da empresa.
* @constructor cria um objeto completo do tipo
* [Partner].
*/
class Partner(
val name: String,
private val webPage: String,
private val logo: Int ) : ListItem {
override fun getMainText()
= name
override fun getWebUri()
= Uri.parse( webPage )
override fun getIcon()
= logo
}
Persistência estática
No pacote /data/fixed crie a classe persistência PartnersData com os códigos fonte a seguir:
package formula1.com.youtubechannel.data.fixed
import android.content.res.Resources
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.model.Partner
/**
* Contém os dados das empresas parceiras da
* Formula 1 na temporada.
*
* O objetivo desta classe é trabalhar como uma
* persistência local estática, fixa, que contém
* os dados das empresas parceiras.
*
* Como esses dados tendem a sofrer poucas
* alterações (incluindo a inserção de novos livros)
* e com espaços de tempo longos entre as
* alterações, então a melhor escolha foi o trabalho
* deles em uma classe estática (companion object)
* que trabalha como se fosse uma persistência de
* dados estáticos.
*/
abstract class PartnersData {
companion object {
/**
* Retorna todos os parceiros da Formula 1.
*
* @return lista não mutável de objetos
* [Partner].
*/
fun getBooks( res: Resources )
= listOf(
Partner(
name = res.getString( R.string.partner_rolex ),
webPage = "https://www.rolex.com/",
logo = R.drawable.ic_logo_rolex
),
Partner(
name = res.getString( R.string.partner_dhl ),
webPage = "https://www.dhl.com/",
logo = R.drawable.ic_logo_dhl
),
Partner(
name = res.getString( R.string.partner_aramco ),
webPage = "https://www.aramco.com/",
logo = R.drawable.ic_logo_aramco
),
Partner(
name = res.getString( R.string.partner_pirelli ),
webPage = "https://www.pirelli.com/",
logo = R.drawable.ic_logo_pirelli
),
Partner(
name = res.getString( R.string.partner_emirates ),
webPage = "https://www.emirates.com/",
logo = R.drawable.ic_logo_emirates
),
Partner(
name = res.getString( R.string.partner_heineken ),
webPage = "https://www.theheinekencompany.com/",
logo = R.drawable.ic_logo_heineken
),
Partner(
name = res.getString( R.string.partner_aws ),
webPage = "https://aws.amazon.com/",
logo = R.drawable.ic_logo_aws
),
Partner(
name = res.getString( R.string.partner_expo_2020 ),
webPage = "https://www.expo2020dubai.com/",
logo = R.drawable.ic_logo_expo_2020_dubai_uae
),
Partner(
name = res.getString( R.string.partner_amg ),
webPage = "https://www.mercedes-amg.com/",
logo = R.drawable.ic_logo_amg
),
Partner(
name = res.getString( R.string.partner_caterpillar ),
webPage = "https://www.caterpillar.com/",
logo = R.drawable.ic_logo_caterpillar
),
Partner(
name = res.getString( R.string.partner_188_bet ),
webPage = "https://www.188bet.com/",
logo = R.drawable.ic_logo_188_bet
),
Partner(
name = res.getString( R.string.partner_citibank ),
webPage = "https://corporateportal.brazil.citibank.com/index.htm",
logo = R.drawable.ic_logo_citibank
),
Partner(
name = res.getString( R.string.partner_petronas ),
webPage = "https://www.petronas.com/",
logo = R.drawable.ic_logo_petronas
)
)
}
}
PartnersFragment
Enfim o fragmento de empresas parceiras.
No pacote /ui/fragment adicione o fragmento PartnersFragment com o seguinte código fonte:
package formula1.com.youtubechannel.ui.fragment
import android.os.Bundle
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.data.fixed.PartnersData
import formula1.com.youtubechannel.model.Partner
import formula1.com.youtubechannel.ui.adapter.ListItemAdapter
/**
* Contém a lista de parceiros da Formula 1.
*
* @constructor cria um objeto completo do tipo
* [PartnersFragment].
*/
class PartnersFragment : FrameworkListFragment() {
companion object {
/**
* Constante com o identificador único do
* fragmento [PartnersFragment] para que
* ele seja encontrado na pilha de fragmentos
* e assim não seja necessária a construção
* de mais de um objeto deste fragmento em
* memória enquanto o aplicativo estiver em
* execução.
*/
const val KEY = "PartnersFragment_key"
}
override fun onActivityCreated(
savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
setUiModel(
titleText = getString( R.string.partners_content_title )
)
val adapter = ListItemAdapter(
context = activity!!,
items = PartnersData.getBooks( resources ),
callExternalAppCallback = {
item -> callExternalApp(
webUri = item.getWebUri(),
failMessage = String.format(
getString( R.string.partners_toast_alert ),
(item as Partner).name
)
)
}
)
initList( adapter = adapter )
}
}
Pronto.
Somente mais uma tela. A de Loja.
Loja
Agora a tela que contém uma única entidade item. A tela de loja:
Lembrando que parte do objetivo do app é sim aumentar os ganhos do canal.
Então não vejo porque não adicionar no projeto o link para acesso à Formula 1 Store.
Estáticos de interface
Rótulos
Em /res/values/strings.xml adicione as propriedades String a seguir:
...
<!-- StoreFragment -->
<string name="store_content_title">
Loja (10% de desconto)
</string>
<string name="store_desc">
Acesse nossa loja agora e tenha até 10% de
desconto na primeira compra.
</string>
<string name="store_toast_alert">
É preciso o aplicativo navegador Web para
poder acessar a loja.
</string>
...
Ícone
Para ícone vetorial de item de lista em Loja, temos:
Ícone de compra (simulando um macacão de corrida, é sério 😁), ic_overall.xml:
Realize o download do ícone e coloque-o no folder /res/drawable do projeto.
Códigos dinâmicos
Classe de domínio
No pacote /model adicione a classe de domínio Store com o código a seguir:
package formula1.com.youtubechannel.model
import android.net.Uri
/**
* Objeto loja da Formula 1.
*
* @property name nome da loja.
* @property webUri URL da loja.
* @property logo ícone que identifica a loja.
* @constructor cria um objeto completo do tipo
* [Store].
*/
class Store(
val name: String,
private val webUri: String,
private val logo: Int ) : ListItem {
override fun getMainText()
= name
override fun getWebUri()
= Uri.parse( webUri )
override fun getIcon()
= logo
}
Dados fixos
No pacote /data/fixed adicione a classe persistência StoreData com o exato código a seguir:
package formula1.com.youtubechannel.data.fixed
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.model.Store
/**
* Contém os dados de lojas da Formula 1.
*
* O objetivo desta classe é trabalhar como uma
* persistência local estática, fixa, que contém
* todos os dados de lojas.
*
* Como esses dados tendem a sofrer poucas
* alterações (incluindo a inserção de novos contatos)
* e com espaços de tempo longos entre as alterações,
* então a melhor escolha foi o trabalho deles em
* uma classe estática (companion object) que
* trabalha como se fosse uma persistência de dados
* estáticos.
*/
abstract class StoreData {
companion object{
/**
* Retorna a loja Formula 1.
*
* @return lista não mutável de objetos
* [Store].
*/
fun getStore()
= listOf(
Store(
name = "F1 Store",
webUri = "https://f1store4.formula1.com/",
logo = R.drawable.ic_overall
)
)
}
}
StoreFragment
Por fim o último fragmento que representa uma tela em app.
No pacote /ui/fragment adicione o fragmento StoreFragment com o código a seguir:
package formula1.com.youtubechannel.ui.fragment
import android.os.Bundle
import formula1.com.youtubechannel.R
import formula1.com.youtubechannel.data.fixed.StoreData
import formula1.com.youtubechannel.ui.adapter.ListItemAdapter
/**
* Contém a listagem de lojas online da Formula 1.
*
* @constructor cria um objeto completo do tipo
* [StoreFragment].
*/
class StoreFragment : FrameworkListFragment() {
companion object {
/**
* Constante com o identificador único do
* fragmento [StoreFragment] para que
* ele seja encontrado na pilha de fragmentos
* e assim não seja necessária a construção
* de mais de um objeto deste fragmento em
* memória enquanto o aplicativo estiver em
* execução.
*/
const val KEY = "StoreFragment_key"
}
override fun onActivityCreated(
savedInstanceState: Bundle? ){
super.onActivityCreated( savedInstanceState )
setUiModel(
titleText = getString( R.string.store_content_title ),
subTitleText = getString( R.string.store_desc )
)
val adapter = ListItemAdapter(
context = activity!!,
items = StoreData.getStore(),
callExternalAppCallback = {
item -> callExternalApp(
webUri = item.getWebUri(),
appUri = item.getAppUri(),
failMessage = String.format(
getString( R.string.store_toast_alert )
)
)
}
)
initList( adapter = adapter )
}
}
Agora é preciso atualizar alguns trechos de código da atividade principal.
Atividade principal
Com a adição (e possíveis remoções) de alguns fragmentos, ainda é preciso atualizar dois métodos da atividade principal.
São eles:
- getFragment();
- getFragmentInKey().
Sendo assim, na MainActivity atualize o método getFragment() como a seguir:
...
private fun getFragment(
itemId: Int = R.id.last_video ) : Fragment {
val key = getFragmentInKey( itemId = itemId )
var fragment = supportFragmentManager
.findFragmentByTag( key )
if( fragment == null ){
fragment = when( itemId ){
R.id.social_networks -> SocialNetworksFragment()
R.id.play_lists -> PlayListsFragment()
R.id.circuits -> CircuitsFragment()
R.id.partners -> PartnersFragment()
R.id.store -> StoreFragment()
else -> LastVideoFragment()
}
}
return fragment
}
...
Logo depois o método getFragmentInKey() como a seguir:
...
private fun getFragmentInKey(
itemId: Int = R.id.last_video )
= when( itemId ){
R.id.social_networks -> SocialNetworksFragment.KEY
R.id.play_lists -> PlayListsFragment.KEY
R.id.circuits -> CircuitsFragment.KEY
R.id.partners -> PartnersFragment.KEY
R.id.store -> StoreFragment.KEY
else -> LastVideoFragment.KEY
}
...
É isso.
Vamos às notificações.
Atualizações para notificações push
No caso das notificações nós iremos mudar poucas coisas.
Rótulo
Em /res/values/strings.xml atualize a propriedade String a seguir:
...
<string name="notification_verbose_description">
Notificação de novos vídeos do canal YouTube
Formula 1.
</string>
...
Ícone
O ícone de notificação push será:
Ícone logo da Formula 1, ic_f1_blog_color.xml:
É exatamente o mesmo ícone utilizado no item de lista Blog da tela de Redes do canal.
Caso você ainda não tenha feito, então realize o download do ícone e coloque-o no folder /res/drawable.
Banco de dados
Caso você já tenha testado em seu ambiente de desenvolvimento o app framework.
Então para testar a versão do aplicativo que você fez para algum cliente YouTuber é prudente atualizar a versão de banco de dados.
Atualize na classe ChannelDatabase, no pacote /data/dynamic, a propriedade version como a seguir:
...
@Database(
...,
version = 21
)
...
Acrescente +1 ao valor atual de version. Era 20 aqui, assim colocamos 21.
Resultado
Executando a nova versão de nosso app framework e passando por todas as telas, temos:
Tela de abertura de aplicativo. | Tela de Último vídeo liberado em canal. |
Carregando novo último vídeo (swipe). | Tela de PlayLists canal. |
Carregando PlayLists (swipe). | Carregando PlayLists (abertura de tela). |
Nenhuma PlayList disponível. | Tela das redes e site do canal. |
Tela de circuitos Formula 1. | Item de circuito acionado. |
Tela de parceiros. | Tela de loja. |
Notificação de novo vídeo. | Notificação em bandeja de notificações. |
Notificação expandida. |
|
Minha recomendação final para você que já está ao menos na terceira versão, projeto, de nosso app Android framework (está é a terceira):
Remova do projeto todas as entidades que não mais estão sendo utilizadas.
Por exemplo:
Os fragmentos CoursesFragment e BooksFragment estão na lista de entidades que podem com segurança serem removidas de projeto.
Sendo assim remova também esses fragmentos e todas as entidades vinculadas a eles.
GitHub do projeto
Como com o projeto app framework, o aplicativo de exemplo que utilizamos aqui do canal Formula 1 também está disponível em repositório público:
➙ GitHub do projeto de aplicativo Android do canal YouTube Formula 1.
Conclusão
Como informado e apresentado em projeto:
Com uma simples linha de código utilizando a API de Mapas de Alta Qualidade é possível, mesmo em um aplicativo simples...
... é possível oferecer ao usuário uma experiência melhor e mais tangível.
Em nosso caso colocamos ele dentro de circuitos da temporada 2020 da Formula 1.
Esse app framework que desenvolvemos tem na verdade infinitas possibilidades.
Uma que provavelmente será um pedido a você caso você resolva monetizar o projeto, será:
Permitir que novas postagens no Instagram, Pinterest, ... também notifiquem o usuário em app.
Aliás esse é um possível conteúdo que provavelmente estarei fazendo também sobre este nosso aplicativo framework.
Então é isso.
Surgindo dúvidas sobre o projeto deste artigo ou sobre o app framework em geral. Pode deixar na área de comentários abaixo que logo eu lhe respondo.
Por fim, não esqueça de conhecer também o meu canal no YouTube (caso você ainda não o conheça) e...
... não deixe de se inscrever na 📩 lista de e-mails para também garantir a versão em PDF não somente deste projeto de aplicativo Android, mas também de cada novo conteúdo.
Abraço.
Fontes
Lista de autódromos de Fórmula 1
Scale svg images in ImageView - Resposta de Pavan
long SVG Asset file error : R is not a valid verb. Failure occurred at position 2 of path: STRING_TOO_LARGE - Resposta de TheBigBadBoy
How to record screen with Android Studio - Resposta de Vaibhav Yadav e Glenn
Comentários Facebook