Icône avec un compteur dans la barre d'outils supérieure: un exemple d'une variété d'approches pour une tâche


Dans la vie de chaque développeur, il y a un moment où, lorsque vous voyez une solution intéressante dans une application étrangère, vous voulez l'implémenter dans la vôtre. Ceci est logique et devrait être assez simple. Et sûrement, des gens attentionnés de la "corporation de la bonté" ont écrit une sorte de guide à ce sujet ou ont fait une vidéo de formation où il a été montré sur les doigts comment appeler quelques méthodes nécessaires pour atteindre le résultat souhaité. C'est souvent le cas.

Mais cela se produit d'une manière complètement différente: vous voyez la mise en œuvre de quelque chose dans chaque seconde application, et quand il s'agit de la mise en œuvre de la même chose à la maison - il s'avère qu'il n'y a toujours pas de solutions faciles pour cela ...

Cela m'est arrivé quand il est devenu nécessaire d'ajouter une icône avec un compteur au panneau supérieur. J'ai été très surpris quand il s'est avéré qu'il n'y avait pas de solution simple pour implémenter un élément d'interface utilisateur aussi familier et exigé. Mais cela arrive, malheureusement. Et j'ai décidé de me tourner vers la connaissance du réseau mondial. Le problème de placer une icône avec un compteur dans la barre d'outils supérieure, comme il s'est avéré, en a inquiété beaucoup. Après avoir passé un peu de temps sur Internet, j'ai trouvé beaucoup de solutions différentes. En général, ils sont tous des travailleurs et ont droit à la vie. De plus, le résultat de mes recherches montre clairement comment aborder la solution des tâches dans Android de différentes manières.

Dans cet article, je parlerai de plusieurs implémentations de l'icône de compteur. Voici 4 exemples. Si vous pensez un peu plus largement, nous parlerons de presque tous les éléments personnalisés que nous voulons placer dans la barre d'outils supérieure. Commençons donc.

Décision un


Concept


Chaque fois que vous devez dessiner ou mettre à jour le compteur sur l'icône, vous devez créer Drawable fonction du fichier de balisage et le dessiner sur la barre d'outils sous forme d'icône.

Implémentation


Créez un fichier de balisage badge_with_counter_icon dans 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> 

Ici, nous attachons le compteur lui-même au bord gauche de l'icône et indiquons un retrait fixe: cela est nécessaire pour que lorsque la longueur du texte du compteur augmente, l'icône principale ne se chevauche pas plus fortement - c'est moche.

En res/values/dimens ajouter:

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

Taille de l'icône selon le guide Material Design .

Dans res/values/colors ajouter:

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

Dans res/values/styles ajoutez:

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

Créez res/drawable/ dans 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> 

En tant qu'icône, prenez votre photo, appelez-la icon et insérez des ressources.

Dans res/menu créez le fichier 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> 

Créez une classe qui convertit le balisage en 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); } } 

Ensuite, dans l' Activity nous avons besoin, ajoutez:

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

Maintenant, s'il est nécessaire de mettre à jour le compteur, nous appelons la méthode updateFirstCounter , en lui passant la valeur réelle. Ici, j'ai affiché une augmentation de la valeur du compteur lorsque je clique sur l'icône. Avec d'autres implémentations, je ferai de même.

Il est nécessaire de faire attention aux éléments suivants: nous formons une image, que nous alimentons ensuite à l'élément de menu - tous les retraits nécessaires sont générés automatiquement, nous n'avons pas besoin de les prendre en compte.

Deuxième décision


Concept


Dans cette implémentation, nous LayerList icône basée sur l'élément multicouche décrit dans LayerList , dans laquelle au bon moment nous LayerList le compteur lui-même, en laissant l'icône inchangée.

Implémentation


Ci-après, j'ajouterai progressivement des ressources et du code pour toutes les implémentations.

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

Dans res/menu/menu_main.xml ajoutez:

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

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

Créez le fichier 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; } } 

Cette classe affichera le compteur dans le coin supérieur droit de notre icône. Le moyen le plus simple de dessiner un arrière-plan de compteur est de simplement dessiner un rectangle arrondi en appelant canvas.drawRoundRect , mais cette méthode convient aux versions d'API supérieures au 21. Bien que pour les versions antérieures de l'API, ce n'est pas particulièrement difficile.

Ensuite, dans notre Activity ajoutez:

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

Ajoutez le code à onOptionsItemSelected . Basée sur le code de la première implémentation, cette méthode ressemblera à ceci:

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

C'est tout, la deuxième implémentation est prête. Comme la dernière fois, j'ai raccroché la mise à jour du compteur en cliquant sur l'icône, mais elle peut être initialisée de n'importe où en appelant la méthode updateSecondCounter . Comme vous pouvez le voir, nous dessinons le comptoir sur la toile avec nos mains, mais vous pouvez trouver quelque chose de plus intéressant - tout dépend de votre imagination ou des souhaits du client.

Décision trois


Concept


Pour un élément de menu, nous utilisons non pas une image, mais un élément avec un balisage arbitraire. Ensuite, nous trouvons les composants de cet élément et enregistrons des liens vers eux.

Dans ce cas, nous nous intéressons aux icônes TextView et TextView compteur TextView , mais en réalité, cela peut être quelque chose de plus personnalisé. Accélérez immédiatement le traitement du clic sur cet élément. Cela doit être fait, car la méthode onOptionsItemSelected pas appelée pour les éléments personnalisés dans la barre d'outils.

Implémentation


Nous créons un fichier de balisage badge_with_counter.xml dans 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 ajouter:

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

Ajoutez à 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"/> 

Ensuite, dans notre Activity ajoutez:

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

Dans onPrepareOptionsMenu ajoutez:

 initThirdCounter(menu); 

Maintenant, compte tenu des modifications précédentes, cette méthode ressemble à ceci:

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

C'est fait! Veuillez noter que pour notre élément, nous avons pris le balisage, dans lequel nous avons indépendamment indiqué toutes les tailles et retraits nécessaires - dans ce cas, le système ne le fera pas pour nous.

Décision quatre


Concept


Identique à la version précédente, mais ici nous créons et ajoutons notre élément directement à partir du code.

Implémentation


Dans Activity ajoutez:

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

Dans cette option, l'ajout de notre élément au menu doit être fait déjà dans onCreateOptionsMenu

Sur la base des modifications précédentes, cette méthode ressemble maintenant à ceci:

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

C'est fait!

À mon avis, les deux dernières solutions sont les plus simples et les plus élégantes, ainsi que les plus courtes: nous sélectionnons simplement la disposition des éléments dont nous avons besoin et la déposons dans la barre d'outils, et mettons à jour le contenu comme si vous utilisiez une vue standard.

Il semblerait, pourquoi devrais-je simplement ne pas décrire cette approche et ne pas m'y attarder? Il y a deux raisons à cela:

  • tout d'abord, je veux montrer qu'un problème peut avoir plusieurs solutions;
  • deuxièmement, chacune des options envisagées a droit à la vie.

Rappelez-vous, j'ai écrit que vous pouvez traiter ces solutions non seulement comme implémentant une icône avec un compteur, mais les utiliser dans un élément personnalisé très complexe et intéressant pour une barre d'outils, pour laquelle l'une des solutions proposées sera la plus appropriée? Je vais vous donner un exemple.

De toutes les méthodes discutées, la plus controversée est la première, car elle charge le système assez lourdement. Son utilisation peut être justifiée dans le cas où nous devons masquer les détails de la formation de l'icône et transférer l'image déjà formée dans la barre d'outils. Cependant, il convient de garder à l'esprit qu'avec une mise à jour fréquente de l'icône de cette manière, nous pouvons nuire gravement aux performances.

La deuxième méthode nous convient lorsque vous devez dessiner vous-même quelque chose sur la toile. Les troisième et quatrième implémentations sont les plus universelles pour les tâches classiques: changer la valeur du champ de texte au lieu de former une image séparée sera une bonne solution.

Quand il devient nécessaire de mettre en œuvre une sorte de fonctionnalité graphique compliquée, je me dis généralement: "Il n'y a rien d'impossible - la seule question est de savoir combien de temps et d'efforts doivent être consacrés à la mise en œuvre".

Vous avez maintenant plusieurs options pour réaliser la tâche et, comme vous pouvez le voir, vous avez besoin de très peu de temps et d'efforts pour implémenter chaque option.

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


All Articles