📜 ⬆️ ⬇️

Icon with a counter in the upper toolbar: an example of the diversity of approaches to a single task


In every developer's life, there is a moment when, after seeing an interesting solution in another application, I want to implement it in my own. This is logical and should be quite simple. And surely caring people from the “corporation of good” wrote a guide on this or made an instructional video, where they show how to call a couple of necessary methods to achieve the desired result. It often happens that way.

But it happens in a completely different way: you see the realization of something in every second application, and when it comes to implementing the same in yourself, it turns out that there are still no easy solutions for it ...

It happened to me when it became necessary to add an icon with a counter to the top panel. I was very surprised when it turned out that to implement such a familiar and sought-after UI element there is no simple solution. But it happens, unfortunately. And I decided to turn to the knowledge of the world wide web. The question of placing an icon with a counter in the upper toolbar, as it turned out, was quite a matter of concern. After spending some time on the Internet, I found a lot of different solutions. In general, they are all workers and have the right to life. Moreover, the result of my research clearly shows how different ways you can approach the solution of problems in Android.
')
In this article I will talk about several implementations of the icon with the counter. Here are 4 examples. If you think a little wider, then we will talk about almost any custom element that we want to place in the upper toolbar. So, let's begin.

Solution one


Concept


Every time you need to draw or update the counter on the icon, you need to create a Drawable based on the markup file and draw it on the toolbar as an icon.

Implementation


Create a badge_with_counter_icon markup file 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> 

Here, the counter itself, we bind to the left edge of the icon and specify a fixed indent: this is necessary so that as the length of the text of the counter value increases, the main icon does not overlap more strongly — this is ugly.

In res/values/dimens add:

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

The size of the icon is in accordance with the material design guide.

Add to res/values/colors :

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

Add to res/values/styles :

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

Create in res/drawable/ resource counter_background.xml :

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

As an icon, take your picture, call it icon and put it into resources.

In res/menu create the menu_main.xml file:

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

Create a class that converts markup to 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); } } 

Next, in the Activity we need, we add:

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

Now, if you need to update the counter, call the updateFirstCounter method, passing the current value to it Here I hung up the increase in the counter value when I click on the icon. With other implementations I will do the same.

It is necessary to pay attention to the following: we form an image, which we then feed to the menu item - all necessary indents are formed automatically, we do not need to take them into account.

Solution two


Concept


In this implementation, we LayerList icon based on the layered element described in LayerList , in which at the right moment we draw the meter itself, leaving the icon unchanged.

Implementation


Hereinafter, I will gradually add resources and code for all implementations.

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> 

Add to res/menu/menu_main.xml :

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

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

Create a CounterDrawable.java file:

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

This class will be engaged in drawing the counter in the upper right corner of our icon. The easiest way to draw the counter background is to simply draw a rectangle with rounded corners by calling canvas.drawRoundRect , but this method is suitable for the API version above the 21st. Although for earlier versions of the API, this is not particularly difficult.

Further, in our Activity add:

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

Add the code to onOptionsItemSelected . Given the code for the first implementation, this method will look like this:

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

That's it, the second implementation is ready. Like last time, I hung the update of the counter by clicking on the icon, but it can be initialized from anywhere by calling the updateSecondCounter method. As you can see, we draw the counter on the canvas with our hands, but you can come up with something more interesting - it all depends on your imagination or on the wishes of the customer.

Third decision


Concept


For the menu item, we use not an image, but an element with arbitrary markup. Then we find the components of this element and save the links to them.

In this case, we are interested in the ImageView icons and the TextView counter, but in fact it could be something more custom. Immediately fasten the processing of clicking on this item. This must be done, since the onOptionsItemSelected method onOptionsItemSelected not called for custom elements in the toolbar.

Implementation


Create a badge_with_counter.xml markup file 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 add:

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

Add to 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"/> 

Further, in our Activity add:

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

Add to onPrepareOptionsMenu :

 initThirdCounter(menu); 

Now, with the previous changes, this method looks like this:

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

Done! Please note that for our element we took a markup in which we independently specified all the necessary dimensions and indents - in this case the system will not do this for us.

Fourth solution


Concept


The same as in the previous version, but here we create and add our element directly from the code.

Implementation


Add to Activity :

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

In this case, adding our item to the menu should be done in onCreateOptionsMenu

Taking into account the previous changes, this method now looks like this:

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

Done!

In my opinion, the last two solutions are the simplest and most elegant, and also the shortest: we simply select the element markup we need and throw it into the toolbar, and update the content as when working with a normal View.

It would seem, why I simply did not describe this approach and did not dwell on it? There are two reasons for this:


Remember, I wrote that you can treat these solutions not only as an implementation of an icon with a counter, but to use them in some very complex and interesting custom element for a toolbar, for which one of the proposed solutions will be the most appropriate? I will give an example.

Of all the methods considered, the most controversial is the first, since it loads the system quite heavily. Its use can be justified in the case when we have a requirement to hide the details of the formation of the icon and transfer to the toolbar the already formed image. However, it should be borne in mind that with frequent updates of the icon in this way, we can deal a serious blow to performance.

The second method will suit us when you need to draw something on the canvas yourself. The third and fourth implementations are the most universal for classical problems: changing the value of a text field instead of forming a separate image will be a perfectly successful solution.

When there is a need to implement some kind of difficult graphic feature, I usually say to myself: “Nothing is impossible - the only question is how much time and effort you need to spend on implementation.”

Now you have several options to achieve the task and, as you can see, you need very little time and energy to implement each option.

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


All Articles