Olá novamente, recruta!
Hoje vamos falar sobre notificações Toast no Android utilizando Delphi
FireMonkey .
Rapidamente para quem não está familiarizado com o assunto, Toast
Notidication se refere a pequenas mensagens pop-up que aparecem na tela do
mobile quando efetuamos algumas operações em certos Apps, como enviar um
e-mail ou o concluir um download.
Essas notificações permanecem por um tempo na tela e depois somem, elas não
impedem ou bloqueiam a execução da tela atual, ou seja, não são janelas
modais, inclusive, mesmo que nossa aplicação seja finalizada elas
permanecerão na tela pelo tempo que foi configurado.
Existem algumas formas de emular um Toast Notification, algumas bem
difundidas na internet, o que de certa forma é bem interessante, ao ponto
que não ficamos preso a uma plataforma específica, mas não é tão eficiente quanto usar o Toast nativo da plataforma. A forma que
nós iremos utilizar será com a própria API do Toast Notification do
Android, consequentemente você não poderá utilizar no Windows nem no
IOS.
Antes de tudo um pouquinho de história.
interface type TToastLength = (LongToast, ShortToast); procedure Toast(const Msg: string; Duration: TToastLength = ShortToast); Implementation procedure Toast(const Msg: string; Duration: TToastLength); var ToastLength: Integer; begin if Duration = ShortToast then ToastLength := TJToast.JavaClass.LENGTH_SHORT else ToastLength := TJToast.JavaClass.LENGTH_LONG; CallInUiThread(procedure begin TJToast.JavaClass.makeText(SharedActivityContext, StrToJCharSequence(msg), ToastLength).show; end); end;
Como podemos observar, para chamar um Toast Notification é bastante
simples, basta instanciarmos um JToast utilizando o método makeText passando
os devidos parâmetros e depois executar o método show para que a mensagem
seja exibida na tela.
O Método MakeText exige 3 parâmetros:
- Context : A grosso modo seria algo como o handle da sua aplicação ou do
formulário principal no Windows, o Self.Handle, no firemonkey, para o
Android, podemos usar o TAndroidHelper.Context disponível na Unit
Androidapi.Helpers;
- Text: O texto que será exibido no Toast em formato JCharSequence. Para
converter sua String utilize o método StrToJCharSequence também na Unit
Androidapi.Helpers;
- Duration: O tempo de exibição do Toast. Esse parâmetro mesmo sendo um
inteiro só aceita dois valores, 0 para período curto ou 1 para período
longo. Para não haver surpresas futuras, principalmente devido as milhares
de customizações de Androids por aí, recomenda-se usar a
propriedade TJToast.JavaClass.LENGTH_SHORT para períodos curtos e
TJToast.JavaClass.LENGTH_LONG para períodos longos.
O MakeText deve ser executado sempre na thread principal, então para garantir isso efetuamos a chamada através de um método anônimo utilizando o método CallInUiThread;
De uma forma bastante simples, bastaríamos fazer algo como:
CallInUiThread( procedure begin TJToast.JavaClass.makeText( TAndroidHelper.Context, StrToJCharSequence('Teste de Toast Notification'), TJToast.JavaClass.LENGTH_SHORT).show end);
Exemplo de chamada de Toast Notification |
Agora que sabemos como chamar um Toast Notification vamos construir uma classe para tornar nosso processo mais reutilizável.
Em nossa classe iremos declarar um método de classe chamado Show, apenas
com dois parâmetros, a mensagem a ser exibida como string e a duração usando
nosso tipo enumerado. Esse método será usado para invocar nosso Toast
Notification. Optamos por utilizar um método de classe, assim não
precisaremos instanciar um objeto para efetuar a chamada do método.
unit uToastNotification; interface uses Androidapi.JNI.Widget; type TToastLength = (ShortToast, LongToast); TToastNotification = class public class procedure Show(pMsg: string; pDuration: TToastLength = ShortToast); end; implementation uses FMX.Helpers.Android, Androidapi.Helpers, Androidapi.JNI.JavaTypes; { TToastNotification } class procedure TToastNotification.Show(pMsg: string; pDuration: TToastLength); var lToastLength: Integer; begin if pDuration = ShortToast then lToastLength := TJToast.JavaClass.LENGTH_SHORT else lToastLength := TJToast.JavaClass.LENGTH_LONG; CallInUiThread(procedure begin TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(pMsg), lToastLength).show; end); end;
Para usar nosso código basta fazer a seguinte chamada:
TToastNotification.Show('Teste de Toast Notification');
Isso já irá exibir nossa janela com um período curto,
para um período logo basta adicionar o parâmetro
LongToast
TToastNotification.Show('Teste de Toast Notification', LongToast);
Pausa para uma refatoração!
Antes de adicionarmos mais funcionalidades ao nosso Toast, como a seleção
de cores por exemplo, vamos fazer uma pequena refatoração.
Vou adicionar um Record Helper ao nosso tipo enumerado TToastLength para facilitar a conversão para o inteiro utilizado no método
TJToast.JavaClass.makeText.
TToastLengthHelper = record helper for TToastLength function ToAndroidLength: integer; end; { TToastLengthHelper } function TToastLengthHelper.ToAndroidLength: integer; begin case self of ShortToast: result := TJToast.JavaClass.LENGTH_SHORT; LongToast: result := TJToast.JavaClass.LENGTH_LONG; end; end;
Agora podemos remover aquele "if" inicial do nosso método show
class procedure TToastNotification.Show(pMsg: string; pDuration: TToastLength); begin CallInUiThread(procedure begin TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(pMsg), pDuration.ToAndroidLength).show; end); end;
Muito melhor de bom, não acham?!
Além de apenas exibir um texto dentro do Toast padrão do android, podemos fazer uma série de outras alterações, como a cor do Toast, fonte, tamanho, posicionamento, inclusive substituir o Toast por outro objeto.
Se olharmos a Interface JToast que está na unit Androidapi.JNI.Widget.pas,
podemos observar alguns métodos, uns bem óbvios como SetText, e SetDuration
que passamos diretamente pelos parâmetros do método
makeText, mas que podem ser modificados através do acesso de uma variável do tipo
JToast.
[JavaSignature('android/widget/Toast')] JToast = interface(JObject) ['{410DDA5F-7D4B-415E-8BE4-F545D331176C}'] procedure cancel; cdecl; function getDuration: Integer; cdecl; function getGravity: Integer; cdecl; function getHorizontalMargin: Single; cdecl; function getVerticalMargin: Single; cdecl; function getView: JView; cdecl; function getXOffset: Integer; cdecl; function getYOffset: Integer; cdecl; procedure setDuration(duration: Integer); cdecl; procedure setGravity(gravity: Integer; xOffset: Integer; yOffset: Integer); cdecl; procedure setMargin(horizontalMargin: Single; verticalMargin: Single); cdecl; procedure setText(resId: Integer); cdecl; overload; procedure setText(s: JCharSequence); cdecl; overload; procedure setView(view: JView); cdecl; procedure show; cdecl; end; TJToast = class(TJavaGenericImport<JToastClass, JToast>) end;
GetView
O método GetView retorna a Interface para acessarmos o Objeto que é exibido pelo Toast.
Através do GetView podemos alterar diversas características do Toast, como
cor, tamanho e posicionamento.
Continuando a implementação da nossa classe para manipulação do Toast vamos declarar as suas propriedades conforme formos aprendendo a manipular mais recursos.
TToastNotification = class private FText: string; FDuration: TToastLength; FColor: TAlphaColor; public class procedure Show(pMsg: string; pDuration: TToastLength = ShortToast); overload; procedure Show; overload; property Text: string read FText write FText; property Duration: TToastLength read FDuration write FDuration; property Color: TAlphaColor read FColor write FColor; end;
Conforme visto no código em acima, adicionei as propriedades que já conhecemos
e inclui a propriedade Color. Também declarei um overload do Método Show.
Diferente do anterior esse é um método do objeto e não da classe, será chamado
a partir de uma instância do
TToastNotification.
Alterando Cor
Nosso método Show irá retornar a interface
JToast para nossa variável, assim podemos acessar a View do Toast e através do
método
setBackgroundColor poderemos alterar a cor de fundo do Toast.
procedure TToastNotification.Show; begin CallInUiThread(procedure var lToast: JToast; begin lToast := TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(FText), FDuration.ToAndroidLength); lToast.getView.setBackgroundColor(FColor); lToast.show; end); end;
Testando nossa classe
Criei um App para testarmos nossa classe, nele eu adicionei um TEdit o qual chamei de edtText e um TColorComboBox como nome de ColorComboBox.
Coloquei dois botões, o primeiro o nomeei para btnShowToast e chamei o método da
classe Show;
procedure TfrmToastNotification.btnShowToastClick(Sender: TObject); begin TToastNotification.Show(edtText.Text); end;
O segundo botão chamei de btnShowToastColor e utilizei o método show do
Objeto.
procedure TfrmToastNotification.btnShowToastColorClick(Sender: TObject); var lToastNotification : TToastNotification; begin lToastNotification := TToastNotification.Create; try lToastNotification.Text := edtText.Text; lToastNotification.Duration := TToastLength.LongToast; lToastNotification.Color := ColorComboBox.Color; lToastNotification.Show; finally lToastNotification.Free; end; end;
App para teste de Toast Notification |
O primeiro método gerou o Toast padrão do Android (o formato pode variar
conforme a versão do Android e fabricante do seu mobile), no meu caso ele
retornou com um layout Cinza de bordas arredondadas com a mensagem que
passamos por parâmetro.
Toast retornado pelo método btnShowToastClick |
Já o segundo botão, gerou um Toast na cor que atribuímos para a propriedade Color de nosso objeto. Mas desta vez ele perdeu as bordas arredondadas. Isso ocorreu porque alteramos a cor do background da View, substituindo o desenho original.
Toast retornado pelo método btnShowToastColorClick |
Uma maneira de manter o formato original é entrar um pouco mais a fundo na JView e aplicar um filtro de cor diretamente na propriedade Background da View.
O Background da View possui um método chamado setColorFilter com dois parâmetros, a cor que iremos aplicar e o modo como ela será aplicada (PorterDuff.Mode).
O filtro que iremos aplicar é o SRC_IN, que irá cobrir todos os pixels visíveis com a cor selecionada. Para mais informações sobre os filtros disponíveis veja a documentação do Android em https://developer.android.com/reference/android/graphics/PorterDuff.Mode
No Delphi a constante PorterDuff.Mode.SRC_IN fica
TJPorterDuff_Mode.JavaClass.SRC_IN
Então substituímos nossa chamada de:
lToast.getView.setBackgroundColor(FColor);
para
lToast.getView.getBackground.setColorFilter(FColor,
TJPorterDuff_Mode.JavaClass.SRC_IN);
procedure TToastNotification.Show; begin CallInUiThread(procedure var lToast: JToast; begin lToast := TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(FText), FDuration.ToAndroidLength); lToast.getView.getBackground.setColorFilter(FColor, TJPorterDuff_Mode.JavaClass.SRC_IN) lToast.show; end); end;
Toast retornado pelo método btnShowToastColorClick após o ajuste |
Alinhamento
Também podemos trabalhar o alinhamento e tamanho do Toast através do método SetGravity.
Neste método passamos o Gravity, que é o posicionamento que queremos utilizar, como Top, Bottom, e mais uma série de opções (A lista completa de opções pode ser verificada em https://developer.android.com/reference/android/view/Gravity),
O Delphi fornece as opções através do TJGravity na Unit
Androidapi.JNI.GraphicsContentViewText, onde podemos concatena-las utilizando
o operador “OR” por exemplo:
TJGravity.JavaClass.AXIS_CLIP or TJGravity.JavaClass.BOTTOM
Os outros dois parâmetros do método SetGravity é relacionado a posição em
pixels onde o Toast irá ser mostrado, XOffset e YOffset. Por exemplo, se
alinhado em Top ou Bottom o YOffset funcionará como uma margem.
Mas tem um segredinho, o valor usado para X e Y não é o mesmo valor que
estamos acostumados a usar nas posições e tamanhos no Delphi então precisamos
fazer a conversão usando o método ConvertPointToPixel.
Digamos que você queira fazer um alinhamento superior com um espaço de 200 do
Top da aplicação.
var lPointF := ConvertPointToPixel(TPointF.Create(0,200)); lToast.setGravity((TJGravity.JavaClass.AXIS_CLIP or TJGravity.JavaClass.TOP), Trunc(lPointF.X),Trunc(lPointF.Y));
Vamos adicionar estes novos recursos a nossa classe, criando mais 3
propriedades. Gravity, XOffset e YOffset, todos do tipo Integer.
Também declarei um construtor para iniciar o Gravity com -1, indicando que ele
não será modificado.
TToastNotification = class private FText: string; FDuration: TToastLength; FColor: TAlphaColor; FXOffset: Integer; FYOffset: Integer; FGravity: integer; public constructor Create; class procedure Show(pMsg: string; pDuration: TToastLength = ShortToast); overload; procedure Show; overload; property Text: string read FText write FText; property Duration: TToastLength read FDuration write FDuration; property Color: TAlphaColor read FColor write FColor; property Gravity: integer read FGravity write FGravity; property XOffset: Integer read FXOffset write FXOffset; property YOffset: Integer read FYOffset write FYOffset; end; constructor TToastNotification.Create; begin FGravity := -1; end;
No nosso método Show adicionamos o teste para a implementação do Gravity.
procedure TToastNotification.Show; begin CallInUiThread(procedure var lToast: JToast; begin lToast := TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(FText), FDuration.ToAndroidLength); lToast.getView.getBackground.setColorFilter(FColor, TJPorterDuff_Mode.JavaClass.SRC_IN); if FGravity <> -1 then begin var lPointFGravity:= ConvertPointToPixel(TPointF.Create(FXOffset,FYOffset)); lToast.setGravity(FGravity,Trunc(lPointFGravity.X),Trunc(lPointFGravity.Y)); end; lToast.show; end); end;
A alteração do Gravity fica bastante simples. Vou deixar fixo no nosso btnShowToastColorClick para fazer um alinhamento Top com espaço de 200 do topo da aplicação, mas você já entendeu como funciona!
lToastNotification.Gravity := TJGravity.JavaClass.AXIS_CLIP or TJGravity.JavaClass.TOP; lToastNotification.XOffset := 0; lToastNotification.YOffset := 200;
Tamanho
A alteração do tamanho do Toast podemos utilizar os métodos setMinimumWidth e
setMinimumHeight da View. Como o próprio nome já diz, ele irá definir o
tamanho mínimo da View.
Lembrando que sempre que nos referirmos a dimensões temos que utilizar o
método ConvertPointToPixel para termos as dimensões corretas em relação ao que
utilizamos no design do Delphi.
A View possui ainda uma série de outros método que podem ser utilizados para
customizar seu Toast, como rotação, escalas entre outros.
A interface JToast ainda possui configurações de margem horizontal e
vertical.
Texto
Nosso último tópico vamos falar sobre a customização de cor e tamanho do texto
do Toast.
Essa parte é um pouco mais complicada, porque não existe dentro do View do Toast um método que nos retorne o texto para que possamos customiza-lo.
Procurando um pouco na internet, é fácil achar a seguinte técnica para alterar
a cor do texto de um Toast:
Toast toast = Toast.makeText(context, TEXT, duration); View view = toast.getView(); TextView text = view.findViewById(android.R.id.message); text.setTextColor(YOUR_TEXT_COLOUR); toast.show();
Se verificarmos esse código Java, veremos que já fazemos o MakeText e temos o getView retornando a View do Toast. O que precisamos fazer agora é buscar a TextView que está dentro da View do Toast, e é justamente essa parte que ninguém conta como se faz em Delphi.
Não contavam até agora!
O método FindViewById precisa do ID do item que queremos buscar. Se olharmos a
documentação do Android expecificamente para o Objeto android.R.ID
descobriremos que Message é uma constante de valor 16908299.
(https://developer.android.com/reference/android/R.id.html#message) .
No Android os componentes do tipo Views geralmente possuem um ID, e a TextView
do Toast, assim como um AlertDialog (outro tipo de janelinha de mensagem do
Android) utilizam essa mesma constante para identificar sua View de Texto.
Dessa forma eu posso usar essa constante e ser feliz. Pelo menos até o Android
decidir em alguma nova versão que esse número não é tão mágico assim.
Obedecendo os conselhos do meu velho pai vamos usar um método para retornar
esse ID, assim como é feito no Java.
Então
android.R.id.message vira:
TAndroidHelper.Activity.getResources.getIdentifier( StringToJString('message'), StringToJString('id'), StringToJString('android'));
Agora que temos nosso ID basta passá-lo para o método FindViewById e
retornar a nossa View.
var lResourceID := TAndroidHelper.Activity.getResources.getIdentifier( StringToJString('message1'), StringToJString('id'), StringToJString('android')); if lResourceID <> 0 then begin var lView := lToast.getView.findViewById(lResourceID); end;
Em teoria como a interface JViewText é uma implementação da interface JView e
sabemos que o retorno desejado é um JViewText bastaria fazermos um Type Cast
para o tipo desejado e tudo funcionaria como esperado, algo como:
var lText: JTextView; lText := (lView as JTextView);
Certo? Errado!
Acontece que quando trabalhamos com os objetos Java do android, estamos
acessando apenas interfaces e elas não tem o mesmo dinamismo de classes e
objetos que estamos acostumados. Quando chamamos o FindViewById, ele retornou
uma série de ponteiros de memória para acessarmos um JView e todo o restante
passa a ser ignorado. Mesmo que você teste o método Suports para verificar se o
JView retornado tem suporte ao JTextView, verá que não. Tudo irá indicar que
ele não é um JTextView.
E como resolvemos isso, já que temos certeza absoluta de que essa JView é um
JTextView?
Toda interface Java no Delphi possui uma classe herdada de TJavaGenericImport
que sem nos estendermos muito nos detalhes, ela funciona como uma classe de
apoio para toda essa comunicação entre as classes Java do Android e o Delphi.
E um destes métodos é o Wrap. Com ele podemos fazer a carga correta da
interface esperada a partir de uma interface mais abstrata.
Assim utilizamos o objeto TJTextView para carregar nosso JTextView corretamente.
Nossa implementação fica desta forma:
var lText: JTextView;lText:= TJTextView. Wrap(lView);
Para a implementação da nossa classe eu adicionei 3 propriedades. CustomText (Boolean), para indicar que iremos customizar o texto, TextColor (TAlphaColor) e TextSize (Single), respectivamente a cor e tamanho da fonte.
property CustomText: Boolean read FCustomText write FCustomText; property TextColor: TAlphaColor read FTextColor write FTextColor; property TextSize: Single read FTextSize write FTextSize;
Nosso método Show finalizado fica com a seguinte implementação
constructor TToastNotification.Create; begin FGravity := -1; end; procedure TToastNotification.Show; begin CallInUiThread(procedure var lToast: JToast; begin lToast := TJToast.JavaClass.makeText(TAndroidHelper.Context, StrToJCharSequence(FText), FDuration.ToAndroidLength); lToast.getView.getBackground.setColorFilter(FColor, TJPorterDuff_Mode.JavaClass.SRC_IN); if FGravity <> -1 then begin var lPointFGravity := ConvertPointToPixel(TPointF.Create(FXOffset,FYOffset)); lToast.setGravity(FGravity,Trunc(lPointFGravity.X),Trunc(lPointFGravity.Y)); end; var lPointFTamanho := ConvertPointToPixel(TPointF.Create(FMinimumWidth,FMinimumHeight)); lToast.getView.setMinimumWidth(Trunc(lPointFTamanho.X)); lToast.getView.setMinimumHeight(Trunc(lPointFTamanho.Y)); if FCustomText then begin var lResourceID := TAndroidHelper.Activity.getResources.getIdentifier( StringToJString('message'), StringToJString('id'), StringToJString('android')); if lResourceID <> 0 then begin var lText := TJTextView.wrap(lToast.getView.findViewById(lResourceID)); lText.setTextColor(FTextColor); lText.setTextSize(FTextSize); end; end; lToast.show; end); end;
No nosso aplicativo de exemplo adicionei um TCheckBox (cbxCustomFont), um novo
TColorComboBox (ColorComboBoxText) e um TSpinBox (SpinBoxTextSize), para
habilitar a customização de texto selecionando a cor e o tamanho.
E no click do nosso botão ficou:
procedure TfrmToastNotification.btnShowToastColorClick(Sender: TObject); var lToastNotification : TToastNotification; begin lToastNotification := TToastNotification.Create; try lToastNotification.Text := edtText.Text; lToastNotification.Duration := TToastLength.LongToast; lToastNotification.Color := ColorComboBox.Color; lToastNotification.Gravity := TJGravity.JavaClass.AXIS_CLIP or TJGravity.JavaClass.TOP; lToastNotification.XOffset := 0; lToastNotification.YOffset := 200; lToastNotification.MinimumWidth := 300; lToastNotification.MinimumHeight := 150; lToastNotification.CustomText := cbxCustomFont.IsChecked; lToastNotification.TextColor := ColorComboBoxText.Color; lToastNotification.TextSize := SpinBoxTextSize.Value; lToastNotification.Show; finally lToastNotification.Free; end; end;
Exemplo de Toast customizado |
Plus, adicional, a mais!
Incluí uma brincadeira para mostrar as possibilidades do Toast
Notification.
Assim como mudamos as propriedades da View do Toast, podemos substituí-la por
outra.
Por exemplo podemos usar um JViewCalendar que é uma implementação de
Calendário de uma JView e passar ele para o Toast através do método
setView.
Criei um método de classe chamado ShowCalendar para exemplificar este caso.
class procedure TToastNotification.ShowCalendar(pDate: TDate; pDuration: TToastLength); var lJView: JCalendarView; begin lJView := TJCalendarView.JavaClass.init(TAndroidHelper.Context); lJView.setDate(DateTimeToUnix(pDate + 1) * MSecsPerSec); CallInUiThread(procedure var lToast: JToast; begin lToast := TJToast.JavaClass.init(TAndroidHelper.Context); lToast.setView(lJView); lToast.SetDuration(TJToast.JavaClass.LENGTH_LONG); lToast.show; end); end;
Exemplo substituindo a View padrão do Toast por um JCalendarView |
A seguir deixei um pequeno guia de referência com alguns tipos e métodos que precisei utilizar e seus respectivos uses.
Guia de referência:
TAlphaColor - System.UITypes
StrToJCharSequence - Androidapi.Helpers
JView.setBackgroundColor - Androidapi.JNI.GraphicsContentViewText
StringToJString - Androidapi.JNI.JavaTypes (para suporte inline)
ConvertPointToPixel - FMX.Platform.UI.Android
TPointF - System.Types
DateTimeToUnix - System.DateUtils
MSecsPerSec - System.SysUtils