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);
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) {
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);
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.