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


    In the life of every developer, 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 pretty simple. And surely caring people from the “corporation of good” wrote a guide on this or made an instructional video, where they show on their fingers 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 issue 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 counter icon. Here are 4 examples. If you think a little wider, then we will focus on almost any custom element that we want to place in the upper toolbar. So, let's begin.

    Solution one


    Concept


    Each time you need to draw or update the counter on the icon, you need to create it Drawableon the basis of the markup file and draw it on the toolbar as an icon.

    Implementation


    Create res/layoutsa markup file badge_with_counter_icon:

    <?xml version="1.0" encoding="utf-8"?><RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="@dimen/menu_item_icon_size"
      ><ImageViewandroid: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"/><TextViewandroid: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/dimensadd:

    <dimenname="menu_item_icon_size">24dp</dimen><dimenname="counter_left_margin">14dp</dimen><dimenname="counter_badge_radius">6dp</dimen><dimenname="counter_text_size">9sp</dimen><dimenname="counter_text_horizontal_padding">4dp</dimen>

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

    In res/values/colorsadd:

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

    In res/values/stylesadd:

    <stylename="CounterText"><itemname="android:fontFamily">sans-serif</item><itemname="android:textSize">@dimen/counter_text_size</item><itemname="android:textColor">@color/counter_text_color</item><itemname="android:textStyle">normal</item></style>

    Create a res/drawable/resource counter_background.xml:

    <?xml version="1.0" encoding="utf-8"?><shapexmlns:android="http://schemas.android.com/apk/res/android"android:shape="rectangle"><solidandroid:color="@color/counter_background_color"/><cornersandroid:radius="@dimen/counter_badge_radius"/></shape>

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

    In res/menucreating the file menu_main.xml:

    <?xml version="1.0" encoding="utf-8"?><menuxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"><itemandroid: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;
    publicclassLayoutToDrawableConverter{
      publicstatic 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);
         returnnew BitmapDrawable(context.getResources(), bitmap);
      }
    }
    

    Next, in the right we Activityadd:

    privateint mCounterValue1 = 0;
      @OverridepublicbooleanonCreateOptionsMenu(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));
            returntrue;
    }
    @OverridepublicbooleanonOptionsItemSelected(final MenuItem item){
      switch (item.getItemId()) {
         case R.id.action_counter_1:
            updateFirstCounter(mCounterValue1 + 1);
            returntrue;
         default:
            returnsuper.onOptionsItemSelected(item);
      }
    }
    privatevoidupdateFirstCounter(int newCounterValue){
        mCountrerValue1 = newCounterValue;
        invalidateOptionsMenu();
    }
    

    Now, if you need to update the counter, we call the method updateFirstCounter, passing in it the current value. Here I hung the increase in the counter value when I clicked 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 create an icon based on the layered element described in LayerList, in which at the right time we draw the counter itself directly, 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-listxmlns:android="http://schemas.android.com/apk/res/android"><itemandroid:drawable="@drawable/icon"android:gravity="center" /><itemandroid:id="@+id/ic_counter"android:drawable="@android:color/transparent" /></layer-list>

    In res/menu/menu_main.xmladd:

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

    In res/values/dimensadd:

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

    Create a file 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;
    publicclassCounterDrawableextendsDrawable{
      private Paint mBadgePaint;
      private Paint mTextPaint;
      private Rect mTxtRect = new Rect();
      private String mCount = "";
      privateboolean mWillDraw;
      private Context mContext;
      publicCounterDrawable(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);
      }
      @Overridepublicvoiddraw(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);
      }
      publicvoidsetCount(String count){
         mCount = count;
         mWillDraw = !count.equalsIgnoreCase("0");
         invalidateSelf();
      }
      @OverridepublicvoidsetAlpha(int alpha){
         // do nothing
      }
      @OverridepublicvoidsetColorFilter(ColorFilter cf){
         // do nothing
      }
      @OverridepublicintgetOpacity(){
         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, invoking 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.

    Next, in our Activityadd:

    privateint mCounterValue2 = 0;
    private LayerDrawable mIcon2;
    privatevoidinitSecondCounter(Menu menu){
      MenuItem menuItem = menu.findItem(R.id.action_counter_2);
      mIcon2 = (LayerDrawable) menuItem.getIcon();
      updateSecondCounter(mCounterValue2);
    }
    privatevoidupdateSecondCounter(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 code to onOptionsItemSelected. Given the code for the first implementation, this method will look like this:

    @OverridepublicbooleanonOptionsItemSelected(final MenuItem item){
      switch (item.getItemId()) {
         case R.id.action_counter_1:
            updateFirstCounter(mCounterValue1 + 1);
            returntrue;
         case R.id.action_counter_2:
            updateSecondCounter(++mCounterValue2);
            returntrue;
         default:
            returnsuper.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 method updateSecondCounter. 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 ImageViewicons and the TextViewcounter, but in fact it may be something more custom. Immediately fasten the processing of clicking on this item. This must be done, since the method is onOptionsItemSelectednot called for custom elements in the toolbar .

    Implementation


    Create res/layoutsa markup file badge_with_counter.xml:

    <?xml version="1.0" encoding="utf-8"?><FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="wrap_content"android:layout_height="wrap_content"><RelativeLayoutandroid:layout_width="@dimen/menu_item_size"android:layout_height="@dimen/menu_item_size"><ImageViewandroid: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" /><TextViewandroid: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/dimensadd:

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

    Add to res/menu/menu_main.xml:

    <itemandroid:id="@+id/action_counter_3"app:actionLayout="@layout/badge_with_counter"android:title="existing action view"app:showAsAction="ifRoom"/>

    Next, in our Activityadd:

    privateint mCounterValue3 = 0;
    private ImageView mIcon3;
    private TextView mCounterText3;
    privatevoidinitThirdCounter(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);
    }
    privatevoidonThirdCounterClick(){
      updateThirdCounter(++mCounterValue3);
    }
    privatevoidupdateThirdCounter(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));
      }
    }
    

    In onPrepareOptionsMenuadd:

    initThirdCounter(menu);

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

    @OverridepublicbooleanonPrepareOptionsMenu(final Menu menu){
      // the second counter
      initSecondCounter(menu);
      // the third counter
      initThirdCounter(menu);
      returnsuper.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


    In Activityadd:

    privateint mCounterValue4 = 0;
    privatevoidaddFourthCounter(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);
    }
    privatevoidonFourthCounterClick(){
      updateFourthCounter(++mCounterValue4);
    }
    privatevoidupdateFourthCounter(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 variant, the addition of our element to the menu should be done already in onCreateOptionsMenu

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

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

    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:

    • First, I want to show that one problem can have several solutions;
    • secondly, each of the options considered has the right to life.

    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 considered methods, 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 tasks: 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 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.

    Also popular now: