Icono con un contador en la barra de herramientas superior: un ejemplo de una variedad de enfoques para una tarea


En la vida de cada desarrollador, hay un momento en que, cuando ve una solución interesante en una aplicación extranjera, desea implementarla en la suya. Esto es lógico y debería ser bastante simple. Y seguramente personas atentas de la "corporación de la bondad" escribieron algún tipo de guía sobre esto o hicieron un video de capacitación donde se mostró con los dedos cómo llamar a un par de los métodos necesarios para lograr el resultado deseado. Este es a menudo el caso.

Pero sucede de una manera completamente diferente: ves la implementación de algo en cada segunda aplicación, y cuando se trata de la implementación de la misma en casa, resulta que todavía no hay soluciones fáciles para esto ...

Esto me sucedió cuando fue necesario agregar un ícono con un contador en el panel superior. Me sorprendió mucho cuando resultó que no existe una solución simple para implementar un elemento de interfaz de usuario tan familiar y exigente. Pero sucede, desafortunadamente. Y decidí recurrir al conocimiento de la red mundial. Resultó que la cuestión de colocar un icono con un contador en la barra de herramientas superior preocupaba a muchos. Después de pasar un tiempo en Internet, encontré muchas soluciones diferentes. En general, todos son trabajadores y tienen derecho a la vida. Además, el resultado de mi investigación muestra claramente cómo abordar la solución de tareas en Android de diferentes maneras.

En este artículo hablaré sobre varias implementaciones del icono del contador. Aquí hay 4 ejemplos. Si piensa un poco más ampliamente, hablaremos de casi cualquier elemento personalizado que queramos colocar en la barra de herramientas superior. Entonces comencemos.

Decisión uno


Concepto


Cada vez que necesite dibujar o actualizar el contador en el icono, debe crear Drawable función del archivo de marcado y dibujarlo en la barra de herramientas como un icono.

Implementación


Cree un archivo de marcado badge_with_counter_icon en 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> 

Aquí adjuntamos el contador al borde izquierdo del icono e indicamos una sangría fija: esto es necesario para que cuando la longitud del texto del contador aumente, el icono principal no se superponga con más fuerza; esto es feo.

En res/values/dimens agregue:

 <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> 

Tamaño de icono según la guía de diseño de materiales .

En res/values/colors agregue:

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

En res/values/styles agregue:

 <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> 

Cree res/drawable/ en 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 ícono, toma tu foto, llámalo icon y agrega recursos.

En res/menu cree el archivo 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> 

Cree una clase que convierta el marcado a 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); } } 

A continuación, en la Activity que necesitamos, agregue:

  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(); } 

Ahora, si es necesario actualizar el contador, llamamos al método updateFirstCounter , pasándole el valor real. Aquí publiqué un aumento en el valor del contador cuando hago clic en el icono. Con otras implementaciones haré lo mismo.

Es necesario prestar atención a lo siguiente: formamos una imagen, que luego alimentamos al elemento del menú: todos los sangrados necesarios se generan automáticamente, no necesitamos tenerlos en cuenta.

Segunda decisión


Concepto


En esta implementación, LayerList ícono basado en el elemento multicapa descrito en LayerList , en el que en el momento correcto dibujamos el contador en sí, dejando el ícono sin cambios.

Implementación


De aquí en adelante, iré agregando gradualmente recursos y código para todas las implementaciones.

En res/drawable/ ic_layered_counter_icon.xml 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> 

En res/menu/menu_main.xml agregue:

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

En res/values/dimens agregue:

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

Cree el archivo 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 clase mostrará el contador en la esquina superior derecha de nuestro icono. La forma más fácil de dibujar un fondo de contador es simplemente dibujar un rectángulo redondeado llamando a canvas.drawRoundRect , pero este método es adecuado para versiones de API superiores al 21. Aunque para versiones anteriores de la API, esto no es particularmente difícil.

A continuación, en nuestra Activity agregue:

 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); } 

Agregue el código a onOptionsItemSelected . Basado en el código para la primera implementación, este método se verá así:

 @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); } } 

Eso es todo, la segunda implementación está lista. Como la última vez, colgué la actualización del contador haciendo clic en el icono, pero se puede inicializar desde cualquier lugar llamando al método updateSecondCounter . Como puede ver, dibujamos el mostrador en el lienzo con nuestras manos, pero puede llegar a algo más interesante: todo depende de su imaginación o de los deseos del cliente.

Decisión tres


Concepto


Para un elemento de menú, no utilizamos una imagen, sino un elemento con marcado arbitrario. Luego encontramos los componentes de este elemento y guardamos enlaces a ellos.

En este caso, estamos interesados ​​en los iconos de ImageView y en el contador de TextView , pero en realidad puede ser algo más personalizado. Fije inmediatamente el procesamiento de hacer clic en este elemento. Esto debe hacerse, ya que el método onOptionsItemSelected no se llama para elementos personalizados en la barra de herramientas.

Implementación


Creamos un archivo de marcado badge_with_counter.xml en 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> 

En res/values/dimens agregue:

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

Añadir 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"/> 

A continuación, en nuestra Activity agregue:

 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)); } } 

En onPrepareOptionsMenu agregue:

 initThirdCounter(menu); 

Ahora, teniendo en cuenta los cambios anteriores, este método se ve así:

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

Hecho Tenga en cuenta que para nuestro elemento tomamos el marcado, en el que indicamos independientemente todos los tamaños y sangrías necesarios; en este caso, el sistema no hará esto por nosotros.

Decisión cuatro


Concepto


Lo mismo que en la versión anterior, pero aquí creamos y agregamos nuestro elemento directamente desde el código.

Implementación


En Activity agregue:

 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)); } } 

En esta opción, agregar nuestro elemento al menú ya debe hacerse en onCreateOptionsMenu

Según los cambios anteriores, este método ahora se ve así:

 @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; } 

Hecho

En mi opinión, las dos últimas soluciones son las más simples y elegantes, y también las más cortas: simplemente seleccionamos el diseño del elemento que necesitamos y lo colocamos en la barra de herramientas, y actualizamos el contenido como si estuviera usando una Vista normal.

Parecería, ¿por qué simplemente no describiría este enfoque y no me detendría en él? Hay dos razones para esto:

  • en primer lugar, quiero mostrar que un problema puede tener varias soluciones;
  • En segundo lugar, cada una de las opciones consideradas tiene derecho a la vida.

Recuerde, escribí que puede tratar estas soluciones no solo como implementar un ícono con un contador, sino también usarlas en algún elemento personalizado muy complejo e interesante para una barra de herramientas, ¿para cuál de las soluciones propuestas sería la más adecuada? Daré un ejemplo.

De todos los métodos discutidos, el más controvertido es el primero, ya que carga el sistema con bastante fuerza. Su uso puede justificarse en el caso de que tengamos que ocultar los detalles de la formación del icono y transferir la imagen ya formada a la barra de herramientas. Sin embargo, debe tenerse en cuenta que con la actualización frecuente del icono de esta manera, podemos infligir un duro golpe al rendimiento.

El segundo método es adecuado para nosotros cuando necesita dibujar algo en el lienzo usted mismo. Las implementaciones tercera y cuarta son las más universales para las tareas clásicas: cambiar el valor del campo de texto en lugar de formar una imagen separada será una buena solución.

Cuando se hace necesario implementar algún tipo de característica gráfica complicada, generalmente me digo a mí mismo: "No hay nada imposible: la única pregunta es cuánto tiempo y esfuerzo se debe dedicar a la implementación".

Ahora tiene varias opciones para lograr la tarea y, como puede ver, necesita muy poco tiempo y esfuerzo para implementar cada opción.

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


All Articles