Handling Android resources with non-standard formats
In Android you have to deal with a large variety of screen sizes. The current solution is to use adaptive design to handle all the screens.
One concept of adaptive design is that you have ever-growing content frames... up to a point. Once the device is too large or wide, you setup margins instead so that your app doesn't look ridiculous.
In the above example (from Trello), we use the full screen width for cards on smaller devices, but once we get to tablet-sized devices we treat it like a dialog.
That's all well and good, but how do you actually go about implementing it?
An initial attempt might be to do this:
<FrameLayout
android:id="@+id/content"
android:layout_width="@dimen/responsive_width"
android:layout_height="match_parent" />
The idea is that responsive_width
should be match_parent
when the screen is small, but a specific width (say, 800dp
) when the screen gets wide enough:
<!-- Inside /res/values/dimens.xml -->
<dimen name="responsive_width">match_parent</dimen>
<!-- Inside /res/values-sw800dp/dimens.xml -->
<dimen name="responsive_width">800dp</dimen>
You'll quickly discover this doesn't work:
String types not allowed (at 'responsive_width' with value 'match_parent').
The core problem is that match_parent
is an enum constant for a particular attribute. It's defined only in the context of layout width or height, so a generic dimension field doesn't know how to interpret it.
You can find the definition of these enums in attrs.xml
of the platform source code. For example, here's layout_width
:
<attr name="layout_width" format="dimension">
<enum name="fill_parent" value="-1" />
<enum name="match_parent" value="-1" />
<enum name="wrap_content" value="-2" />
</attr>
Interesting - what happens if we use its underlying constant value (which, in this case, is -1)?
<dimen name="responsive_width">-1</dimen>
Nice try, buddy! You'll get this error message instead:
Integer types not allowed (at 'responsive_width' with value '-1').
The core problem is that <dimen>
is essentially shorthand for this:
<item name="responsive_width" type="dimen" format="dimension">800dp</item>
item
is a more generic resource, where you can define the type
and format
separately. Notice how, in this case, we're specifying the format
to be dimension
. This is why the compiler rejects string and integer values; they're not formatted correctly.
It turns out you don't actually have to be that strict; you can leave that out and it'll still work.
Let's try defining both match_parent
and wrap_content
without a format
:
<item name="match_parent" type="dimen">-1</item>
<item name="wrap_content" type="dimen">-2</item>
We can reference those in any other dimension value:
<!-- Inside /res/values/dimens.xml -->
<dimen name="responsive_width">@dimen/match_parent</dimen>
<!-- Inside /res/values-sw800dp/dimens.xml -->
<dimen name="responsive_width">800dp</dimen>
Ta-da! Now your responsive width is easy to implement.
I wouldn't go hog wild with this trick because using constants indirectly means you lose some type-checking. But in some cases, it's the best solution because it can save you from having to write multiple layout files for attributes you could've otherwise been switching in resources.
This trick isn't limited to just enums; <item>
can be used anytime you want a resource with a special format.
For example, View
has the attribute alpha
, defined thus:
<declare-styleable name="View">
<attr name="alpha" format="float" />
</declare-styleable>
But if you try this you get an error:
<dimen name="view_alpha">.5</dimen>
Error:(6, 25) Float types not allowed (at 'view_alpha' with value '.5').
Again, we can use <item>
to better define our dimension:
<item name="view_alpha" type="dimen" format="float">.5</item>
Now we can use it in a View
:
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/click_me"
android:alpha="@dimen/view_alpha"/>
(What's weird here is that you can actually use any type
for the item, as long as it's one you can reference in code. I could have used string
or even bool
above and it would still work. Again, it makes me a bit hesitant to code minus the type checking, but AFAIK there's no other way to do it...)
The important pattern here is to know the format of a View's attributes. Yet again the Android open source project saves the day: I highly recommend reading through the system's attrs.xml
to find attribute formats when you're working with item
. That way you can confirm that what you're creating matches the expected format for the attribute.