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