Personalização do Android: como criar um componente de interface do usuário que faça o que você deseja

Publicados: 2022-03-11

Não é incomum que os desenvolvedores precisem de um componente de interface do usuário que não é fornecido pela plataforma que eles estão direcionando ou é, de fato, fornecido, mas não possui uma determinada propriedade ou comportamento. A resposta para ambos os cenários é um componente de interface do usuário personalizado.

O modelo de interface do usuário do Android é inerentemente personalizável, oferecendo os meios de personalização do Android, teste e a capacidade de criar componentes de interface do usuário personalizados de várias maneiras:

  • Herdar um componente existente (ou seja, TextView , ImageView , etc.) e adicionar/substituir a funcionalidade necessária. Por exemplo, um CircleImageView que herda ImageView , substituindo a função onDraw() para restringir a imagem exibida a um círculo e adicionando uma função loadFromFile() para carregar uma imagem da memória externa.

  • Crie um componente composto de vários componentes. Essa abordagem geralmente aproveita os Layouts para controlar como os componentes são organizados na tela. Por exemplo, um LabeledEditText que herda LinearLayout com orientação horizontal e contém um TextView atuando como um rótulo e um EditText atuando como um campo de entrada de texto.

    Essa abordagem também pode fazer uso da anterior, ou seja, os componentes internos podem ser nativos ou personalizados.

  • A abordagem mais versátil e complexa é criar um componente auto-desenhado . Nesse caso, o componente herdaria a classe View genérica e substituiria funções como onMeasure() para determinar seu layout, onDraw() para exibir seu conteúdo etc. Os componentes criados dessa maneira geralmente dependem muito da API de desenho 2D do Android.

Estudo de caso de personalização do Android: o CalendarView

O Android fornece um componente nativo CalendarView . Ele tem um bom desempenho e fornece a funcionalidade mínima esperada de qualquer componente de calendário, exibindo um mês inteiro e destacando o dia atual. Alguns podem dizer que parece bom também, mas apenas se você estiver procurando uma aparência nativa e não tiver interesse em personalizar a aparência.

Por exemplo, o componente CalendarView não fornece nenhuma maneira de alterar como um determinado dia é marcado ou qual cor de fundo usar. Também não há como adicionar qualquer texto ou gráfico personalizado, para marcar uma ocasião especial, por exemplo. Resumindo, o componente fica assim, e quase nada pode ser alterado:

Captura de tela
CalendarView no tema AppCompact.Light .

Faça o seu próprio

Então, como se cria a própria visualização de calendário? Qualquer uma das abordagens acima funcionaria. No entanto, a praticidade geralmente descarta a terceira opção (gráficos 2D) e nos deixa com os dois outros métodos, e empregaremos uma mistura de ambos neste artigo.

Para acompanhar, você pode encontrar o código-fonte aqui.

1. O Layout do Componente

Primeiro, vamos começar com a aparência do componente. Para simplificar, vamos exibir os dias em uma grade e, na parte superior, o nome do mês junto com os botões “próximo mês” e “mês anterior”.

Captura de tela
Visualização de calendário personalizada.

Esse layout é definido no arquivo control_calendar.xml , conforme a seguir. Observe que algumas marcações repetitivas foram abreviadas com ... :

 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>

2. A Classe do Componente

O layout anterior pode ser incluído como está em uma Activity ou Fragment e funcionará bem. Mas encapsulá-lo como um componente de interface do usuário autônomo evitará a repetição de código e permitirá um design modular, onde cada módulo lida com uma responsabilidade.

Nosso componente de interface do usuário será um LinearLayout , para corresponder à raiz do arquivo de layout XML. Observe que apenas as partes importantes são mostradas no código. A implementação do componente reside em CalendarView.java :

 public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }

O código é bem direto. Após a criação, o componente infla o layout XML e, quando isso é feito, atribui os controles internos às variáveis ​​locais para facilitar o acesso posterior.

3. Alguma Lógica é Necessária

Para fazer com que esse componente realmente se comporte como uma exibição de calendário, é necessária alguma lógica de negócios. Pode parecer complicado no começo, mas na verdade não há muito o que fazer. Vamos decompô-lo:

  1. A visualização do calendário tem sete dias de largura e é garantido que todos os meses começarão em algum lugar na primeira linha.

  2. Primeiro, precisamos descobrir em que posição o mês começa e, em seguida, preencher todas as posições anteriores com os números do mês anterior (30, 29, 28.. etc.) até chegarmos à posição 0.

  3. Em seguida, preenchemos os dias do mês atual (1, 2, 3… etc).

  4. Depois disso vêm os dias para o próximo mês (novamente, 1, 2, 3.. etc), mas desta vez só preenchemos as posições restantes na(s) última(s) linha(s) da grade.

O diagrama a seguir ilustra essas etapas:

Captura de tela
Lógica de negócios de exibição de calendário personalizada.

A largura da grade já está especificada em sete células, denotando um calendário semanal, mas e a altura? O maior tamanho da grade pode ser determinado pelo pior cenário de um mês de 31 dias começando em um sábado, que é a última célula da primeira linha, e precisará de mais 5 linhas para ser exibida por completo. Portanto, configurar o calendário para exibir seis linhas (totalizando 42 dias) será suficiente para lidar com todos os casos.

Mas nem todos os meses têm 31 dias! Podemos evitar complicações decorrentes disso usando a funcionalidade de data integrada do Android, evitando a necessidade de descobrir o número de dias por conta própria.

Como mencionado anteriormente, as funcionalidades de data fornecidas pela classe Calendar tornam a implementação bastante direta. Em nosso componente, a função updateCalendar() implementa esta lógica:

 private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }

4. Personalizável no coração

Como o componente responsável por exibir os dias individuais é um GridView , um bom lugar para personalizar como os dias são exibidos é o Adapter , pois ele é responsável por manter os dados e inflar as visualizações para células individuais da grade.

Para este exemplo, exigiremos o seguinte de nosso CalendearView :

  • O dia atual deve estar em negrito em texto azul .
  • Os dias fora do mês atual devem estar acinzentados .
  • Os dias com um evento devem exibir um ícone especial.
  • O cabeçalho do calendário deve mudar de cor dependendo da estação (verão, outono, inverno, primavera).

Os três primeiros requisitos são simples de serem alcançados alterando os atributos de texto e os recursos de plano de fundo. Vamos implementar um CalendarAdapter para realizar esta tarefa. É simples o suficiente para que possa ser uma classe membro em CalendarView . Ao substituir a função getView() , podemos atender aos requisitos acima:

 @Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }

O requisito final de design exige um pouco mais de trabalho. Primeiro, vamos adicionar as cores das quatro estações em /res/values/colors.xml :

 <color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>

Então, vamos usar uma matriz para definir a estação para cada mês (assumindo o hemisfério norte, para simplificar; desculpe, Austrália!). Em CalendarView adicionamos as seguintes variáveis ​​de membro:

 // seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};

Dessa forma, a seleção de uma cor apropriada é feita selecionando a estação apropriada ( monthSeason[currentMonth] ) e, em seguida, escolhendo a cor correspondente ( rainbow[monthSeason[currentMonth] ), isso é adicionado a updateCalendar() para garantir que a cor apropriada seja selecionada sempre que o calendário é alterado.

 // set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));

Com isso, obtemos o seguinte resultado:

Personalização do Android
A cor do cabeçalho muda de acordo com a estação.

Nota importante devido à maneira como HashSet compara objetos, a verificação acima eventDays.contains(date) em updateCalendar() não produzirá true para objetos de data a menos que sejam exatamente idênticos. Ele não executa nenhuma verificação especial para o tipo de dados Date . Para contornar isso, essa verificação é substituída pelo seguinte código:

 for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }

5. Parece feio em tempo de design

A escolha do Android para espaços reservados em tempo de design pode ser questionável. Felizmente, o Android realmente instancia nosso componente para renderizá-lo no designer de interface do usuário, e podemos explorar isso chamando updateCalendar() no construtor do componente. Dessa forma, o componente realmente fará sentido em tempo de design.

Captura de tela

Se a inicialização do componente exigir muito processamento ou carregar muitos dados, isso poderá afetar o desempenho do IDE. Nesse caso, o Android fornece uma função bacana chamada isInEditMode() que pode ser usada para limitar a quantidade de dados usados ​​quando o componente é realmente instanciado no designer de interface do usuário. Por exemplo, se houver muitos eventos a serem carregados no CalendarView , podemos usar isInEditMode() dentro da função updateCalendar() para fornecer uma lista de eventos vazia/limitada no modo de design e carregar o real caso contrário.

6. Invocando o Componente

O componente pode ser incluído em arquivos de layout XML (um exemplo de uso pode ser encontrado em activity_main.xml ):

 <samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>

E recuperado para interagir assim que o layout for carregado:

 HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);

O código acima cria um HashSet de eventos, adiciona o dia atual a ele e o passa para CalendarView . Como resultado, o CalendarView exibirá o dia atual em negrito azul e também colocará o marcador de evento nele:

Captura de tela
CalendarView exibindo um evento

7. Adicionando Atributos

Outra facilidade fornecida pelo Android é atribuir atributos a um componente personalizado. Isso permite que os desenvolvedores Android que usam o componente selecionem configurações por meio do XML de layout e vejam o resultado imediatamente no designer de interface do usuário, em vez de esperar e ver como o CalendarView se parece em tempo de execução. Vamos adicionar a capacidade de alterar a exibição do formato de data no componente, por exemplo, para soletrar o nome completo do mês em vez da abreviação de três letras.

Para isso, são necessários os seguintes passos:

  • Declare o atributo. Vamos chamá-lo de dateFormat e dar-lhe o tipo de dados string . Adicione-o a /res/values/attrs.xml :
 <resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
  • Use o atributo no layout que está usando o componente e dê a ele o valor "MMMM yyyy" :
 <samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
  • Por fim, faça com que o componente use o valor do atributo:
 TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);

] Construa o projeto e você notará as alterações de data exibidas no designer de interface do usuário para usar o nome completo do mês, como “julho de 2015”. Tente fornecer valores diferentes e veja o que acontece.

Captura de tela
Alterando os atributos CalendarView .

8. Interagindo com o Componente

Você já tentou pressionar em um dia específico? Os elementos de UI internos em nosso componente ainda se comportam da maneira normal esperada e dispararão eventos em resposta às ações do usuário. Então, como lidamos com esses eventos?

A resposta envolve duas partes:

  • Capture eventos dentro do componente e
  • Reporte eventos para o pai do componente (pode ser um Fragment , uma Activity ou até mesmo outro componente).

A primeira parte é bem direta. Por exemplo, para lidar com itens de grade de pressionamento longo, atribuímos um ouvinte correspondente em nossa classe de componente:

 // long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });

Existem vários métodos para relatar eventos. Uma maneira direta e simples é copiar a maneira como o Android faz isso: ele fornece uma interface para os eventos do componente que é implementada pelo pai do componente ( eventHandler no trecho de código acima).

As funções da interface podem receber quaisquer dados relevantes para o aplicativo. No nosso caso, a interface precisa expor um manipulador de eventos, ao qual é passada a data do dia pressionado. A seguinte interface é definida em CalendarView :

 public interface EventHandler { void onDayLongPress(Date date); }

A implementação fornecida pelo pai pode ser fornecida à visualização do calendário por meio de um setEventHandler() . Aqui está um exemplo de uso de `MainActivity.java':

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }

Pressionar um dia por muito tempo acionará um evento de pressionamento longo que é capturado e manipulado pelo GridView e relatado chamando onDayLongPress() na implementação fornecida, que, por sua vez, mostrará a data do dia pressionado na tela:

Captura de tela

Outra maneira mais avançada de lidar com isso é usando Intents e BroadcastReceivers do Android. Isso é particularmente útil quando vários componentes precisam ser notificados sobre o evento do calendário. Por exemplo, se pressionar um dia no calendário requer que um texto seja exibido em uma Activity e um arquivo seja baixado por um Service em segundo plano.

Usar a abordagem anterior exigirá que a Activity forneça um EventHandler para o componente, manipulando o evento e passando-o para o Service . Em vez disso, fazer com que o componente transmita um Intent e a Activity e o Service o aceitem por meio de seus próprios BroadcastReceivers não apenas facilita a vida, mas também ajuda a desacoplar a Activity e o Service em questão.

Conclusão

Contemple o incrível poder da personalização do Android!
Tweet

Então, é assim que você cria seu próprio componente personalizado em algumas etapas simples:

  • Crie o layout XML e estilize-o para atender às suas necessidades.
  • Derive sua classe de componente do componente pai apropriado, de acordo com seu layout XML.
  • Adicione a lógica de negócios do seu componente.
  • Use atributos para permitir que os usuários modifiquem o comportamento do componente.
  • Para facilitar o uso do componente no designer de interface do usuário, use a função isInEditMode() do Android.

Neste artigo, criamos uma visualização de calendário como exemplo, principalmente porque a visualização de calendário de ações está, em muitos aspectos, ausente. Mas você não está limitado quanto ao tipo de componentes que pode criar. Você pode usar a mesma técnica para criar qualquer coisa que precisar, o céu é o limite!

Obrigado por ler este guia, desejo-lhe boa sorte em seus esforços de codificação!