Páginas

quarta-feira, 19 de agosto de 2020

Criando um ListBox Delphi com a usabilidade da lista de contatos do WhatsApp

Olá, recruta

Neste post iremos dar algumas dicas de usabilidade criando um Listbox estilo WhatsApp.

Mas o que exatamente queremos dizer isso?

Muitas vezes desenvolvedores colocam botões de excluir, editar e outras funcionalidades em cada um dos itens do Listbox, desse forma repetindo cada um deles em cada registro da lista. Se repararmos no WhatsApp as únicas opções de um item é clicarmos nele para irmos para a tela de mensagens do contato, e a seleção do item ao manter-lo pressionado, que é justamente quando as demais opções aparecem. E é exatamente isso que iremos fazer, ao clicar em um item da nossa lista ir para uma aba de detalhe do item e habilitar botões como excluir ou alguma outra opção apenas quando houver um ou mais itens selecionados.

Vamos especificar exatamente nossas funcionalidades, nosso Listbox terá o seguinte comportamento:

- Ao clicar em um item irá para a tela de detalhe do item clicado;

- Ao segurarmos pressionado um item ele será selecionado;

- Quando um item já estiver selecionado ao clicar em um outro item ele também será selecionado;

- Ao clicar novamente em um item selecionado a seleção do item é desfeita.

- Ao “deselecionar” o último item selecionado, o comportamento de click dos itens irá voltar ao normal, ou seja, ao clicar irá para tela de detalhe;

- Quando um ou mais registros estiverem selecionados iremos exibir o botão de excluir e um botão de deselecionar todos.

Ele terá essa aparência: 

(Tela com o Listbox implementado)

Antes de tudo, quero deixar claro que esta é uma das formas que encontrei para criar esta usabilidade, é possível chegar no mesmo resultado de diversas formas, fique a vontade para adaptar a técnica a sua maneira, e se encontrar uma forma mais eficiente de chegar neste resultado, por favor não deixa de compartilhar com a comunidade Delphi.

Então vamos lá, bora criar uma nova aplicação mobile.

Clique no menu, File -> New -> Multi-Device Application – Delphi.

(File -> New -> Multi-Device Application – Delphi)

Na próxima janela selecione a opção Blank Application, para criarmos uma Aplicação em branco.  

(Blank Application)

Vamos aos nomes!

Vou chamar nosso projeto de ListBoxWhatsApp.dproj, nossa Unit de FListBoxWhatsApp.pas e o Form irei renomear para frmListBoxWhatsApp.  

Em nosso Form vamos colocar um TTabControl com dois TabItem.

No primeiro TabItem vamos colocar um TLayout alinhado ao topo e um TListBox com alinhamento AlClient, esse será o ListBox que faremos tudo acontecer.

Renomearemos o ListBox para lbxWhatsapp;

Já faz algum tempo que o Delphi trabalha com o conceito de estilos, e é isso que iremos fazer, criar um estilo para os ListBoxItems do lbxWhatsapp.

Adicione um novo ListBoxItem ao Listbox e click com o botão direito do mouse e selecione a opção Edit Custom Style...

(Edit Custom Style)

Seremos direcionados ao Style Designer, onde será criado um TLayout com o nome do seu ListBoxItem acrescido de “Style1”, no nosso caso ficou o nome ficou “ListBoxItem1Style1”, esse TLayout representará o nosso Style customizado. Vamos aproveitar e renomear a propriedade StyleName do Layout para stlWhatsApp, o StyleName é a propriedade que representa o nome do componente no Style Designer. É pelo StyleName que iremos acessar os componentes do Style Designer em nosso formulário.

(Style Designer)


Delete todos os componentes que estão no nosso Layout, vamos ficar apenas com o ActiveStyleObject, ele é responsável por fazer uma pequena animação quando o item é pressionado, assim como ocorre no WhatsApp.


(Structure - StyleContainer)


Agora é hora de montar nosso layout como consta na imagem a seguir.


(stlWhatsApp)


Redimensione o stlWhatsApp para 65 de Height  e 300 de Width;

Abaixo segue a lista de componentes que usamos e os StyleNames que foram dados:

stlWhatsApp = TLayout
ActiveStyleObject= TActiveStyleObject
recFundo = TRectangle
cirIniciais = TCircle
txtIniciais = TStyleTextObject
layTexto = TLayout
txtTitulo = TStyleTextObject
txtDetalhe = TStyleTextObject
linSeparador = TLine
imgSelecionado = TImage

(Componentes contidos em nosso Layout)

 




(Structure completo do Style)

Você pode baixar o arquivo com o layout neste link, basta copiar o texto e colar no StyleContainer:
https://drive.google.com/file/d/15N6WPjq9iCcQKw3Gz4ie98myNAfFGHax/view

Quando criamos um layout customizado temos que ter alguns cuidados, um deles é que os componentes colocados nele também podem reagir a eventos como clicks e gestures, isso faz como que eventos do ListboxItem e Listbox possam não ser executados. Por exemplo, se usarmos um retângulo como fundo para nosso Style, os eventos do retângulo terão prioridade sobre os eventos do ListboxItem, fazendo com que eventos como OnIntemClick e OnGesture do Listbox não funcionem.

Mas tem uma solução, basta desabilitar a propriedade HitTest.

HitTest define se um controle irá reagir a eventos do mouse ou não. No caso de um mobile, os eventos de touch, já que seu dedo é o mouse!

Então, como não vamos precisar de nenhum evento no nosso Style, garanta que a propriedade HintTest dos componentes contidos nele estejam desabilitadas.

Agora que já desenhamos nosso style e garantimos que ele não irá atrapalhar os eventos do nosso Listbox vamos voltar a nossa programação normal.😜

Vamos começar criando uma pequena lista de objetos que iremos utilizar para popular nosso Listbox, estes objetos terão apenas os campos que iremos utilizar para nosso exemplo.


 
   TRegistro = class
  private
    FId: Integer;
    FTitulo: string;
    FDetalhe: string;
    FIniciais: string;
  public
    property Id: Integer read FId write FId;
    property Titulo: string read FTitulo write FTitulo;
    property Detalhe: string read FDetalhe write FDetalhe;
    property Iniciais: string read FIniciais write FIniciais;
  end;


Para nossa lista iremos utilizar um TObjectList genérico da Unit Generics.Collections;

Dessa forma iremos declarar nossa variável, que será global, e chamaremos de FListaRegistro, na seção private da nossa unit;

FListaRegistro: TObjectList<TRegistro>;

No evento OnCreate do nosso formulário iremos instanciar nossa lista e populá-la com alguns registros.


 
procedure TfrmListboxWhatsApp.FormCreate(Sender: TObject);
var
  lRegistro: TRegistro;
begin
  FListaRegistro := TObjectList<TRegistro>.Create;
  lRegistro := TRegistro.Create;
  lRegistro.Id := 1;
  lRegistro.Iniciais := 'BW';
  lRegistro.Titulo := 'Bruce Wayne';
  lRegistro.Detalhe := 'Batman - Cavaleiro das Trevas';
  FListaRegistro.Add(lRegistro);

  lRegistro := TRegistro.Create;
  lRegistro.Id := 2;
  lRegistro.Iniciais := 'CK';
  lRegistro.Titulo := 'Clark Kent';
  lRegistro.Detalhe := 'Superman - Homem de Aço';
  FListaRegistro.Add(lRegistro);

  lRegistro := TRegistro.Create;
  lRegistro.Id := 3;
  lRegistro.Iniciais := 'DP';
  lRegistro.Titulo := 'Diana Prince';
  lRegistro.Detalhe := 'Mulher Maravilha - Princesa de Themysira';
  FListaRegistro.Add(lRegistro);

  lRegistro := TRegistro.Create;
  lRegistro.Id := 4;
  lRegistro.Iniciais := 'HJ';
  lRegistro.Titulo := 'Hal Jordan';
  lRegistro.Detalhe := 'Lanterna Verde - Cavaleiro Esmeralda';
  FListaRegistro.Add(lRegistro);

  lRegistro := TRegistro.Create;
  lRegistro.Id := 5;
  lRegistro.Iniciais := 'BA';
  lRegistro.Titulo := 'Barry Allen';
  lRegistro.Detalhe := 'Flash - Velocista Escarlate';
  FListaRegistro.Add(lRegistro);
end;


Agora que temos nossa lista vamos usá-la para popular nosso ListBox. Para isso iremos criar uma procedure chamada PopularListBox, conforme o fonte a seguir;


   
procedure TfrmListboxWhatsApp.PopularListbox;
begin
  lbxWhatsapp.BeginUpdate;
  try
    lbxWhatsapp.Clear;
    for var lRegistro in FListaRegistro do
    begin
      var lListBoxItem := TListBoxItem.Create(lbxWhatsapp);
      lListBoxItem.Height := 65;
      lListBoxItem.Parent := lbxWhatsapp;
      lListBoxItem.StyleLookup := 'stlWhatsApp';
      lListBoxItem.StylesData['txtTitulo'] := lRegistro.Titulo;
      lListBoxItem.StylesData['txtDetalhe'] := lRegistro.Detalhe;
      lListBoxItem.StylesData['txtIniciais'] := lRegistro.Iniciais;
      lListBoxItem.StylesData['imgSelecionado.visible'] := False;
      lListBoxItem.Tag := integer(lRegistro);
    end;
  finally
    lbxWhatsapp.EndUpdate;
  end;
end;


Vamos analisar este código,

Sempre que vamos fazer uma alteração em nosso Listbox é importante chamar o método BeginUpdate antes das alterações de layout, isso irá evitar que o Listbox fique se redesenhando a todo momento, gastando processamento desnecessário. Quando tudo estiver pronto, finalizamos com o método EndUpdate, para que todo o desenho seja feito de uma única vez.

Percorremos nossa lista através de um for-in para criarmos os itens de nosso ListBox.
Um recurso recente do Delphi (Inline Variable Declaration) permite que eu declare minhas variáveis locais diretamente no código, eu particularmente gosto muito desse recurso, uma vez que estas variáveis tem uma vida apenas no contexto onde são declarados, ou seja, ele irá existir apenas dentro do bloco begin-end onde foram declaradas, no nosso caso, nenhuma existirá ou será acessada fora do bloco do loop.

Para cada ListboxItem que criarmos iremos atribuir o Style que criamos através da propriedade StyleLookup.

Os componentes utilizados em nosso Style não estão declarados em nenhuma Unit, então não podemos acessa-lo como fazemos normalmente com qualquer outro componente, sendo assim a forma que a Embarcadero criou para que possamos acessa-los foi através de recursos de RTTI, as propriedades e eventos dos componentes do Style poderão ser alterados através de uma declaração em forma de string pela propriedade StylesData do ListBoxItem.

Observe que para atribuir valor para a propriedade visible do componente imgSelecionado escrevemos “imgSelecionado.visible” enquanto que, para atribuir a propriedade texto dos componentes TStyleTextObject apenas passamos o nome do componente. Isso ocorre porque a propriedade Text é a propriedade Default do TStyleTextObject, e pode ser suprimida ao passarmos como parâmetro para o StylesData.

Uma coisa que eu gosto de fazer quando trabalho com lista de objetos e Listbox, é atribuir o ponteiro do item da lista do objeto a propriedade Tag do ListboxItem, isso facilita quando quisermos buscar quaisquer informação do objeto, muito mais eficiente que atribuir apenas um id e depois ter que percorrer a lista de objetos para localizar o item em questão.

Para exemplificar essa técnica, criei uma segunda aba (tbiDetalhe) no nosso PageControl (pgcPrincipal) onde será exibido os dados do contato que estão em nosso objeto. Ao clicar em nosso ListBoxItem vamos para a pagina de detalhe e populamos os Labels contidos lá.


   
procedure TfrmListboxWhatsApp.lbxWhatsappItemClick(const Sender: TCustomListBox;
  const Item: TListBoxItem);
begin
  if (pItem.Tag <> 0) then
  begin
    var lRegistro := TRegistro(pItem.Tag);
    lblId.Text := lRegistro.Id.ToString;
    lblIniciais.Text := lRegistro.Iniciais;
    lblTitulo.Text := lRegistro.Titulo;
    lblDetalhe.Text := lRegistro.Detalhe;
    tbcPrincipal.GotoVisibleTab(tbiDetalhe.Index);
  end;
end;  


Observe no código acima que basta fazer um type cast da Tag do item clicado para acessar o objeto referente a ele.

Vamos aproveitar o momento e já refatorar esse método, encapsulando esse código em método com um nome que condiga com seu propósito.


  
procedure TfrmListboxWhatsApp.lbxWhatsappItemClick(const Sender: TCustomListBox;
  const Item: TListBoxItem);
begin
  MostrarDetalhesDoContato(Item);
end;

procedure TfrmListboxWhatsApp.MostrarDetalhesDoContato(pItem: TListBoxItem);
begin
  if (pItem.Tag <> 0) then
  begin
    var lRegistro := TRegistro(pItem.Tag);
    lblId.Text := lRegistro.Id.ToString;
    lblIniciais.Text := lRegistro.Iniciais;
    lblTitulo.Text := lRegistro.Titulo;
    lblDetalhe.Text := lRegistro.Detalhe;
    tbcPrincipal.GotoVisibleTab(tbiDetalhe.Index);
  end;
end;  


Assim como o WhatsApp , ao clicarmos no nosso ListboxItem também estamos sendo direcionados para uma outra aba referente ao item clicado. Nossa próxima meta é selecionar um item ao segura-lo pressionado.

Para essa interação vamos utilizar um Gesture, para sermos mais específico, um LongTap.

Os Gestures são capturado facilmente no evento OnGesture de qualquer controle, onde através do parâmetro EventInfo podemos identificar qual Gesture foi executado.

Sendo assim no evento OnGesture do stlWhatsApp verificamos se o EventInfo.GestureID é igual a igiLongTap e assim efetuamos a seleção do nosso registro.

Como vamos ter múltipla seleção, vamos usar a propriedade isChecked do ListBoxItem para marcar os itens selecionados.

Criei um método chamado SelecionarItem passando o ListBoxItem como parâmetro, ao seleciona-lo mudamos a cor de fundo e mostramos a imagem de seleção (imgSelecionado), caso o item já esteja selecionado, fazemos a operação inversa. 

Eu atribuo false para IsSelected para que ele fique com a cor que selecionamos e não a cor de seleção padrão.


  
procedure TfrmListboxWhatsApp.SelecionarItem(pListBoxItem : TListBoxItem);
begin
  if pListBoxItem.IsChecked then
    pListBoxItem.StylesData['recFundo.Fill.Color'] := TValue.From<TAlphaColor>(TAlphaColorRec.White)
  else
    pListBoxItem.StylesData['recFundo.Fill.Color'] := TValue.From<TAlphaColor>(TAlphaColorRec.Papayawhip);

  pListBoxItem.StylesData['imgSelecionado.Visible'] := TValue.From<boolean>(not pListBoxItem.IsChecked);
  pListBoxItem.IsChecked := not pListBoxItem.IsChecked;
  pListBoxItem.IsSelected := False;
end;


No evento OnGesture do stlWhatsApp vamos identificar o item que está sendo pressionado e chamar nosso método de seleção. O item sempre é selecionado quando executamos um LongTap, então podemos pegar o ItemIndex para identificar o ListBoxItem que queremos marcar a seleção.


  
procedure TfrmListboxWhatsApp.lbxWhatsappGesture(Sender: TObject;
  const EventInfo: TGestureEventInfo; var Handled: Boolean);
begin
  if EventInfo.GestureID = igiLongTap then
  begin
    SelecionarItem(lbxWhatsapp.Selected);
    FLongTap := True;
  end;
end;


Por padrão a captura do gesture LongTap não está habilitada, então selecione o ListBox, no Object Inpector na propriedade Touch > InteractiveGestures e habilite o item LongTap.


Object Inspector - LongTap


Mesmo capturando o LongTap, o evento de OnItemClick ainda é executado, então criamos uma variável global booleana (FLongTap) para que saibamos que o LongTap foi executado e assim não irmos para a aba de detalhe do item, fazendo apenas a seleção.

No OnItemClick do ListBox, onde inicialmente vamos para a aba de detalhe referente ao item clicado, vamos dar uma incrementada, para que se houver pelo menos um item selecionado, quando clicarmos em um outro item ele também será selecionado, se por acaso esse item já estiver selecionado, o item é desmarcado, até não restar mais nenhum item selecionado, e assim a função de click será restaurada para sua função padrão, que é exibir os detalhes do item clicado.

Estamos utilizando a propriedade IsCheck para marcar os itens selecionados, e vamos precisar saber se já temos pelo menos um item selecionado para nosso ajuste do evento OnItemClick do ListBox. Posteriormente podemos querer exibir o número de registros selecionados, então nada melhor que criar um método que nos retorne isso, assim não teremos que ficar fazendo um loop em vários pontos do nosso código, já que o Listbox não possui um método que nos retorne essa quantidade.

Abrindo um adendo a nossa implementação, uma técnica muito boa para implementarmos nosso contador de ListBoxsItens checados é a de ClassHelper. Essa técnica nos permite adicionarmos métodos a uma classe sem precisarmos estendê-las, ou seja, podemos adicionar uma função CheckedCount ao Listbox sem ter que criar uma herança dele.

Para criar um Class Helper, declararmos um novo tipo como “Class helper for” seguido pela classe que o helper irá se referenciar, no nosso caso um TListBox.

Veja a seguir nossa declaração na seção Interface.

  
type
 TListBoxHelper = class helper for TListBox
    function CheckedCount: Integer;
  end;


Logo a baixo nossa implentação.

  
{ TListBoxHelper }

function TListBoxHelper.CheckedCount: Integer;
begin
  Result := 0;
  for var li := 0 to Items.Count - 1 do
  begin
    if ListItems[li].IsChecked then
      inc(Result);
  end;
end;


Voltando ao nosso evento OnItemClick, testamos se este click não foi executado em decorrência do LongTap e caso tenhamos algum item checado nós executamos o método SelecionarItem, do contrário é chamado o método MostrarDetalhesDoContato, por fim garantimos que a variável FLongTap será atribuída com false.


  
procedure TfrmListboxWhatsApp.lbxWhatsappItemClick(const Sender: TCustomListBox;
  const Item: TListBoxItem);
begin
  if not FLongTap then
  begin
    if lbxWhatsapp.CheckedCount > 0 then
      SelecionarItem(Item)
    else
      MostrarDetalhesDoContato(Item);
  end;
  Item.IsSelected := False;
FLongTap := False; end;


Com isso nossa implementação do ListBox está concluída, mas podemos adicionar mais alguns itens para melhorar ainda mais nossa usabilidade, como por exemplo, ao selecionarmos um item do Listbox podemos realizar uma breve vibração, assim como ocorre no WhatsApp. Vou demonstrar como fazer isso no Android, uma pequena receita de bolo. 

Criei o método vibrar passando como parâmetro o tempo de vibração em milissegundos. Depois basta adicionar esse método no evento OnGesture quando por executado um LongTap.


  
procedure TfrmListboxWhatsApp.Vibrar(pTempo: cardinal);
{$IF DEFINED(ANDROID)}
var
  VibratorObj: JObject;
  Vibrator: JVibrator;
{$ENDIF}
begin
{$IF DEFINED(ANDROID)}
  VibratorObj := TAndroidHelper.Activity.getSystemService(TJActivity.JavaClass.VIBRATOR_SERVICE);
  Vibrator    := TJVibrator.Wrap((VibratorObj as ILocalObject).GetObjectID);
  Vibrator.vibrate(pTempo);
{$ENDIF}
end;


Para exibir botões ou a quantidade de itens selecionados basta adicionar seu código no método SelecionarItem e testar com se o lbxWhatsapp.CheckedCount é maior que zero para saber se existe algum item selecionado. No código a seguir eu mostro dois botões se houver alguma seleção, um botão de exclusão e um para desmarcar todos.


procedure TfrmListboxWhatsApp.SelecionarItem(pListBoxItem : TListBoxItem);
begin
  if pListBoxItem.IsChecked then
    pListBoxItem.StylesData['recFundo.Fill.Color'] := TValue.From<TAlphaColor>(TAlphaColorRec.White)
  else
    pListBoxItem.StylesData['recFundo.Fill.Color'] := TValue.From<TAlphaColor>(TAlphaColorRec.Papayawhip);

  pListBoxItem.StylesData['imgSelecionado.Visible'] := TValue.From<Boolean>(not pListBoxItem.IsChecked);
  pListBoxItem.IsChecked := not pListBoxItem.IsChecked;
  pListBoxItem.IsSelected := False;

  btnDesmarcar.Visible := lbxWhatsapp.CheckedCount > 0;
  btnExcluir.Visible := lbxWhatsapp.CheckedCount > 0;
end;


Para finalizar segue o código para o click botão de desmarcar todos.

 
procedure TfrmListboxWhatsApp.btnDesmarcarClick(Sender: TObject);
begin
  for var li := 0 to  lbxWhatsapp.Items.Count - 1 do
  begin
    if lbxWhatsapp.ListItems[li].IsChecked then
      SelecionarItem(lbxWhatsapp.ListItems[li]);
  end;
end;

Finalizamos aqui nosso conjunto de dicas para criar um Listbox com a cara e o jeito do WhatsApp. O código fonte deste post está em https://github.com/MukaDavid/ListBoxWhatsApp.

Fique ligado no blog que em breve teremos mais novidades. 

Ficou interessado em saber mais sobre customizações e utilização de ListBoxs no firemonkey, conheça o treinamento "Exibindo Listagem de Dados no Delphi/Firemonkey" do nosso grande amigo Landerson Gomes na plataforma Eduzz;


Quer ser um programador de Elite?
Nunca serão! 😉