Search code examples
androidandroid-actionbarandroid-support-libraryshapedrawable

ActionBar item is not showing when a ShapeDrawable is set as it's icon


I am working on a simple project and I want to avoid the PNG images as much as possible. I need a '+' (Add) button and I created it using the ShapeDrawable with the given code.

res/drawable/plus_icon_drawable.xml

<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="line">
            <stroke android:color="#000"
                android:width="3dp"/>
        </shape>
    </item>
    <item>
        <rotate
            android:fromDegrees="90"
            android:toDegrees="90">
            <shape android:shape="line">
                <stroke android:color="#000"
                    android:width="3dp"/>
            </shape>
        </rotate>
    </item>
</layer-list>

This code works fine when I add it as a background of Button or an ImageButton but when I add this as an icon of an ActionBar item, the item is not getting displayed.

When I set a PNG image as the icon, it works fine. Is there any restriction for the ShapeDrawables which blocks it from getting rendered on ActionBar?

This is my ActiobBar code

res/menu/action_my_actions.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/action_some_id"
        android:icon="@drawable/plus_icon_drawable"
        android:title="My Action"
        android:showAsAction="always"/>
</menu>

Update (08 July 2014): Tested with both Native and AppCompat. Actually the item is present there but the ShapeDrawable image is not getting rendered. The action item responds to clicks.

What am I missing here?


Solution

  • Is there any restriction for the ShapeDrawables which blocks it from getting rendered on ActionBar?

    Nothing like that. Its just how things are. Here's why you're seeing this:

    Layer/ShapeDrawables don't have a default size. In other words, its perfectly fine for a ShapeDrawable to be of zero width and height. The stroke-width parameter does not dictate anything either.

    This code works fine when I add it as a background of Button or an ImageButton but when I add this as an icon of an ActionBar item, the item is not getting displayed.

    Yes, of course. ImageButtons/Buttons have a concept of padding/ min-width/min-height. These allow the ShapeDrawable to have some room to breathe. For example, a Button styled with Widget.Holo.Button will have the minWidth and minHeight set:

    <item name="android:minHeight">48dip</item>
    <item name="android:minWidth">64dip</item>
    

    A valid test here would be:

    define an ImageView as follows:

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    
        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:src="@drawable/add" />
    
    </RelativeLayout>
    

    The ImageView has no sizing guidelines in the definition above - we're using wrap_content. Nothing will be displayed. On the other hand, if you use a png drawable, it itself defines the size. In the example above, using a explicit size (say 50dp) will make the ShapeDrawable visible.

    This is how ActionMenuItemView sets the drawable:

    public void setIcon(Drawable icon) {
        mIcon = icon;
        if (icon != null) {
    
            // Zero in case you don't define the <size /> 
            int width = icon.getIntrinsicWidth();
            int height = icon.getIntrinsicHeight();
            if (width > mMaxIconSize) {
                final float scale = (float) mMaxIconSize / width;
                width = mMaxIconSize;
                height *= scale;
            }
            if (height > mMaxIconSize) {
                final float scale = (float) mMaxIconSize / height;
                height = mMaxIconSize;
                width *= scale;
            }
            icon.setBounds(0, 0, width, height);
        }
        setCompoundDrawables(icon, null, null, null);
    
        updateTextButtonVisibility();
    }
    

    Snippet from ShapeDrawable#inflate(...) method:

     setIntrinsicWidth((int)
                a.getDimension(com.android.internal.R.styleable.ShapeDrawable_width, 0f));
     setIntrinsicHeight((int)
                a.getDimension(com.android.internal.R.styleable.ShapeDrawable_height, 0f));
    

    Without ShapeDrawable_width and ShapeDrawable_height defined, the intrinsic width and height will be set to zero. This means, the statement icon.setBounds(0, 0, width, height); (from setIcon(Drawable) method) is in fact => icon.setBounds(0, 0, 0, 0);.

    So, it seems that we're indeed dealing with ShapeDrawables that don't have size info.

    A possible fix:

    <shape android:shape="line">
        <size android:width="?android:attr/actionBarSize"
              android:height="?android:attr/actionBarSize"/>
        <stroke android:color="#000"
                android:width="3dp"/>
    </shape>
    

    But this won't work - for some reason ?android:attr/actionBarSize is not considered a dimension. This is odd because ?android:attr/actionBarSize works fine when supplied to android:layout_width in case of layouts.

    Workaround would be to define a new dimension, say actionBarSizeDp and set it up:

    In res/values/dimens.xml:

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

    In res/values-land/dimens.xml:

    <dimen name="actionBarSizeDp">40dp</dimen>
    

    These values are the default action bar size values as defined in android's own res/values-XX/dimens.xml.

    Finally, use actionBarSizeDp in plus_icon_drawable.xml:

    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item>
            <shape android:shape="line">
                <size android:width="@dimen/actionBarSizeDp"
                      android:height="@dimen/actionBarSizeDp"/>
                <stroke android:color="#000"
                        android:width="3dp"/>
            </shape>
        </item>
        <item>
            <rotate
                android:fromDegrees="90"
                android:toDegrees="90">
                <size android:width="@dimen/actionBarSizeDp"
                      android:height="@dimen/actionBarSizeDp"/>
                <shape android:shape="line">
                    <stroke android:color="#000"
                            android:width="3dp"/>
                </shape>
            </rotate>
        </item>
    </layer-list>
    

    In fact, defining the size on one of the LayerDrawables should suffice. The other(s) will be drawn accordingly.