Paul Brower bio photo

Paul Brower

Android development for fun and profit.

Email Twitter Github Stackoverflow Codementor

Find further code and implementation here: https://github.com/browep/AndroidRatioButtons

When creating buttons across apps I have found a common pattern is creating a button according to a ratio instead of absolute dimensions. Often I want buttons that are twice as long as they are high or I want a Golden Ratio button ( 1:1.618 ). Building this using absolute dimensions would mean twice as many number to keep track of. For example let’s say I have a button 1:2/3 button.

<Button
android:layout_height="40"
android:layout_width="60"
android:text="60x40" />

Now let’s say I want to support different screen sizes ( http://developer.android.com/guide/practices/screens_support.html ). I would make a dimension entry in dimens and reference it.

res/values/dimens.xml

<resources>
    <dimen name="my_button_width">60dip</dimen>
    <dimen name="my_button_height">40dip</dimen>
</resources>

res/values-hdpi/dimens.xml

<resources>
    <dimen name="my_button_width">120dip</dimen>
    <dimen name="my_button_height">80dip</dimen>
</resources>
<Button
android:layout_height="@dimen/my_button_height"
android:layout_width="@dimen/my_button_width"
android:text="60x40" />

That works and is clean code. Now let’s say I have changed how my UI works a little bit, I want the buttons a little slimmer, 2:1 instead of 1:2/3. I have two places I need to update this now and I have to maintain that ratio through the dimens files. This is easy place to make an error, an error that would be very difficult to spot.

It would be a lot less error prone if we only had to adjust one dimension and the other relied on a ratio to update itself. Enter the RatioButton:

src/com/github/browep/ratiobuttons/view/RatioButton.java

public class RatioButton extends Button {

  private static String TAG = RatioButton.class.getCanonicalName();

  private float ratio;

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

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

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

  private void setup(Context context, AttributeSet attrs) {
    TypedArray styledAttribute = context.obtainStyledAttributes(attrs,
      R.styleable.com_github_browep_ratiobuttons_view_RatioButton);

      ratio = styledAttribute.getFloat(R.styleable.com_github_browep_ratiobuttons_view_RatioButton_percent_of_width,
        1.0f);

      }

      @Override
      protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getSize(widthMeasureSpec);
        int height = (int) (width * ratio);
        super.onMeasure(makeMeasureSpec(width, getMode(widthMeasureSpec)),
        makeMeasureSpec(height, getMode(heightMeasureSpec)));
      }

}

and the corresponding attrs.xml to tie it together

res/values/attrs.xml

<resources>

      <attr name="percent_of_width" format="float"/>

      <declare-styleable name="com.github.browep.ratiobuttons.view.RatioButton">
      <attr name="percent_of_width"/>
      </declare-styleable>

      <declare-styleable name="com.github.browep.ratiobuttons.view.RatioToggleButton">
      <attr name="percent_of_width"/>
      </declare-styleable>

</resources>

What’s happening?

When this button is put in a layout the layout inflater will call the constructor which will call setup. This function will pull from the XML attributes a custom attribute percent_of_width. It stores it as a field on the object. When it’s time to put the layout on the screen, the View is queried to measure itself using onMeasure. Here we will take the width and calculate the height from it. We have to translate some of the values using getSize, getMode, and getMeasureSpec. See http://developer.android.com/reference/android/view/View.MeasureSpec.html#getSize(int) for more reading.

This means the button will always respect this ratio.

An example:

<com.github.browep.ratiobuttons.view.RatioButton
      android:layout_height="0dip"
      android:layout_width="120dip"
      app:percent_of_width="0.5"
      android:text="0.5" />

Here you can see we have the “layout_height” set to 0dip. That’s ok because the value is completely ignored. This will make a button that is twice as wide as it is high. See https://github.com/browep/AndroidRatioButtons/blob/master/res/layout/main.xml for more examples.

You can download a full working project as an example from https://github.com/browep/AndroidRatioButtons