Criando um design moderno no Delphi

Há um tempo, um amigo me mostrou um design que achou interessante e me desafiou a reproduzi-lo no Delphi.

Aceitei o desafio e ele virou essas 5 vídeo aulas que estão no meu canal no YouTube.

Nessas aulas mostro como:

Conciliar a Programação Orientada a Objetos com a metodologia RAD;

Criar uma lista com cards de tamanhos diferentes;

Usar animações para melhorar experiência do usuário.

Link para o código fonte do que foi apresentado nos vídeos.

https://github.com/ImperiumDelphi/AppLojaYouTube

Não se esqueçam de seguir o canal no YouTube.

https://www.youtube.com/channel/UCQI9BbVeXWp3Iel54abU8Ag

Até a próxima!

Loading Placeholder

Também chamado de Skeleton loading ou Shimmer loading, é uma animação mais amigável que os famosos “Spinners”, pois mostra uma prévia da formatação dos dados que serão exibidos ao final do loading.

No post de hoje vamos ver como implementar esse efeito em nossos apps desenvolvidos em Delphi.

À primeira vista pode parecer algo complexo, mas se prestarmos atenção no efeito, nada mais é que um gradiente linear se movimentando pelo controle.

Todos os controles herdados de TShape possuem a propriedade Fill que controla como o fundo do controle será exibido, e a propriedade Stroke que define o contorno. Para todas as duas podemos definir o preenchimento como gradiente, ou seja, podemos aplicar o efeito apenas no fundo, na borda ou em ambos.

Definições de Fill e Stroke para o Retângulo que será animado.

Como vimos na imagem acima, basta definir o gradiente com 3 pontos, deixar o ponto central com a cor em destaque, e aproximar os pontos laterais do ponto central.

Agora basta usar um TFloatAnimation para alterar o Offset de cada ponto e criar o efeito de movimentação.

Definições da animação e propriedade criada no form.

Após colocar o componente TFloatAnimation no Form, definimos as propriedades como na figura acima. Para evitar de ter que usar 1 componente de animação para cada ponto do gradiente, criei uma propriedade no Form para definir o offset dos 3 pontos em uma única animação.

App em execução.

Ao final do loading basta parar a animação, alterar o parent ou a propriedade visible do retângulo, assim a área com os dados passa a ser exibida.

Está disponível no Youtube um tutorial mais completo.

Download dos fontes:

https://github.com/ImperiumDelphi/PlaceHolder

Até a próxima!

Compartilhando várias imagens ou arquivos com o Whatsapp ou outro app.

11/02/2021

Durante o desenvolvimento do aplicativo https://play.google.com/store/apps/details?id=com.imperium.JusImperium, esbarrei na necessidade de compartilhar várias imagens de uma só vez com os aplicativos de mensagens (Whatsapp, Messenger ou com o FaceBook), depois de muito pesquisar, a solução que encontrei foi apelar para os Intents do Android. Uma fonte de informação que me ajudou muito foi o blog do MVP Landerson Gomes.

O primeiro passo para se compartilhar as imagens é pedir a permissão READ/WRITE_EXTERNAL_STORAGE e gravar as imagens em uma pasta compartilhada. Costumo usar System.IOUtils.TPath.GetSharedDocumentsPath. Utilizando esse caminho não há necessidade de utilizar o FileProvider para se obter acesso às imagens. No caso do fonte abaixo, eu já possuo 3 bitmaps armazenados em Image1, Image2 e Image3. Faço a gravação das imagens com os nomes 1.png, 2.png e 3.png.

Após as imagens gravadas, crio um JArrayList para carregar as URIs de todas as imagens, crio o Intent definindo a ação para ACTION_SEND.

Para que o intent abra diretamente o WhatsApp, o Package é definido para “com.whastapp’. Caso queira que o usuário escolha para qual aplicativo as imagens devem ser enviadas, retire o SetPackage do código, ou substitua de acordo com sua necessidade.

Pode-se junto com as imagens enviar um texto, para isso basta definir o type para text/plain e incluir um EXTRA_TEXT com o texto desejado. O próximo passo é criar os URIs, definir seu tipo e inclui-los no array. Agora basta incluir o array como EXTRA_STREAM através do método putParcelableArrayListExtra, setar o flag de READ_URI_PERMISSION e enviar o Intent.

procedure TForm1.Button1Click(Sender: TObject);
Var
   Intent : JIntent;
   Uri    : Jnet_Uri;
   Uris   : JArrayList;
   Path   : String;
begin
Path := System.IOUtils.TPath.GetSharedDocumentsPath+PathDelim;
Image1.Bitmap.SaveToFile(Path+'1.png');
Image2.Bitmap.SaveToFile(Path+'2.png');
Image3.Bitmap.SaveToFile(Path+'3.png');
Uris   := TJArrayList.Create;
Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_SEND);
Intent.setPackage(StringToJString('com.whatsapp'));
Intent.setType(StringToJString('text/palin'));
Intent.putExtra(TJIntent.JavaClass.EXTRA_TEXT, StringToJString(Texto.Text));

Uri := TJNet_Uri.JavaClass.parse(StringToJString(Path+'1.png')); 
Uris.add(Uri); 
Intent.setDataAndType(Uri, StringToJString('image/png'));

Uri := TJNet_Uri.JavaClass.parse(StringToJString(Path+'2.png')); 
Uris.add(Uri); 
Intent.setDataAndType(Uri, StringToJString('image/png'));

Uri := TJNet_Uri.JavaClass.parse(StringToJString(Path+'3.png')); 
Uris.add(Uri); 
Intent.setDataAndType(Uri, StringToJString('image/png'));

Intent.putParcelableArrayListExtra(TJIntent.JavaClass.EXTRA_STREAM, Uris);
Intent.setFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION);
TAndroidHelper.Activity.startActivity(Intent);
end;

O WhatsApp será aberto com sua lista de contatos, basta escolher para qual contato quer enviar.

Uma fonte de informação muito valiosa é o tutorial escrito pelo Landerson Gomes sobre o uso dos intents com o Delphi.

Até a próxima!

Formatando campos numéricos no FireMonkey

Para deixar mais completa a informação passada no vídeo acima, vamos implementar uma rotina de formação onde poderemos variar a máscara, aumentando as possibilidades de formatação.

Notem que a rotina mostrada no vídeo serve para a formatação de um campo de valor, enquanto apresentada nesse artigo vai servir para campos tipo telefone, CPF, CNPJ, etc.

Vamos incluir a function abaixo, ela fará a formatação conforme a máscara que for informada:

function TTestEdit.ApplyMask(aMask, aValue: String): String;
Var
   M, V  : Integer;
   Texto : String;
begin
Result := '';
Texto  := '';
aMask  := aMask.ToUpper;
for V := 0 to aValue.Length-1 do
   if aValue.Chars[V] In ['0'..'9'] Then
      Texto := Texto + aValue.Chars[V];
M := 0;
V := 0;
while (V < Texto.Length) And (M < aMask.Length) do
   Begin
   While aMask.Chars[M] <> '#' Do
      Begin
      Result := Result + aMask.Chars[M];
      Inc(M);
      End;
   Result := Result + Texto.Chars[V];
   Inc(M);
   Inc(V);
   End;
end;

para formatar um número de telefone, substituímos o evento OnTyping pelo a seguir:

procedure TTestEdit.Edit1Typing(Sender: TObject);
begin
TThread.Queue(Nil,
   Procedure
   begin
   Edit1.Text := ApplyMask('(##) #.####-####', Edit1.Text);
   Edit1.CaretPosition := Edit1.Text.Length;
   End);
end;

Um formulário com 4 TEdits, cada um chamando seu evento OnTyping aplicando as máscaras ‘(##) #.####-####’, ‘###.###.###-##’, “###.###.###/####-##’ e ‘#-(#)-(#)-#’ se comportará como no vídeo abaixo:

Em um próximo artigo vamos pegar nosso aprendizado de hoje e criar o componente TMaskEdit, colocando-o na paleta de componentes do Delphi.

Até a próxima!

Base64

Apesar do byte sempre ser formado por 8 bits, nos primórdios da informática, usava-se apenas 7 bits para os caracteres que eram impressos e posteriormente mostrados em tela.

Além de só haver 128 combinações possíveis, os 32 primeiros caracteres foram usados como controle. Como os conhecidos CR e LF, que nos remetem à antiga máquina de escrever, onde ao terminar uma linha você retornava o “carro” (Carriage Return) e fazia o papel subir, abrindo espaço para a próxima linha (Line Feed).

Nesse cenário, como transportar informações em que todos os bits são necessários?

Alguém teve a brilhante ideia de pegar o stream de dados e dividi-lo em blocos de 6 bits, daí o nome Base64 (2^6 = 64). Mas continuou tendo o problema dos primeiros 32 caracteres não serem exibidos, pois teríamos blocos com valores de 0..63. A coisa mais óbvia a se fazer seria por exemplo somar $30 (Caracter “0”) aos valores, mas nesse caso entrariam alguns símbolos na sequência, como “:”, “;” e “<” que vem logo após o “9” na tabela ASCII. Acharam isso ruim e decidiram fazer uma tabela própria, onde só entrariam as letras maiúsculas, minúsculas e os números, mas ficou faltando 2 caracteres para completar os 64, assim, tiveram que “superar o TOC” e incluir “+” e “/” na tabela.

Nos dias atuais ainda existe a necessidade de transportar dados onde a base64 é a forma ideal.

Mas por que não enviar e receber tudo em binário? Bom, acho que todos nós já ouvimos falar em XML e Json, certo? Imaginem como ficaria um binário dentro deles. (De início podia parecer frescura adotarem uma tabela onde só letras, números, “+” e “/” são usados, mas não podemos negar que é muito melhor trabalhar com XML sabendo que os caracteres “<” e “>” não estarão nos dados.)

Como base64 trabalha com 6 bits e nós trabalhamos com 8 bits, vemos que a cada 3 caracteres, teremos 4 em base 64. (Ora pois, 3*8 = 4*6).

Vamos a um exemplo:

Faremos com a sequencia “Del”, de “Delphi”.

D = 01000100, e = 01100101, l = 01101100  
010001000110010101101100

Dividindo de 6 em 6 bits
010001 000110 010101 101100
17 6 21 44

Os valores achados são os índices dos caracteres na tabela:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Portanto, "Del" = "RGVs"

Vamos aos códigos?

Primeiramente, definimos a tabela dos caracteres usados na base64, uma tabela com os valores das posições de cada bloco de 6 bits na sequencia de 24 bits e um tipo de 32 bits, onde temos acesso ao seu valor byte a byte, esse tipo será usado na decodificação para simplificar o processo.

Const
TabBase64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
Tab64 = array[0..3] of Int32 = (262144, 4096, 64, 1);
type
T32bits = Record
case Byte of
1 : (Value : Int32);
2 : (V : Array[0..3] Of Byte);
End;

Agora a rotina para codificar.

Function CodificaBase64(Texto: String) : String;
Var
   X, L : Integer;
   B, C : Byte;
begin
Result := '';
X      := -1;
L      := Texto.Length;
while Texto.Length Mod 3 <> 0 do Texto := Texto + #0;
while X < Texto.Length-1 do
   Begin
   Inc(x);
   B := Ord(Texto.Chars[x]);
   C := B Shl 6;
   B := B Shr 2;
   Result := Result + TabBase64.Chars[B];
   Inc(x);
   B := Ord(Texto.Chars[x]);
   C := ((B Shr 2) Or C) Shr 2;
   Result := Result + TabBase64.Chars[C];
   C := B Shl 4;
   Inc(x);
   B := Ord(Texto.Chars[x]);
   C := ((B Shr 4) Or C) Shr 2;
   Result := Result + TabBase64.Chars[C];
   C := B And $3F;
   Result := Result + TabBase64.Chars[C];
   End;
Result := Result.Remove(Result.Length - L) + String.Create('=',  Result.Length -  L);
end;

Não creio estar muito complicado de entender, primeiro fazemos com que o texto fique com um tamanho múltiplo de 3, depois fazemos um loop para trabalhar com todos os blocos de 3 caracteres, gerando assim a sequência em 4 blocos de 6 bits.
Notem que inicialmente guardamos em L o comprimento de Texto antes de completar com #0s. Ao final, removemos do resultado a diferença de tamanho do texto, completamos com “=” que significa que o texto terminou antes de completar o último bloco de 3 bytes. Afinal, precisamos que a decodificação nos retorne um texto exatamente igual ao que codificamos, os #0 no final seriam retornados na decodificação.
Para quem não está familiarizado com os operadores Shl e Shr, eles respectivamente, rotacionam para a esquerda e para a direita n bits.

Decodificando…

function DecodificaBase64(Texto: String): String;
Var
V : T32Bits;
X, I : Integer;
Begin
Result := '';
I := -1;
x := 0;
V.Value := 0;
while (X < Texto.Length) And (Texto.Chars[x] <> '=') Do
Begin
Inc(I);
V.Value := V.Value + TabBase64.IndexOf(Texto.Chars[x]) * Tab64[I];
if I = 3 then
Begin
I := -1;
Result := Result + Chr(V.V[2]) + Chr(V.V[1]) + Chr(V.V[0]);
V.Value := 0;
End;
Inc(x);
End;
x := 2;
while I > 0 do
Begin
Result := Result + Chr(V.V[X]);
Dec(x);
Dec(I);
End;
End;

Fazemos um loop percorrendo os caracteres do texto até achar o “=” ou até o final. Durante o loop, ao completar um bloco de 4 caracteres, Somamos os bytes do bloco em Value, convertendo a base64 da mesma forma que fazemos para converter um binário, só que no lugar de 2(Base2) usamos 64(base64), concatenamos os dados convertidos em result.
( 64^3 = 262144, 64^2 = 4096, 64^1 = 64, 64^0 = 1)
Usamos o IndexOf para pegar o índice do caractere dentro da tabela e multiplicamos pelo valor de sua posição dentro da sequencia de blocos de 6 bits, armazenada na tabela Tab64 tendo como índice I.
Aqui entra o facilitador do tipo que definimos no início.
Ele parte o inteiro de 32 bits em 4 inteiros de 8 bits, só precisamos pegar os 3 bytes menos significativos, afinal estamos trabalhando com 24 bits, não 32.

Apesar do Delphi já possuir essas rotinas prontas, é sempre bom entender o funcionamento das coisas, afinal, profissional de TI que se preze tem que saber arrumar o ar condicionado e principalmente a máquina de café.

Chamando a calculadora do Android de dentro de sua aplicação.

Neste post vamos ver como chamar não só a calculadora, mas qualquer outra aplicação, basta ter no nome do package.

Como diz meu amigo LandersonGomes, vamos usar o “coração do Android”. Os intents.

O Delphi tem a interface completa para o trabalho, basta adicionar no Uses de sua unit:


  {$IFDEF ANDROID}
  Androidapi.Jni,
  Androidapi.JNI.GraphicsContentViewText,
  Androidapi.JNI.provider,
  Androidapi.JNI.JavaTypes,
  Androidapi.JNI.Net,
  Androidapi.JNI.App,
  AndroidAPI.jNI.OS,
  Androidapi.JNIBridge,
  FMX.Helpers.Android,
  Androidapi.Helpers,
  FMX.Platform.Android,
  {$ENDIF ANDROID}

Agora, colocando em prática:

Procedure TForm1.CallAndroidCalculator;
Var
   Intent : JIntent;
   Pack   : JComponentName;
begin
Pack   := TJComponentName.JavaClass.init(StringToJString('com.android.calculator2'), StringToJString('com.android.calculator2.Calculator'));
Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_MAIN);
Intent.addCategory(TJIntent.JavaClass.CATEGORY_LAUNCHER);
Intent.setComponent(Pack);
TAndroidHelper.Activity.StartActivity(Intent);
End;

Simples não é? Em Java seria assim:

Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName("com.android.calculator2",
"com.android.calculator2.Calculator"));
startActivity(intent);

Nesse link você encontra a lista completa de intents para os aplicativos do Google.

Uma fonte de informação muito valiosa é o tutorial escrito pelo Landerson Gomes sobre o uso dos intents com o Delphi.

https://www.landersongomes.com.br/embarcadero/delphi/intents-com-delphi-xe5-comunicando-apps-atraves-do-android?fbclid=IwAR13nkmOQayJaA81p1edBgCwgO02vrQnLhMePhbYg3pj9w5_AjXzfEHmES4

Espero que tenha sido útil. Até a próxima.

O tal do FileProvider do Android.

Hoje me deparei com a parada total dos compartilhamentos de fotos e documentos do meu app, tudo me retornava o erro “android.os.fileuriexposedexception”. Tudo funcionava normalmente até a instalação do update 1 do Delphi Rio.

Pesquisando o erro no grande oráculo, descobri um tal de FileProvider, solução da Google para evitar possíveis erros ao compartilhar arquivos de um aplicativo para outro. Caso o aplicativo que receba os arquivos não possua a permissão de “read external storage”, poderá gerar um erro se o arquivo compartilhado estiver em um armazenamento externo.

Abaixo, um passo a passo da solução que encontrei.

Primeiro passo:

Alterar o template do manifesto do seu app, adicione essas linhas no manifesto logo apos a tag Application.

   <provider
       android:name="android.support.v4.content.FileProvider"
       android:authorities="com.xxx.yyy.fileprovider"
       android:exported="false"
       android:grantUriPermissions="true">
       <meta-data 
           android:name="android.support.FILE_PROVIDER_PATHS"
           android:resource="@xml/provider_paths"/>
       </provider >

Não faça como meu colega de turma do 2º ano do técnico, que ao copiar minha prova de COBOL, copiou junto meu nome em “Author”. Substituam o com.xxx.yyy pelo package do seu app.

Crie um xml com o seguinte conteúdo:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
   <external-path name="external_files" path="."/>
</paths>

Salve-o com o nome de provider_paths.xml. Vá até Projects -> Deployment e inclua esse xml para ser salvo em res\xml\

Segundo passo:

Baixe a biblioteca KastriFree. https://github.com/DelphiWorlds/KastriFree

Use essas duas units:

DW.Android.Helpers.pas
DW.Androidapi.JNI.FileProvider.pas

Coloque-as na uses da sua unit, use-as para definir as Uris.

procedure TDocumentos.OpenFile(FileName, FileType: String);
Var
   Intent : JIntent;
   Uri    : Jnet_Uri;
begin
Uri    := TAndroidHelperEx.UriFromFileName(FileName);
Intent := TJIntent.JavaClass.init(TJIntent.JavaClass.ACTION_VIEW);
Intent.setFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(URI, StringToJString(FileType));
SharedActivity.startActivity(intent);
End;

O que mudou foi “Uri := TAndroidHelperEx.UriFromFileName(FileName);” e a inclusão do flag “Intent.setFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION);”

Nesse exemplo usei a rotina para visualizar imagens, pdfs, etc.. (ACTION_VIEW), é a mesma coisa para compartilhamentos (ACTION_SEND/ ACTION_SEND_MULTIPLE).

No meu caso eu sempre copiei a foto, documento ou zip para a pasta definida em System.IOUtils.TPath.GetTempPath e a compartilhei de lá. Continuei usando dessa mesma forma, deixo para você testar se o compartilhamento funciona de todas as pastas.

Contornando o problema do Push na API 26

Muitos tem sofrido com a mensagem “O aplicativo parou.” ao enviar um push para um aparelho Android 8 ou superior ustilizando a API 26, quando nosso app não está sendo executado no aparelho.

Isso é um problema do Delphi que espero ser corrigido no Update 1 da versão 10.3. Mas enquanto essa correção não sai, podemos usar um artifício do Firebase Messaging.

A solução? Algo bem simples, mas que pode gerar um código adicional em nosso app. Apenas defina a prioridade da mensagem como alta.

Abaixo um exemplo de envio onde o erro acontece:

Após fazer a alteração mostrada na imagem abaixo, o erro parar de ocorrer.

Se seu uso para o Push é apenas mostrar a notificação, problema resolvido. Mas no meu caso como podem ver, envio algumas chaves no Json que preciso que meu app as leia. Mas como ler o Json, se após clickar na notificação e abrir o app, os eventos onReceiveNotification e
onReceiveLocalNotification não são chamados?

Nesse caso vamos ter que capturar o intent que chamou nosso app e extrair de lá a informação que precisamos. O código abaixo resolve esse problema, basta usa-lo para pegar o intent no evento onCreate do form principal de sua app.

Function GetJsonNotification retorna o Json contido no intent que chamou o app
(no caso de uma notificação GCM)

Outra forma de resolver o problema é escrevendo e/ou reescrevendo partes do código Java do FireMonkey, mas para cada versão do Delphi o trabalho terá que ser refeito, a forma descrita nesse post, funciona em qualquer versão alterando apenas o código do seu app.