-1

I think I may have found a bug in the way layout_margin works on buttons. It can be summarized as button margins are not taken into account when measuring. As a result, their parent ViewGroup measures itself slightly too small. When the buttons are finally drawn, there isn't enough room and they're forced to wrap or ellipsize their text. This happens even when there's plenty of room in the buttons' grandparent for the buttons' parent to be larger. The problem exists whether I set the layout_margin in the button's style or on the button itself. I don't know if this affects other kinds of views.

Is this a known issue? Does any workaround exist, other than not using margins on buttons? (Or am I simply doing something wrong?)

I found an unanswered question from about a year ago that says layout_margin does not work at all in custom button styles. It actually works for me the way the asker of that question expected it to, at least in the sense that the buttons do have the specified margin. So maybe Google "fixed" it recently and the fix overlooked something.

This fragment layout demonstrates the issue. It looks fine by itself in the editor. The specified button style has a layout_margin of 3dp.

<?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="wrap_content" >

    <EditText
        android:id="@+id/userField"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="8"
        android:layout_alignParentLeft="true"
        android:layout_alignParentTop="true"
        android:hint="@string/hint_user" />

    <EditText
        android:id="@+id/passwordField"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/userField"
        android:layout_alignLeft="@id/userField"
        android:layout_alignRight="@id/userField"
        android:inputType="textPassword"
        android:fontFamily="sans-serif"
        android:hint="@string/hint_password" />

    <Button
        android:id="@+id/loginButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@+id/userField"
        android:layout_centerVertical="true"
        style="@style/button_blue"
        android:text="@string/login" />

</RelativeLayout>

good layout

But when this fragment is placed inside another layout, the fragment's width is apparently calculated incorrectly.

<?xml version="1.0" encoding="utf-8"?>
<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" >

    <fragment
        android:id="@+id/fragment"
        android:name="com.example.MyFragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        tools:layout="@layout/fragment_login" />

</RelativeLayout>

bad layout

The text wraps mid-word because I used a non-breaking space (\u00A0) in the button text. With a normal space it wraps at the word boundary. The problem disappears if I remove the layout_margin from the button style.

Edit: Here's a stand-alone layout that demonstrates the problem. The problem is always present in the WYSIWYG editor, SDK 21. On real devices, the bug manifests on 2.3.6 and 4.0.4 but not 4.4.1 or 5.0.1.

<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="com.chalcodes.marginbug.MainActivity" >

    <RelativeLayout
        android:id="@+id/buttonLayout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true" >

        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:text="Button" />

        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:layout_toRightOf="@id/button1"
            android:text="Button" />

    </RelativeLayout>

</RelativeLayout>

margin bug

Edit 2: Following up on corsair992's comment, I updated AS to the latest version (1.2 beta 3) and pasted my standalone example above. This does reproduce the problem. But strangely, the problem disappears when I change the preview orientation from portrait to landscape. Stranger still, the problem does not come back when I return to portrait! And if I restart AS, it again renders my layout incorrectly the first time, then renders it correctly after rotation. Eclipse always renders the layout incorrectly.

I've noticed that Android Studio's WYSIWYG editor caches things more aggressively than Eclipse's does. In Eclipse I can immediately see changes to custom views, but AS stubbornly hangs on to old renderings of custom views after a clean & rebuild, or even after a restart. So maybe AS is caching some info about the layout which helps it render correctly the second time. But maybe this is just masking the bug.

Community
  • 1
  • 1
Kevin Krumwiede
  • 9,868
  • 4
  • 34
  • 82

2 Answers2

1

I've actually created my own workaround that seems to address the problem. But this is only a proof of concept. The issue does not seem to affect 4.4 or 5.0, although I swear I've seen it happen on my 4.4 device at some point. In order to make the workaround deployable, I have to activate it conditionally.

I will award the bounty to someone who can show me either what Android versions this issue affects (if it is a known issue) or a clever way to detect at runtime whether the device is affected.

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.Button;

public class WorkaroundButton extends Button {

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();       
        final int width = getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
        final int height = getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
        setMeasuredDimension(width, height);
    }

}
Kevin Krumwiede
  • 9,868
  • 4
  • 34
  • 82
1

The only way I could reproduce this was by selecting the "Wear Square" virtual device in the editor.

enter image description here

It seems that the issue isn't the margin. As you can see in both the image above and in the image of your first edit, the margin is indeed enforced.

The issue is the combination of the margin size, the android:layout_toRightOf property, and the RelativeLayout's lack of the "weight" concept found in LinearLayouts.

Instead of using android:layout_toRightOf, use either a horizontal LinearLayout with weight values assigned to its children, or use android:layout_alignParentLeft="true" on the left button and android:layout_alignParentRight="true" on the right button.

user3829751
  • 712
  • 7
  • 20
  • It's exasperating that I'm the only one who seems able to reproduce the issue. I'd think something is wrong with my dev machine if not for the fact that I can reproduce it on physical devices. My actual layouts are more complex than my example, and I was hoping to avoid using nested LinearLayouts. Your second suggestion would seem to run afoul of the guidelines cited in an earlier answer (now deleted): http://developer.android.com/reference/android/widget/RelativeLayout.html second paragraph. – Kevin Krumwiede Apr 14 '15 at 18:20
  • To avoid the problem cited in that link, you could set the width of immediate RelativeLayout to "match_parent" and add a gravity property. Also I don't know how complex you're making your layouts, but there surely has to be a way you could re-arrange it so that this LinearLayout wouldn't have another LinearLayout as a parent/grandparent/ancestor; and if not, having _one_ nested layout in the entire app can't be too terribly demanding. Quite strange you're getting it on physical devices though, I've only ran into these kind of quirks with the editor. – user3829751 Apr 14 '15 at 18:49