XML Styling with Configurator


XML Styling with Configurator

[last_updated_for version=0.9.4]

Overview

Writing a functional Android app is one thing.  Writing something that looks good on a variety of screen sizes and densities is quite another.  The purpose of this tutorial is to show how to design your plots to support multiple screen sizes.  It assumes that the developer is familiar with the basic concepts behind supporting multiple screen types.  For those that are not we suggest reading this document before continuing.

Usage

The basic approach to making a visual element look good on multiple screens involves providing line and font sizes in density independent values (dp),  customized layouts that can define and arrange visual elements differently depending on the screen type and orientation or some combination of the two.  Android makes this process elegant by separating the presentation logic into various xml resource files.  You've no doubt seen various views styled using the css-like usage of @style:
<TextView
  style="@style/CodeFont"
  android:text="@string/hello" />
Starting with version 0.6.1 and later, an increasing number of styleable attributes are supported.  While these styleable attributes are the preferred method of styling plots to look good across devices, they are often insufficient for many use cases.  As a result of this deficiency it has become common practice to programmatically style plots, thus negating the elegance Android gave us originally.  Sure you could still specify your values in dp by using PixelUtils.dpToPix(float) but it's still not ideal.  What is needed is a way to keep the style and layout parameters in XML. And that's exactly what Configurator give us.  Here's how it works:
  1. Retrieve a list of all XML parameters found in the plot element that start with the prefix "androidPlot".
  2. Use reflection to find the appropriate getters to access the nested parameter specified.
  3. Use reflection to find the appropriate setter to set the nested parameter specified.
  4. Convert the specified value from dp, em, enum string or whatever into the format expected by the setter.
  5. Invoke the setter using the converted value.
Let's take an example.  First the programmatic approach:
plot.getGraphWidget().getRangeLabelPaint().setTextSize(PixelUtils.dpToPix(13));
And now the XML based approach:
androidPlot.graphWidget.rangeLabelPaint.textSize="@dimen/range_tick_label_font_size"
elsewhere range_tick_label_font_size is defined as:
<dimen name="range_label_font_size">13dp</dimen>
We know this is a plot param because it begins with "androidPlot".  Operating on the plot reference for which this param has been defined, we see that the top level object is graphWidget.  Using reflection we retrieve the getter "getGraphWidget()".  We invoke the getter, storing the returned instance and repeat the process with the next segment of the path: "rangeLabelPaint".  This continues until we get to the final segment: "textSize".  For this final segment we use some magic to determine the appropriate setter, convert the specified value into the expected format (in this case converting from dp to px) and invoke the setter.

Configuring Formatters, etc.

Configurator was designed to be generic enough to be useful for configuring other things beyond plots, such as Formatters.  Formatters actually contribute quite a bit to the look and feel of a plot so it's only appropriate that their visual properties be configurable in XML as well.  Now because Formatters are not tangible visual elements like Plots, it makes no sense to define them in the layout xml or even nested within a plot element's xml.  Instead, we use Android's custom xml resource location /res/xml to house our XML definitions for any non plot elements that we wish to configure.  Here's a sample LineAndPointFormatter config taken from the SimpleXYPlot Example:
<?xml version="1.0" encoding="utf-8"?>
<config
        linePaint.strokeWidth="3dp"
        linePaint.color="#00AA00"
        vertexPaint.color="#007700"
        fillPaint.color="#00000000"
        pointLabelFormatter.textPaint.color="#FFFFFF"/>
All that remains is to tell Configurator where to apply this configuration.  We do so programmatically in the Activity source like this:
series1Format.configure(getApplicationContext(),
                R.xml.line_point_formatter_with_plf1);
In the case above, seriesFormat extends the abstract Formatter base class which defines a convenience configure(...) method.  Alternatively, the XML configuration could have been applied to the object instance like this:
Configurator.configure(getApplicationContext(), 
               series1Format, R.xml.line_point_formatter_with_plf1);

Limitations

You can:
  • Configure all primitive types (float, int, boolean, etc.)
  • Specify dimensions using dp, sp, em, etc. suffixes
  • Specify Colors using hex RGB (#FF0000), ARGB (#00FF0000), etc.
  • Specify enum values by name. ex:
    androidPlot.legendWidget.positionMetrics.anchor="right_bottom"
  • Specify values by resource id. such as @dimen/title_font_size, @string/bla, etc.
You cannot:
  • Configure parameters that do not have a public setter 
  • Configure nested parameters that cannot be reached using only public getters
  • Configure parameters that must be accessed via public getters or setter that do not follow standard setter/getter Java Bean notation.
  • Configure parameters that are Genericized Enums