Ícone com um contador na barra de ferramentas superior: um exemplo de uma variedade de abordagens para uma tarefa


Na vida de cada desenvolvedor, há um momento em que, quando você vê uma solução interessante em um aplicativo estrangeiro, deseja implementá-lo por conta própria. Isso é lógico e deve ser bastante simples. E certamente pessoas atenciosas da "corporação da bondade" escreveram algum tipo de guia sobre isso ou fizeram um vídeo de treinamento onde foi mostrado nos dedos como chamar alguns dos métodos necessários para alcançar o resultado desejado. Este é frequentemente o caso.

Mas isso acontece de uma maneira completamente diferente: você vê a implementação de algo em cada segundo aplicativo e, quando se trata da implementação do mesmo em casa - verifica-se que ainda não há soluções fáceis para isso ...

Isso aconteceu comigo quando se tornou necessário adicionar um ícone com um contador ao painel superior. Fiquei muito surpreso quando descobriu que não há uma solução simples para implementar um elemento de interface do usuário tão familiar e exigido. Mas acontece, infelizmente. E decidi voltar ao conhecimento da rede mundial. A questão de colocar um ícone com um contador na barra de ferramentas superior, preocupou-se bastante. Depois de passar algum tempo na Internet, encontrei muitas soluções diferentes. Em geral, todos são trabalhadores e têm direito à vida. Além disso, o resultado da minha pesquisa mostra claramente como abordar a solução de tarefas no Android de diferentes maneiras.

Neste artigo, falarei sobre várias implementações do ícone do contador. Aqui estão 4 exemplos. Se você pensar um pouco mais amplamente, falaremos sobre praticamente qualquer elemento personalizado que queremos colocar na barra de ferramentas superior. Então, vamos começar.

Decisão um


Conceito


Cada vez que você precisa desenhar ou atualizar o contador no ícone, é necessário criar Drawable base no arquivo de marcação e desenhá-lo na barra de ferramentas como um ícone.

Implementação


Crie um arquivo de marcação badge_with_counter_icon em res/layouts :

 <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="@dimen/menu_item_icon_size" > <ImageView android:id="@+id/icon_badge" android:layout_width="@dimen/menu_item_icon_size" android:layout_height="@dimen/menu_item_icon_size" android:scaleType="fitXY" android:src="@drawable/icon" android:layout_alignParentStart="true"/> <TextView android:id="@+id/counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@id/icon_badge" android:layout_alignTop="@+id/icon_badge" android:layout_gravity="center" android:layout_marginStart="@dimen/counter_left_margin" android:background="@drawable/counter_background" android:gravity="center" android:paddingLeft="@dimen/counter_text_horizontal_padding" android:paddingRight="@dimen/counter_text_horizontal_padding" android:text="99" android:textAppearance="@style/CounterText" /> </RelativeLayout> 

Aqui, anexamos o contador à borda esquerda do ícone e indicamos um recuo fixo: isso é necessário para que, quando o comprimento do texto aumentar, o ícone principal do contador não se sobreponha mais - seja feio.

Em res/values/dimens adicione:

 <dimen name="menu_item_icon_size">24dp</dimen> <dimen name="counter_left_margin">14dp</dimen> <dimen name="counter_badge_radius">6dp</dimen> <dimen name="counter_text_size">9sp</dimen> <dimen name="counter_text_horizontal_padding">4dp</dimen> 

Tamanho do ícone de acordo com o guia Design de material .

Em res/values/colors adicione:

 <color name="counter_background_color">@android:color/holo_red_light</color> <color name="counter_text_color">@android:color/white</color> 

Em res/values/styles adicione:

 <style name="CounterText"> <item name="android:fontFamily">sans-serif</item> <item name="android:textSize">@dimen/counter_text_size</item> <item name="android:textColor">@color/counter_text_color</item> <item name="android:textStyle">normal</item> </style> 

Crie res/drawable/ em res/drawable/ resource:

 <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="@color/counter_background_color"/> <corners android:radius="@dimen/counter_badge_radius"/> </shape> 

Como ícone, tire uma foto, chame-o de icon e coloque recursos.

Em res/menu crie o arquivo menu_main.xml :

 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/action_counter_1" android:icon="@drawable/icon" android:title="icon" app:showAsAction="ifRoom"/> </menu> 

Crie uma classe que converte a marcação em Drawable :

LayoutToDrawableConverter.java

 package com.example.counters.counters; import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; public class LayoutToDrawableConverter { public static Drawable convertToImage(Context context, int count, int drawableId) { LayoutInflater inflater = LayoutInflater.from(context); View view = inflater.inflate(R.layout.badge_with_counter_icon, null); ((ImageView) view.findViewById(R.id.icon_badge)).setImageResource(drawableId); TextView textView = view.findViewById(R.id.counter); if (count == 0) { textView.setVisibility(View.GONE); } else { textView.setText(String.valueOf(count)); } view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); view.setDrawingCacheEnabled(true); view.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); view.setDrawingCacheEnabled(false); return new BitmapDrawable(context.getResources(), bitmap); } } 

Em seguida, na Activity que precisamos, adicione:

  private int mCounterValue1 = 0; @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); MenuItem menuItem = menu.findItem(R.id.action_with_counter_1); menuItem.setIcon(LayoutToDrawableConverter.convertToImage(this, mCounterValue1, R.drawable.icon)); return true; } @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_counter_1: updateFirstCounter(mCounterValue1 + 1); return true; default: return super.onOptionsItemSelected(item); } } private void updateFirstCounter(int newCounterValue){ mCountrerValue1 = newCounterValue; invalidateOptionsMenu(); } 

Agora, se for necessário atualizar o contador, chamamos o método updateFirstCounter , passando o valor real para ele. Aqui, publiquei um aumento no valor do contador quando clico no ícone. Com outras implementações, farei o mesmo.

É necessário prestar atenção ao seguinte: formamos uma imagem, que depois alimentamos no item de menu - todos os recuos necessários são gerados automaticamente, não precisamos levar em consideração.

Segunda decisão


Conceito


Nesta implementação, LayerList ícone baseado no elemento multicamada descrito em LayerList , no qual, no momento certo, desenhamos o próprio contador, deixando o ícone inalterado.

Implementação


A seguir, adicionarei gradualmente recursos e código para todas as implementações.

Em res/drawable/ create ic_layered_counter_icon.xml :

 <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/icon" android:gravity="center" /> <item android:id="@+id/ic_counter" android:drawable="@android:color/transparent" /> </layer-list> 

Em res/menu/menu_main.xml adicione:

 <item android:id="@+id/action_counter_2" android:icon="@drawable/ic_layered_counter_icon" android:title="layered icon" app:showAsAction="ifRoom"/> 

Em res/values/dimens adicione:

 <dimen name="counter_text_vertical_padding">2dp</dimen> 

Crie o arquivo CounterDrawable.java :

 package com.example.counters.counters; import android.content.Context; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.support.v4.content.ContextCompat; public class CounterDrawable extends Drawable { private Paint mBadgePaint; private Paint mTextPaint; private Rect mTxtRect = new Rect(); private String mCount = ""; private boolean mWillDraw; private Context mContext; public CounterDrawable(Context context) { mContext = context; float mTextSize = context.getResources() .getDimension(R.dimen.counter_text_size); mBadgePaint = new Paint(); mBadgePaint.setColor(ContextCompat.getColor(context.getApplicationContext(), R.color.counter_background_color)); mBadgePaint.setAntiAlias(true); mBadgePaint.setStyle(Paint.Style.FILL); mTextPaint = new Paint(); mTextPaint.setColor(ContextCompat.getColor(context.getApplicationContext(), R.color.counter_text_color)); mTextPaint.setTypeface(Typeface.DEFAULT); mTextPaint.setTextSize(mTextSize); mTextPaint.setAntiAlias(true); mTextPaint.setTextAlign(Paint.Align.CENTER); } @Override public void draw(Canvas canvas) { if (!mWillDraw) { return; } float radius = mContext.getResources() .getDimension(R.dimen.counter_badge_radius); float counterLeftMargin = mContext.getResources() .getDimension(R.dimen.counter_left_margin); float horizontalPadding = mContext.getResources() .getDimension(R.dimen.counter_text_horizontal_padding); float verticalPadding = mContext.getResources() .getDimension(R.dimen.counter_text_vertical_padding); mTextPaint.getTextBounds(mCount, 0, mCount.length(), mTxtRect); float textHeight = mTxtRect.bottom - mTxtRect.top; float textWidth = mTxtRect.right - mTxtRect.left; float badgeWidth = Math.max(textWidth + 2 * horizontalPadding, 2 * radius); float badgeHeight = Math.max(textHeight + 2 * verticalPadding, 2 * radius); canvas.drawCircle(counterLeftMargin + radius, radius, radius, mBadgePaint); canvas.drawCircle(counterLeftMargin + radius, badgeHeight - radius, radius, mBadgePaint); canvas.drawCircle(counterLeftMargin + badgeWidth - radius, badgeHeight - radius, radius, mBadgePaint); canvas.drawCircle(counterLeftMargin + badgeWidth - radius, radius, radius, mBadgePaint); canvas.drawRect(counterLeftMargin + radius, 0, counterLeftMargin + badgeWidth - radius, badgeHeight, mBadgePaint); canvas.drawRect(counterLeftMargin, radius, counterLeftMargin + badgeWidth, badgeHeight - radius, mBadgePaint); // for API 21 and more: //canvas.drawRoundRect(counterLeftMargin, 0, counterLeftMargin + badgeWidth, badgeHeight, radius, radius, mBadgePaint); canvas.drawText(mCount, counterLeftMargin + badgeWidth / 2, verticalPadding + textHeight, mTextPaint); } public void setCount(String count) { mCount = count; mWillDraw = !count.equalsIgnoreCase("0"); invalidateSelf(); } @Override public void setAlpha(int alpha) { // do nothing } @Override public void setColorFilter(ColorFilter cf) { // do nothing } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } } 

Esta classe renderizará o contador no canto superior direito do nosso ícone. A maneira mais fácil de desenhar um plano de fundo do contador é simplesmente desenhar um retângulo arredondado chamando canvas.drawRoundRect , mas esse método é adequado para versões de API acima do dia 21. Embora para versões anteriores da API, isso não seja particularmente difícil.

Em seguida, em nossa Activity adicione:

 private int mCounterValue2 = 0; private LayerDrawable mIcon2; private void initSecondCounter(Menu menu){ MenuItem menuItem = menu.findItem(R.id.action_counter_2); mIcon2 = (LayerDrawable) menuItem.getIcon(); updateSecondCounter(mCounterValue2); } private void updateSecondCounter(int newCounterValue) { CounterDrawable badge; Drawable reuse = mIcon2.findDrawableByLayerId(R.id.ic_counter); if (reuse != null && reuse instanceof CounterDrawable) { badge = (CounterDrawable) reuse; } else { badge = new CounterDrawable(this); } badge.setCount(String.valueOf(newCounterValue)); mIcon2.mutate(); mIcon2.setDrawableByLayerId(R.id.ic_counter, badge); } 

Adicione o código ao onOptionsItemSelected . Com base no código da primeira implementação, este método será semelhante a este:

 @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_counter_1: updateFirstCounter(mCounterValue1 + 1); return true; case R.id.action_counter_2: updateSecondCounter(++mCounterValue2); return true; default: return super.onOptionsItemSelected(item); } } 

Isso é tudo, a segunda implementação está pronta. Como na última vez, desliguei a atualização do contador clicando no ícone, mas ela pode ser inicializada de qualquer lugar chamando o método updateSecondCounter . Como você pode ver, desenhamos o balcão na tela com as mãos, mas você pode criar algo mais interessante - tudo depende da sua imaginação ou dos desejos do cliente.

Decisão três


Conceito


Para um item de menu, usamos não uma imagem, mas um elemento com marcação arbitrária. Em seguida, encontramos os componentes desse elemento e salvamos os links para eles.

Nesse caso, estamos interessados ​​nos ícones ImageView e TextView contador TextView , mas, na realidade, pode ser algo mais personalizado. Aperte imediatamente o processamento de clicar neste elemento. Isso deve ser feito, pois o método onOptionsItemSelected não onOptionsItemSelected chamado para elementos personalizados na barra de ferramentas.

Implementação


Criamos um arquivo de marcação badge_with_counter.xml em res/layouts :

 <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <RelativeLayout android:layout_width="@dimen/menu_item_size" android:layout_height="@dimen/menu_item_size"> <ImageView android:id="@+id/icon_badge" android:layout_width="@dimen/menu_item_icon_size" android:layout_height="@dimen/menu_item_icon_size" android:layout_centerInParent="true" android:scaleType="fitXY" android:src="@drawable/icon" /> <TextView android:id="@+id/counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignStart="@id/icon_badge" android:layout_alignTop="@+id/icon_badge" android:layout_gravity="center" android:layout_marginStart="@dimen/counter_left_margin" android:background="@drawable/counter_background" android:gravity="center" android:paddingLeft="@dimen/counter_text_horizontal_padding" android:paddingRight="@dimen/counter_text_horizontal_padding" android:text="99" android:textAppearance="@style/CounterText" /> </RelativeLayout> </FrameLayout> 

Em res/values/dimens adicione:

 <dimen name="menu_item_size">48dp</dimen> 

Adicione a res/menu/menu_main.xml :

 <item android:id="@+id/action_counter_3" app:actionLayout="@layout/badge_with_counter" android:title="existing action view" app:showAsAction="ifRoom"/> 

Em seguida, em nossa Activity adicione:

 private int mCounterValue3 = 0; private ImageView mIcon3; private TextView mCounterText3; private void initThirdCounter(Menu menu){ MenuItem counterItem = menu.findItem(R.id.action_counter_3); View counter = counterItem.getActionView(); mIcon3 = counter.findViewById(R.id.icon_badge); mCounterText3 = counter.findViewById(R.id.counter); counter.setOnClickListener(v -> onThirdCounterClick()); updateThirdCounter(mCounterValue3); } private void onThirdCounterClick(){ updateThirdCounter(++mCounterValue3); } private void updateThirdCounter(int newCounterValue) { if (mIcon3 == null || mCounterText3 == null) { return; } if (newCounterValue == 0) { mIcon3.setImageResource(R.drawable.icon); mCounterText3.setVisibility(View.GONE); } else { mIcon3.setImageResource(R.drawable.icon); mCounterText3.setVisibility(View.VISIBLE); mCounterText3.setText(String.valueOf(newCounterValue)); } } 

No onPrepareOptionsMenu adicione:

 initThirdCounter(menu); 

Agora, levando em consideração as alterações anteriores, este método se parece com o seguinte:

 @Override public boolean onPrepareOptionsMenu(final Menu menu) { // the second counter initSecondCounter(menu); // the third counter initThirdCounter(menu); return super.onPrepareOptionsMenu(menu); } 

Feito! Observe que, para o nosso elemento, fizemos a marcação, na qual indicamos independentemente todos os tamanhos e recuos necessários - nesse caso, o sistema não fará isso por nós.

Decisão quatro


Conceito


O mesmo que na versão anterior, mas aqui criamos e adicionamos nosso elemento diretamente do código.

Implementação


Em Activity adicione:

 private int mCounterValue4 = 0; private void addFourthCounter(Menu menu, Context context) { View counter = LayoutInflater.from(context) .inflate(R.layout.badge_with_counter, null); counter.setOnClickListener(v -> onFourthCounterClick()); mIcon4 = counter.findViewById(R.id.icon_badge); mCounterText4 = counter.findViewById(R.id.counter); MenuItem counterMenuItem = menu.add(context.getString(R.string.counter)); counterMenuItem.setActionView(counter); counterMenuItem.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS); updateFourthCounter(mCounterValue4); } private void onFourthCounterClick(){ updateFourthCounter(++mCounterValue4); } private void updateFourthCounter(int newCounterValue) { if (mIcon4 == null || mCounterText4 == null) { return; } if (newCounterValue == 0) { mIcon4.setImageResource(R.drawable.icon); mCounterText4.setVisibility(View.GONE); } else { mIcon4.setImageResource(R.drawable.icon); mCounterText4.setVisibility(View.VISIBLE); mCounterText4.setText(String.valueOf(newCounterValue)); } } 

Nesta opção, adicionar nosso item ao menu já deve ser feito no onCreateOptionsMenu

Com base nas alterações anteriores, esse método agora se parece com o seguinte:

 @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_main, menu); MenuItem menuItem = menu.findItem(R.id.action_counter_1); // the first counter menuItem.setIcon(LayoutToDrawableConverter.convertToImage(this, mCounterValue1, R.drawable.icon)); // the third counter addFourthCounter(menu, this); return true; } 

Feito!

Na minha opinião, as duas últimas soluções são as mais simples e elegantes, e também as mais curtas: simplesmente selecionamos o layout do elemento necessário e o colocamos na barra de ferramentas, e atualizamos o conteúdo como se estivesse usando uma Visualização regular.

Parece, por que simplesmente não devo descrever essa abordagem e não insistir nela? Há duas razões para isso:

  • primeiro, quero mostrar que um problema pode ter várias soluções;
  • segundo, cada uma das opções consideradas tem direito à vida.

Lembre-se de que escrevi que você pode tratar essas soluções não apenas como implementar um ícone com um contador, mas usá-las em algum elemento personalizado muito complexo e interessante para uma barra de ferramentas, para qual das soluções propostas será a mais adequada? Eu darei um exemplo

De todos os métodos discutidos, o mais controverso é o primeiro, uma vez que carrega o sistema bastante. Seu uso pode ser justificado no caso de termos a necessidade de ocultar os detalhes da formação do ícone e transferir a imagem já formada para a barra de ferramentas. No entanto, deve-se ter em mente que, com a atualização frequente do ícone dessa maneira, podemos infligir um sério golpe no desempenho.

O segundo método é adequado para nós quando você mesmo precisa desenhar algo na tela. A terceira e quarta implementações são as mais universais para tarefas clássicas: alterar o valor do campo de texto em vez de formar uma imagem separada será uma boa solução.

Quando se torna necessário implementar algum tipo de recurso gráfico complicado, normalmente digo a mim mesmo: “Não há nada impossível - a única questão é quanto tempo e esforço precisam ser gastos na implementação”.

Agora você tem várias opções para realizar a tarefa e, como você pode ver, precisa de muito pouco tempo e esforço para implementar cada opção.

Source: https://habr.com/ru/post/pt420459/


All Articles