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





