57

Here is my layout:

enter image description here

The issue I'm facing is with the drawable checkmark. How would I go about aligning it next to the text, both of them centered within the button? Here is the XML:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".PostAssignmentActivity" >

    <LinearLayout
        style="?android:attr/buttonBarStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:orientation="horizontal" >

        <Button
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:drawableLeft="@drawable/ic_checkmark_holo_light"
            android:text="Post" />

        <Button
            style="?android:attr/buttonBarButtonStyle"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="Cancel" />
    </LinearLayout>

</RelativeLayout>

Applying android:gravity="center_vertical" pulls the text and drawable together, but then the text is no longer aligned in the center.

Randall Ma
  • 10,486
  • 9
  • 37
  • 45

14 Answers14

78

Solution 1

Set android:paddingLeft inside your first button. This will force the drawableLeft by paddingLeft amount to the right. This is the fast/hacky solution.

Solution 2

Instead of using a ButtonView, use a LinearLayout that contains both a textview and imageview. This is a better solution. It gives you more flexibility in the positioning of the checkmark.

Replace your ButtonView with the following code. You need the LinearLayout and TextView to use buttonBarButtonStyle so that the background colors are correct on selection and the text size is correct. You need to set android:background="#0000" for the children, so that only the LinearLayout handles the background coloring.

<LinearLayout
    style="?android:attr/buttonBarButtonStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_weight="1"
    android:orientation="horizontal" >
    <ImageView 
        style="?android:attr/buttonBarButtonStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:clickable="false"
        android:background="#0000"
        android:src="@drawable/ic_checkmark_holo_light"/>
    <TextView
        style="?android:attr/buttonBarButtonStyle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:clickable="false"
        android:background="#0000"
        android:text="Done" />
</LinearLayout>

Here are some screenshots I took while trying this out.

enter image description here enter image description here enter image description here

Brian Attwell
  • 9,239
  • 2
  • 31
  • 26
  • 5
    Having deep view hierarchies with nested LinearLayouts is a bad idea for performance reasons inside ListViews. I don't think it should be a concern here. – Brian Attwell Feb 03 '13 at 01:34
  • 1
    Thanks for doing a writeup this detailed! :) – Randall Ma Feb 03 '13 at 01:41
  • This is bad solution for performance and code readability reasons. Using paddings is a way better solution to go with and, if it's not enough, you may consider extending Button view and drawing icon with Canvas and onDraw() method. – pawelo Dec 11 '14 at 11:01
  • Pawla, you reiterate a good point. If your layout contains a lot of buttons (perhaps inside a ListView), my Solution #1 doesn't handle your design and you are experiencing performance problems: you might want to override Button in order to minimize the number of Views in your hierarchy. But be careful to test this in a RTL layout. – Brian Attwell Dec 17 '14 at 19:07
  • Did you forget to set the gravity to be centralized in the `LinearLayout`? – corsair992 Jan 18 '15 at 18:17
37

None of these solutions worked correctly without presenting unacceptable trade-offs (create a layout with views in it? Not a good idea). So why not roll your own? This is what I got:

enter image description here

First create an attrs.xml with this:

<resources>
    <declare-styleable name="IconButton">
        <attr name="iconSrc" format="reference" />
        <attr name="iconSize" format="dimension" />
        <attr name="iconPadding" format="dimension" />
    </declare-styleable>
</resources>

This allows to create an icon with specific size, padding from text, and image in our new view. The view code looks like this:

public class IconButton extends Button {
    private Bitmap mIcon;
    private Paint mPaint;
    private Rect mSrcRect;
    private int mIconPadding;
    private int mIconSize;

    public IconButton(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    public IconButton(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    public IconButton(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        int shift = (mIconSize + mIconPadding) / 2;

        canvas.save();
        canvas.translate(shift, 0);

        super.onDraw(canvas);

        if (mIcon != null) {
            float textWidth = getPaint().measureText((String)getText());
            int left = (int)((getWidth() / 2f) - (textWidth / 2f) - mIconSize - mIconPadding);
            int top = getHeight()/2 - mIconSize/2;

            Rect destRect = new Rect(left, top, left + mIconSize, top + mIconSize);
            canvas.drawBitmap(mIcon, mSrcRect, destRect, mPaint);
        }

        canvas.restore();
    }

    private void init(Context context, AttributeSet attrs) {
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.IconButton);

        for (int i = 0; i < array.getIndexCount(); ++i) {
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.IconButton_iconSrc:
                    mIcon = drawableToBitmap(array.getDrawable(attr));
                    break;
                case R.styleable.IconButton_iconPadding:
                    mIconPadding = array.getDimensionPixelSize(attr, 0);
                    break;
                case R.styleable.IconButton_iconSize:
                    mIconSize = array.getDimensionPixelSize(attr, 0);
                    break;
                default:
                    break;
            }
        }

        array.recycle();

        //If we didn't supply an icon in the XML
        if(mIcon != null){
            mPaint = new Paint();
            mSrcRect = new Rect(0, 0, mIcon.getWidth(), mIcon.getHeight());
        }
    }

    public static Bitmap drawableToBitmap (Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable)drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }
}

And then it can be used like this:

<com.example.grennis.myapplication.IconButton
    android:layout_width="200dp"
    android:layout_height="64dp"
    android:text="Delete"
    app:iconSrc="@android:drawable/ic_delete"
    app:iconSize="32dp"
    app:iconPadding="6dp" />

This works for me.

LargeGlasses
  • 912
  • 10
  • 17
Greg Ennis
  • 14,917
  • 2
  • 69
  • 74
  • Take care using measureText() on Android 5 lollipop it will draw the button text ALL CAPS, but measureText() will return the string as it's defined in xml. Use explicit all caps to fix this. – Greg Ennis Mar 24 '15 at 00:07
  • this is the best solution for me because I didn't have to reproduce the nice effects material design buttons had by default – SIr Codealot May 29 '15 at 02:36
  • Great solution except there's a bug imo. The image and the text aren't centered. To center you need to move the canvas.restore(); right behind the super.onDraw(canvas); – Emanuel Moecklin Jul 20 '17 at 22:50
  • How would you handle buttons that set their width to match_parent or wrap_content? I'm assuming you have to override onMeasure – Zachary Sweigart Sep 04 '18 at 22:06
  • @GregEnnis What do you mean with `Use explicit all caps to fix this`? Can you please provide an example? – Paul Spiesberger Oct 02 '18 at 08:59
  • Creating custom view class instead of just build right XML layout is an unacceptable trade-off. – konata May 24 '19 at 16:06
25

You can use <com.google.android.material.button.MaterialButton/> .
https://material.io/develop/android/components/material-button/

It finally allows setting the icon gravity.

 <com.google.android.material.button.MaterialButton
        android:layout_width="match_parent"
        android:layout_height="48dp"
        android:layout_marginStart="16dp"
        android:layout_marginEnd="16dp"
        android:gravity="center"
        android:text="Awesome button"
        app:icon="@drawable/your_icon"
        app:iconGravity="textStart" />
Leo DroidCoder
  • 14,527
  • 4
  • 62
  • 54
19

Here is a clean easy way, without doing anything fancy, to achieve the results of having a Button that is much wider than the content with Image and Text which are centered.

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:clickable="true"
    android:background="@drawable/button_background_selector">

    <Button
        android:layout_centerInParent="true"
        android:gravity="center"
        android:duplicateParentState="true"
        android:layout_width="wrap_content"
        android:text="New User"
        android:textSize="15sp"
        android:id="@android:id/button1"
        android:textColor="@android:color/white"
        android:drawablePadding="6dp"
        android:drawableLeft="@drawable/add_round_border_32x32"
        android:layout_height="64dp" />

</RelativeLayout>

enter image description here

simplatek
  • 637
  • 7
  • 7
  • This is the easiest solution to create this type of buttons. – Ionut Negru Aug 29 '15 at 09:27
  • Terrible way using RelativeLayout, don't do this. – Pedro Paulo Amorim Jan 27 '17 at 15:38
  • 1
    Nesting a standard Checkbox within a RelativeLayout worked great for me. The button image was biased left, the text was a variable distance away depending on the size of the parent / screen size... nesting within a RelativeLayout normalized the size of the checkbox ( where the button and text looked fine ) and allowed the two to be centered. – WM1 Mar 11 '17 at 21:39
  • the downside to this is much of what looks like a button is not tappable. And if your button has state changes on press/etc it'll look strange. – JohnnyLambada Jul 13 '17 at 23:47
  • For me, this was by far most useful piece: " android:drawablePadding="6dp" Thank you. – Roga Men Nov 25 '17 at 20:03
11

In our case, we wanted to use the default Button class (to inherit its various styles and behaviors) and we needed to be able to create the button in code. Also, in our case we could have text, an icon (left drawable), or both.

The goal was to center the icon and/or text as a group when the button width was wider than wrap_content.

public class CenteredButton extends Button
{
    public CenteredButton(Context context, AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);

        // We always want our icon and/or text grouped and centered.  We have to left align the text to
        // the (possible) left drawable in order to then be able to center them in our onDraw() below.
        //
        setGravity(Gravity.LEFT|Gravity.CENTER_VERTICAL);
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        // We want the icon and/or text grouped together and centered as a group.

        // We need to accommodate any existing padding
        //
        float buttonContentWidth = getWidth() - getPaddingLeft() - getPaddingRight();

        // In later versions of Android, an "all caps" transform is applied to buttons.  We need to get
        // the transformed text in order to measure it.
        //
        TransformationMethod method = getTransformationMethod();
        String buttonText = ((method != null) ? method.getTransformation(getText(), this) : getText()).toString();
        float textWidth = getPaint().measureText(buttonText);

        // Compute left drawable width, if any
        //
        Drawable[] drawables = getCompoundDrawables();
        Drawable drawableLeft = drawables[0];
        int drawableWidth = (drawableLeft != null) ? drawableLeft.getIntrinsicWidth() : 0;

        // We only count the drawable padding if there is both an icon and text
        //
        int drawablePadding = ((textWidth > 0) && (drawableLeft != null)) ? getCompoundDrawablePadding() : 0;

        // Adjust contents to center
        //
        float bodyWidth = textWidth + drawableWidth + drawablePadding;
        canvas.translate((buttonContentWidth - bodyWidth) / 2, 0);

        super.onDraw(canvas);
    }
}
BobDickinson
  • 2,156
  • 21
  • 18
  • I like this solution because it rendered my drawable tint correctly. However, I wanted the text centered with the icon to the left, while this code centers the icon+text. I replaced the translate with the values from Greg's IconButton example so it would work: int left = (int)((getWidth() / 2f) - (textWidth / 2f) - drawableWidth - drawablePadding); canvas.translate(left, 0); – Dandalf Jun 29 '16 at 20:06
  • Thanks for the code. In order to have Tint using compat library, I have extended `AppCompatButton` instead of regular Button – Alin Nov 09 '16 at 17:04
  • 1
    This is by far the best solution IMO! Requires only one class with very few code, and also uses the existing android attributes for compound drawables. – Tharkius Apr 08 '19 at 13:25
  • I'd like to add though that to make this work consistently, you must set gravity in your button like the following: android:gravity="left|center_vertical". This is because gravity defaults to "center" for buttons, and this class also centers the button's content, resulting in an unexpected result. – Tharkius Apr 09 '19 at 09:42
9

Here is my code and working perfect.

<Button
    android:id="@+id/button"
    android:layout_width="200dp"
    android:layout_height="50dp"
    android:layout_gravity="center"
    android:background="@drawable/green_btn_selector"
    android:gravity="left|center_vertical"
    android:paddingLeft="50dp"
    android:drawableLeft="@drawable/plus"
    android:drawablePadding="5dp"
    android:text="@string/create_iou"
    android:textColor="@color/white" />
Ali Imran
  • 8,927
  • 3
  • 39
  • 50
  • 2
    Buttons are also better for accessibility reasons. Users using the accessibility features don't know they're supposed to press a LinearLayout with an ImageView and TextView inside of it. – adamame May 06 '15 at 17:10
  • +1 for `android:drawablePadding` works perfectly @CliveJefferies it works for sizes `wrap content` – Vedant Agarwala May 09 '15 at 11:46
3
public class DrawableCenterTextView extends TextView {

    public DrawableCenterTextView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    public DrawableCenterTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public DrawableCenterTextView(Context context) {
        super(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        Drawable[] drawables = getCompoundDrawables();
        if (drawables != null) {
            Drawable drawableLeft = drawables[0];
            Drawable drawableRight = drawables[2];
            if (drawableLeft != null || drawableRight != null) {
                float textWidth = getPaint().measureText(getText().toString());
                int drawablePadding = getCompoundDrawablePadding();
                int drawableWidth = 0;
                if (drawableLeft != null)
                    drawableWidth = drawableLeft.getIntrinsicWidth();
                else if (drawableRight != null) {
                    drawableWidth = drawableRight.getIntrinsicWidth();
                }
                float bodyWidth = textWidth + drawableWidth + drawablePadding;
                canvas.translate((getWidth() - bodyWidth) / 2, 0);
            }
        }
        super.onDraw(canvas);
    }
}
Math
  • 3,334
  • 4
  • 36
  • 51
tangjun
  • 51
  • 1
3

This is now available in the Material Button by default with the app:iconGravity property. However, the Material Button does not allow for setting the background to a drawable (RIP gradients).

I converted the answers by @BobDickinson and @David-Medenjak above to kotlin and it works great.

import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.Gravity
import androidx.appcompat.widget.AppCompatButton
import kotlin.math.max

class CenteredButton @JvmOverloads constructor(
  context: Context,
  attrs: AttributeSet? = null,
  defStyle: Int = R.attr.buttonStyle
) : AppCompatButton(context, attrs, defStyle) {

  init {
    gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
  }

  override fun onDraw(canvas: Canvas) {
    val buttonContentWidth = (width - paddingLeft - paddingRight).toFloat()

    var textWidth = 0f
    layout?.let {
      for (i in 0 until layout.lineCount) {
        textWidth = max(textWidth, layout.getLineRight(i))
      }
    }

    val drawableLeft = compoundDrawables[0]
    val drawableWidth = drawableLeft?.intrinsicWidth ?: 0
    val drawablePadding = if (textWidth > 0 && drawableLeft != null) compoundDrawablePadding else 0

    val bodyWidth = textWidth + drawableWidth.toFloat() + drawablePadding.toFloat()

    canvas.save()
    canvas.translate((buttonContentWidth - bodyWidth) / 2, 0f)
    super.onDraw(canvas)
    canvas.restore()
  }
}
sschmitz
  • 432
  • 5
  • 16
2

I know it's a bit late, but if anyone looking for another answer, here is another way to add icon without the need to wrap button with a ViewGroup

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btnCamera"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Click!"
        android:textAllCaps="false"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

*need to set textAllCaps to false to make the spannable working


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val buttonLabelBuilder = SpannableStringBuilder(btnCamera.text)
        val iconDrawable = AppCompatResources.getDrawable(this, R.drawable.ic_camera)
        iconDrawable?.setBounds(0, 0, btnCamera.lineHeight, btnCamera.lineHeight)
        val imageSpan = ImageSpan(iconDrawable, ImageSpan.ALIGN_BOTTOM)

        buttonLabelBuilder.insert(0, "i ")
        buttonLabelBuilder.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)

        btnCamera.text = buttonLabelBuilder
    }
}

button with imagespan

Handrata Samsul
  • 786
  • 5
  • 10
1

I started with @BobDickinson's answer, but it did not cope well with multiple lines. The approach is good, because you still end up with a Button that can properly be reused.

Here is an adapted solution that will also work if the button has multiple lines (Please don't ask why.)

Just extend Button and use the following in onDraw, the getLineRight() is used to look up the actual length of each line.

@Override
protected void onDraw(Canvas canvas) {
    // We want the icon and/or text grouped together and centered as a group.
    // We need to accommodate any existing padding
    final float buttonContentWidth = getWidth() - getPaddingLeft() - getPaddingRight();

    float textWidth = 0f;
    final Layout layout = getLayout();
    if (layout != null) {
        for (int i = 0; i < layout.getLineCount(); i++) {
            textWidth = Math.max(textWidth, layout.getLineRight(i));
        }
    }

    // Compute left drawable width, if any
    Drawable[] drawables = getCompoundDrawables();
    Drawable drawableLeft = drawables[0];
    int drawableWidth = (drawableLeft != null) ? drawableLeft.getIntrinsicWidth() : 0;

    // We only count the drawable padding if there is both an icon and text
    int drawablePadding = ((textWidth > 0) && (drawableLeft != null)) ? getCompoundDrawablePadding() : 0;

    // Adjust contents to center
    float bodyWidth = textWidth + drawableWidth + drawablePadding;

    canvas.save();
    canvas.translate((buttonContentWidth - bodyWidth) / 2, 0);
    super.onDraw(canvas);
    canvas.restore();
}
Community
  • 1
  • 1
David Medenjak
  • 33,993
  • 14
  • 106
  • 134
0

Here is a another solution:

     <LinearLayout
        android:id="@+id/llButton"
        android:layout_width="match_parent"
        style="@style/button_celeste"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            style="@style/button_celeste"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:drawablePadding="10dp"
            android:clickable="false"
            android:drawableLeft="@drawable/icon_phone"
            android:text="@string/call_runid"/>
    </LinearLayout>

and the event:

    LinearLayout btnCall = (LinearLayout) findViewById(R.id.llButton);
    btnCall.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            call(runid.Phone);
        }
    });
Giovanny Farto M.
  • 1,557
  • 18
  • 20
0

I had the same issue, and I've come up with a solution that doesn't require XML changes or custom Views.

This code snippet retrieves the width of the text and the left/right drawables, and sets the Button's left/right padding so there will only be enough space to draw the text and the drawables, and no more padding will be added. This can be applied to Buttons as well as TextViews, their superclasses.

public class TextViewUtils {
    private static final int[] LEFT_RIGHT_DRAWABLES = new int[]{0, 2};

    public static void setPaddingForCompoundDrawableNextToText(final TextView textView) {
        ViewTreeObserver vto = textView.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                shinkRoomForHorizontalSpace(textView);
            }
        });

    }

    private static void shinkRoomForHorizontalSpace(TextView textView) {
        int textWidth = getTextWidth(textView);
        int sideCompoundDrawablesWidth = getSideCompoundDrawablesWidth(textView);
        int contentWidth = textWidth + sideCompoundDrawablesWidth;
        int innerWidth = getInnerWidth(textView);
        int totalPadding = innerWidth - contentWidth;
        textView.setPadding(totalPadding / 2, 0, totalPadding / 2, 0);
    }

    private static int getTextWidth(TextView textView) {
        String text = textView.getText().toString();
        Paint textPaint = textView.getPaint();
        Rect bounds = new Rect();
        textPaint.getTextBounds(text, 0, text.length(), bounds);
        return bounds.width();
    }

    private static int getSideCompoundDrawablesWidth(TextView textView) {
        int sideCompoundDrawablesWidth = 0;
        Drawable[] drawables = textView.getCompoundDrawables();
        for (int drawableIndex : LEFT_RIGHT_DRAWABLES) {
            Drawable drawable = drawables[drawableIndex];
            if (drawable == null)
                continue;
            int width = drawable.getBounds().width();
            sideCompoundDrawablesWidth += width;
        }
        return sideCompoundDrawablesWidth;
    }

    private static int getInnerWidth(TextView textView) {
        Rect backgroundPadding = new Rect();
        textView.getBackground().getPadding(backgroundPadding);
        return textView.getWidth() - backgroundPadding.left - backgroundPadding.right;
    }
}

Notice that:

  • It actually still leaves some more space than needed (good enough for my purposes, but you may look for the error)
  • It overwrites whatever left/right padding is there. I guess it's not difficult to fix that.

To use it, just call TextViewUtils.setPaddingForCompoundDrawableNextToText(button) on your onCreate or onViewCreated().

ris8_allo_zen0
  • 1,537
  • 1
  • 15
  • 35
  • Haven't tried something like this for long time, but back in times of 2.x, 4.x, 5.x and 6.x API each did evaluate the pixel sizes of text differently, I never managed to get a universally working code across all desired versions of Android, especially 5.x vs 6.x was completely different IIRC, including some internal bugs in implementation of those functions... If you did some extensive testing across common APIs today, it would be helpful to specify that in your answer, that you know it works well. – Ped7g Jan 19 '18 at 13:25
0

There are several solutions to this problem. Perhaps the easiest on some devices is to use paddingRight and paddingLeft to move the image and text next to each other as below.

Original button

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="32dp"
    android:layout_marginEnd="32dp"
    android:layout_marginTop="16dp"
    android:text="@string/scan_qr_code"
    android:textColor="@color/colorPrimary"
    android:drawableLeft="@drawable/ic_camera"
    android:paddingRight="90dp"
    android:paddingLeft="90dp"
    android:gravity="center"
    />

Using Padding can work

The problem here is on smaller devices this padding can cause unfortunate problems such as this: enter image description here

The other solutions are all some version of "build a button out of a layout an image and a textview". They work, but completely emulating a button can be tricky. I propose one more solution; "build a button out of a layout an image, a textview, and a button"

Here's the same button rendered as I propose:

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="32dp"
    android:layout_marginEnd="32dp"
    android:layout_marginTop="16dp"
    android:gravity="center"
    >
    <Button
        android:id="@+id/scanQR"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/white_bg_button"
        />
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_centerInParent="true"
        android:gravity="center"
        android:elevation="10dp"
        >
        <ImageView
            android:id="@+id/scanImage"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="8dp"
            android:src="@drawable/ic_camera"
            />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
            android:text="@string/scan_qr_code"
            android:textColor="@color/colorPrimary"
            />
    </LinearLayout>
</RelativeLayout>

As you can see, the button is now within a relative layout, but it's text and drawableLeft are not part of the button, they are in a separate layout that's placed on top of the button. With this, the button still acts like a button. The gotchas are:

  1. The inner layout needs an elevation for newer versions of Android. The button itself has an elevation greater than the ImageView and TextView, so even though they are defined after the Button, they will still be "below" it in elevation and be invisible. Setting 'android:elevation' to 10 solves this.
  2. The textAppearance of the TextView must be set so that it has the same appearance as it would in a button.
JohnnyLambada
  • 12,700
  • 11
  • 57
  • 61
-4

Another quite hacky alternative is to add blank spacer views with weight="1" on each side of the buttons. I don't know how this would affect performance.

    <View
        android:layout_width="0dp"
        android:layout_height="fill_parent"
        android:layout_weight="1" />
TTransmit
  • 3,270
  • 2
  • 28
  • 43
  • I don't think that's possible, you can't add elements inside a button – Mehdiway May 25 '15 at 11:46
  • You would put the spacer views either side of the button not within the button. It centers the button's text and drawable. The spacer regions are not a clickable part of the button. This technique could also be used making the containing layout clickable. It's not a great solution but it may be suitable for some people. – TTransmit Jun 15 '15 at 12:39