6

I got stuck in an odd issue with Android - I want to have a button that looks like this:

 |-----------------------------------------------------------------------|
 |                   [icon] <5px> [text text text]                       |
 |-----------------------------------------------------------------------|

and the group ([icon] <5px> [text text text]) should be centred. Note that 5px is used just as a placeholder for any padding you want to have between the icon and the text

I found some answers here that were more or less graviting around either setting a background (which I don't want to do because I have another background) or using the android:drawableLeft property to set the icon.

However looks like the documentation of the setCompoundDrawablesWithIntrinsicBounds method is a bit misleading (see here). It states that the image is placed on the left/right/top/bottom side of the TEXT wich is not true. The icon is placed on the corresponding side of the BUTTON. For example:

Setting the android:drawableLeft property puts the icon on the most left position and gets me this (with gravity CENTER):

 |-----------------------------------------------------------------------|
 | [icon]                     [text text text]                           |
 |-----------------------------------------------------------------------|

or this (with gravity LEFT):

 |-----------------------------------------------------------------------|
 | [icon] [text text text]                                               |
 |-----------------------------------------------------------------------|

Both are ugly as hell :(

I found a workaround that looks like this:

public static void applyTextOffset(Button button, int buttonWidth) {
    int textWidth = (int) button.getPaint().measureText(button.getText().toString());
    int padding = (buttonWidth / 2) - ((textWidth / 2) + Constants.ICON_WIDTH  + Constants.ICON_TO_TEXT_PADDING);
    button.setPadding(padding, 0, 0, 0);
    button.setCompoundDrawablePadding(-padding);
}

And it works more or less but I don't find it to my liking for following reasons:

  • it requires to know the button width. With auto-sized buttons it will not be known until the actual layout is done. Google recommend using a listener to learn the actual width after the rendering is done but this immensely complicates the code.
  • I feel like I'm taking over the layout responsibility from the Android layout engine

Isn't there a more elegant solution?

vap78
  • 1,029
  • 2
  • 11
  • 26

4 Answers4

10

You can use the following Button subclass to achieve this effect.

  1. Paste this class into your project and tweak the package name if desired.
    package com.phillipcalvin.iconbutton;

    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Rect;
    import android.graphics.Paint;
    import android.graphics.drawable.Drawable;
    import android.util.AttributeSet;
    import android.widget.Button;

    public class IconButton extends Button {
      protected int drawableWidth;
      protected DrawablePositions drawablePosition;
      protected int iconPadding;

      // Cached to prevent allocation during onLayout
      Rect bounds;

      private enum DrawablePositions {
        NONE,
        LEFT,
        RIGHT
      }

      public IconButton(Context context) {
        super(context);
        bounds = new Rect();
      }

      public IconButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        bounds = new Rect();
      }

      public IconButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        bounds = new Rect();
      }

      public void setIconPadding(int padding) {
        iconPadding = padding;
        requestLayout();
      }

      @Override
      protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        Paint textPaint = getPaint();
        String text = getText().toString();
        textPaint.getTextBounds(text, 0, text.length(), bounds);

        int textWidth = bounds.width();
        int contentWidth = drawableWidth + iconPadding + textWidth;

        int contentLeft = (int)((getWidth() / 2.0) - (contentWidth / 2.0));
        setCompoundDrawablePadding(-contentLeft + iconPadding);
        switch (drawablePosition) {
        case LEFT:
          setPadding(contentLeft, 0, 0, 0);
          break;
        case RIGHT:
          setPadding(0, 0, contentLeft, 0);
          break;
        default:
          setPadding(0, 0, 0, 0);
        }
      }

      @Override
      public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top, Drawable right, Drawable bottom) {
        super.setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
        if (null != left) {
          drawableWidth = left.getIntrinsicWidth();
          drawablePosition = DrawablePositions.LEFT;
        } else if (null != right) {
          drawableWidth = right.getIntrinsicWidth();
          drawablePosition = DrawablePositions.RIGHT;
        } else {
          drawablePosition = DrawablePositions.NONE;
        }
        requestLayout();
      }
    }

2. Modify your layout to use this new subclass instead of plain Button:

    <com.phillipcalvin.iconbutton.IconButton
        android:id="@+id/search"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:drawableLeft="@drawable/search"
        android:text="@string/search" />

3. If you want to add padding between the drawable and the text, add the following to your activity's onCreate:

    // Anywhere after setContentView(...)
    IconButton button = (IconButton)findViewById(R.id.search);
    button.setIconPadding(10);

This subclass also supports drawableRight. It does not support more than one drawable.

If you want more features, such as the ability to specify the iconPadding directly in your layout XML, I have a library that supports this.

Phil Calvin
  • 5,077
  • 2
  • 41
  • 35
0

Simply way is to write Button in LinearLayout. Something like this

 <LinearLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:gravity="center">

            <Button
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:drawableLeft="@drawable/ic_left"
                android:drawablePadding="5dp"
                android:duplicateParentState="true"
                android:text="Button text"/>

        </LinearLayout>
voha
  • 42
  • 8
0

You may also consider using a custom view to create a custom button.

This could be anywhere from easy to extremely complex.

It would be easy if all you need to do is override onDraw(). It will be more complex if you need to lay out multiple views.

Phil Calvin
  • 5,077
  • 2
  • 41
  • 35
stefan bachert
  • 9,413
  • 4
  • 33
  • 40
  • I'll accept this answer because it is correct and provides a solution, although I'm not sure I can clasify it as a "more elegant" solution. My point is that it is actually a common sense to expect tha the text and the icon are centred when you call setGravity(CENTER) and creating custom components just for THAT is a bit of an overkill. – vap78 Apr 17 '12 at 19:19
-3

I tried this in eclipse and it spouted no errors:

<Button android:id="@+id/test"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@android:color/transparent" >
<LinearLayout
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:layout_weight="1"
    android:gravity="center" 
    android:background="@android:color/transparent" >

    </LinearLayout>
    </Button>

Therefore, I suppose you could add an image and text views in a layout, itself inside a button, and center the layout.

Best regards.

pouzzler
  • 1,800
  • 2
  • 20
  • 32
  • hmmm ... I tried this one but I get a java.lang.ClassCastException when running it on an emulator (2.3.3 compliant) – vap78 Apr 02 '12 at 18:41
  • 1
    Button cannot hold a LinearLayout since it is not a ViewGroup. This can't work. – lowellk Nov 28 '12 at 03:08