Symbol mit einem Zähler in der oberen Symbolleiste: Ein Beispiel für verschiedene Ansätze für eine Aufgabe


Im Leben eines jeden Entwicklers gibt es einen Moment, in dem Sie eine interessante Lösung in einer fremden Anwendung sehen und in Ihrer eigenen implementieren möchten. Dies ist logisch und sollte recht einfach sein. Und sicherlich haben fürsorgliche Menschen von der „Corporation of Goodness“ eine Art Leitfaden zu diesem Thema geschrieben oder ein Schulungsvideo erstellt, das an den Fingern zeigt, wie man einige der notwendigen Methoden aufruft, um das gewünschte Ergebnis zu erzielen. Dies ist häufig der Fall.

Aber es passiert ganz anders: Sie sehen die Implementierung von etwas in jeder zweiten Anwendung, und wenn es um die Implementierung derselben Sache zu Hause geht, stellt sich heraus, dass es dafür immer noch keine einfachen Lösungen gibt ...

Dies passierte mir, als es notwendig wurde, ein Symbol mit einem Zähler im oberen Bereich hinzuzufügen. Ich war sehr überrascht, als sich herausstellte, dass es keine einfache Lösung gibt, um ein so bekanntes und gefragtes UI-Element zu implementieren. Aber es passiert leider. Und ich beschloss, mich dem Wissen des weltweiten Netzwerks zuzuwenden. Wie sich herausstellte, hat das Problem, ein Symbol mit einem Zähler in der oberen Symbolleiste zu platzieren, einige beunruhigt. Nachdem ich einige Zeit im Internet verbracht hatte, fand ich viele verschiedene Lösungen. Im Allgemeinen sind sie alle Arbeiter und haben das Recht auf Leben. Darüber hinaus zeigt das Ergebnis meiner Forschung deutlich, wie man die Lösung von Aufgaben in Android auf unterschiedliche Weise angeht.

In diesem Artikel werde ich über verschiedene Implementierungen des Zählersymbols sprechen. Hier sind 4 Beispiele. Wenn Sie etwas breiter denken, werden wir über fast jedes benutzerdefinierte Element sprechen, das wir in der oberen Symbolleiste platzieren möchten. Also fangen wir an.

Entscheidung eins


Konzept


Jedes Mal, wenn Sie den Zähler auf dem Symbol zeichnen oder aktualisieren müssen, müssen Sie Drawable basierend auf der Markup-Datei erstellen und in der Symbolleiste als Symbol zeichnen.

Implementierung


Erstellen Sie eine badge_with_counter_icon Markup-Datei in 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> 

Hier befestigen wir den Zähler selbst am linken Rand des Symbols und geben einen festen Einzug an: Dies ist erforderlich, damit sich das Hauptsymbol bei zunehmender Textlänge des Zählers nicht stärker überlappt - dies ist hässlich.

In res/values/dimens hinzufügen:

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

Symbolgröße gemäß Material Design Guide.

In res/values/colors hinzufügen:

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

In res/values/styles hinzufügen:

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

Erstellen Sie res/drawable/ in 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> 

Nehmen Sie als Symbol Ihr Bild auf, nennen Sie es icon und geben Sie Ressourcen ein.

Erstellen menu_main.xml in res/menu die Datei 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> 

Erstellen Sie eine Klasse, die Markup in 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); } } 

Als nächstes fügen Sie in der Activity wir benötigen, Folgendes hinzu:

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

Wenn nun der Zähler aktualisiert werden muss, rufen wir die updateFirstCounter Methode auf und übergeben den tatsächlichen Wert an ihn. Hier habe ich eine Erhöhung des Zählerwerts gepostet, wenn ich auf das Symbol geklickt habe. Mit anderen Implementierungen werde ich das gleiche tun.

Folgendes ist zu beachten: Wir bilden ein Bild, das wir dann dem Menüpunkt zuführen - alle notwendigen Einrückungen werden automatisch generiert, wir müssen sie nicht berücksichtigen.

Zweite Entscheidung


Konzept


In dieser Implementierung LayerList wir LayerList Symbol basierend auf dem in LayerList beschriebenen mehrschichtigen Element, in dem wir im richtigen Moment den Zähler selbst zeichnen und das Symbol unverändert lassen.

Implementierung


Im Folgenden werde ich schrittweise Ressourcen und Code für alle Implementierungen hinzufügen.

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

res/menu/menu_main.xml Sie in res/menu/menu_main.xml hinzu:

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

In res/values/dimens hinzufügen:

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

Erstellen Sie die Datei 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; } } 

Diese Klasse rendert den Zähler in der oberen rechten Ecke unseres Symbols. Der einfachste Weg, einen canvas.drawRoundRect zu zeichnen, besteht darin, einfach ein abgerundetes Rechteck durch Aufrufen von canvas.drawRoundRect zu zeichnen. Diese Methode eignet sich jedoch für API-Versionen über dem 21 canvas.drawRoundRect Obwohl dies für frühere Versionen der API nicht besonders schwierig ist.

Fügen Sie als Nächstes in unserer Activity hinzu:

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

Fügen Sie den Code zu onOptionsItemSelected . Basierend auf dem Code für die erste Implementierung sieht diese Methode folgendermaßen aus:

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

Das ist alles, die zweite Implementierung ist fertig. Wie beim letzten Mal habe ich das Zähler-Update durch Klicken auf das Symbol aufgehängt, aber es kann von überall durch Aufrufen der updateSecondCounter Methode updateSecondCounter werden. Wie Sie sehen, zeichnen wir die Theke mit unseren Händen auf die Leinwand, aber Sie können sich etwas Interessanteres einfallen lassen - alles hängt von Ihrer Vorstellungskraft oder den Wünschen des Kunden ab.

Entscheidung drei


Konzept


Für einen Menüpunkt verwenden wir kein Bild, sondern ein Element mit beliebigem Markup. Dann finden wir die Komponenten dieses Elements und speichern Links zu ihnen.

In diesem Fall interessieren uns ImageView Symbole und der TextView Zähler, aber in Wirklichkeit kann es sich um etwas Benutzerdefinierteres handeln. Befestigen Sie sofort die Verarbeitung des Klickens auf dieses Element. Dies muss erfolgen, da die Methode onOptionsItemSelected nicht für benutzerdefinierte Elemente in der Symbolleiste aufgerufen wird.

Implementierung


Wir erstellen eine Markup-Datei badge_with_counter.xml in 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> 

In res/values/dimens hinzufügen:

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

Zu 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"/> 

Fügen Sie als Nächstes in unserer Activity hinzu:

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

onPrepareOptionsMenu Sie im onPrepareOptionsMenu hinzu:

 initThirdCounter(menu); 

Unter Berücksichtigung früherer Änderungen sieht diese Methode folgendermaßen aus:

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

Fertig! Bitte beachten Sie, dass wir für unser Element das Markup verwendet haben, in dem wir unabhängig alle erforderlichen Größen und Einrückungen angegeben haben. In diesem Fall wird das System dies nicht für uns tun.

Entscheidung vier


Konzept


Das gleiche wie in der vorherigen Version, aber hier erstellen und fügen wir unser Element direkt aus dem Code hinzu.

Implementierung


Activity in Activity hinzu:

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

Bei dieser Option muss das Hinzufügen unseres Elements zum Menü bereits in onCreateOptionsMenu

Basierend auf früheren Änderungen sieht diese Methode nun folgendermaßen aus:

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

Fertig!

Meiner Meinung nach sind die letzten beiden Lösungen die einfachsten und elegantesten sowie die kürzesten: Wir wählen einfach das benötigte Elementlayout aus, legen es in der Symbolleiste ab und aktualisieren den Inhalt wie in einer normalen Ansicht.

Es scheint, warum sollte ich diesen Ansatz einfach nicht beschreiben und nicht darauf eingehen? Dafür gibt es zwei Gründe:

  • Zunächst möchte ich zeigen, dass ein Problem mehrere Lösungen haben kann.
  • zweitens hat jede der in Betracht gezogenen Optionen das Recht auf Leben.

Denken Sie daran, ich habe geschrieben, dass Sie diese Lösungen nicht nur als Implementierung eines Symbols mit einem Zähler behandeln können, sondern sie auch in einem sehr komplexen und interessanten benutzerdefinierten Element für eine Symbolleiste verwenden können, für welche der vorgeschlagenen Lösungen am besten geeignet ist. Ich werde ein Beispiel geben.

Von allen diskutierten Methoden ist die erste die umstrittenste, da sie das System ziemlich stark belastet. Die Verwendung kann gerechtfertigt sein, wenn die Details der Symbolbildung ausgeblendet und das bereits erstellte Bild in die Symbolleiste übertragen werden müssen. Es sollte jedoch berücksichtigt werden, dass wir durch häufiges Aktualisieren des Symbols auf diese Weise die Leistung erheblich beeinträchtigen können.

Die zweite Methode eignet sich für uns, wenn Sie selbst etwas auf die Leinwand zeichnen müssen. Die dritte und vierte Implementierung sind die universellsten für klassische Aufgaben: Eine Änderung des Werts des Textfelds anstelle eines separaten Bildes ist eine gute Lösung.

Wenn es notwendig wird, eine komplizierte Grafikfunktion zu implementieren, sage ich mir normalerweise: „Es ist nichts unmöglich - die einzige Frage ist, wie viel Zeit und Mühe für die Implementierung aufgewendet werden muss.“

Jetzt haben Sie mehrere Optionen, um die Aufgabe zu erfüllen, und wie Sie sehen, benötigen Sie nur sehr wenig Zeit und Mühe, um jede Option zu implementieren.

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


All Articles