Skip to content

UI Layout without XML

Sung-Ho Lee edited this page Jun 11, 2013 · 5 revisions

Android SDK leverages XML to build UI layouts. However, XML is considered still a bit verbose, and lacks programmability. Scaloid composes UI layout in Scala DSL style, therefore achieve both clarity and programmability. For example, suppose a legacy XML layout as shown below:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:layout_height="wrap_content" android:padding="20dip">
    <TextView android:layout_width="match_parent"
            android:layout_height="wrap_content" android:text="Sign in"
            android:layout_marginBottom="25dip" android:textSize="24.5sp"/>
    <TextView android:layout_width="match_parent"
            android:layout_height="wrap_content" android:text="ID"/>
    <EditText android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/userId"/>
    <TextView android:layout_width="match_parent"
            android:layout_height="wrap_content" android:text="Password"/>
    <EditText android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/password"
            android:inputType="textPassword"/>
    <Button android:layout_width="match_parent"
            android:layout_height="wrap_content" android:id="@+id/signin"
            android:text="Sign in"/>
    <LinearLayout android:orientation="horizontal"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
        <Button android:text="Help" android:id="@+id/help"
                android:layout_width="match_parent" 
                android:layout_height="wrap_content"/>
        <Button android:text="Sign up" android:id="@+id/signup"
                android:layout_width="match_parent" 
                android:layout_height="wrap_content"/>
    </LinearLayout>
</LinearLayout>

is reduced to:

new SVerticalLayout {
  STextView("Sign in").textSize(24.5 sp).<<.marginBottom(25 dip).>>
  STextView("ID")
  SEditText()
  STextView("Password")
  SEditText() inputType TEXT_PASSWORD
  SButton("Sign in")
  this += new SLinearLayout {
    SButton("Help")
    SButton("Sign up")
  }
}.padding(20 dip)

The layout description shown above is highly programmable. You can easily wire your logic into the layout:

new SVerticalLayout {
  STextView("Sign in").textSize(24.5 sp).<<.marginBottom(25 dip).>>
  STextView("ID")
  val userId = SEditText()
  STextView("Password")
  val pass = SEditText() inputType TEXT_PASSWORD
  SButton("Sign in", signin(userId.text, pass.text))
  this += new SLinearLayout {
    SButton("Help", openUri("http://help.url"))
    SButton("Sign up", openUri("http://signup.uri"))
  }
}.padding(20 dip)

Because a Scaloid layout description is plain Scala code, it is type-safe. Please refer to layout context for more details.

Automatic layout converter

This converter turns an Android XML layout into a Scaloid layout:

http://layout.scaloid.org

Migration tip

Scaloid is fully compatible with legacy xml layout files. You can access a widget described in xml layout as:

onCreate {
  setContentView(R.layout.main)
  val name = find[EditText](R.id.name)
  // do something with `name`
}

Layout context

In Android API, layout information is stored into a View object via the method View.setLayoutParams(ViewGroup.LayoutParams). A specific type of parameter passing into that method is determined by a the type of ...Layout object which contains the View object. For example, let us see some Java code shown below:

LinearLayout layout = new LinearLayout(context);
Button button = new Button(context);
button.setText("Click");
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams();
params.weight = 1.0f;  // sets some value
button.setLayoutParams(params);
layout.addView(button);

Because the button is appended into the LinearLayout, the layout parameter must be LinearLayout.LayoutParams, otherwise a runtime error might be occurred. Meanwhile, Scaloid eliminate this burden, while still preserving rigorous typing of LayoutParams. The code shown below is equivalent to the previous Java code:

val layout = new SLinearLayout {
  SButton("Click").<<.Weight(1.0f).>>
}

In the anonymous constructor of 'SLinearLayout', Scaloid provides an implicit function called "layout context". This affects a return type of the method << defined in the class SButton. If we use SFrameLayout as a layout context, the method << returns FrameLayout.LayoutParams, which does not have Weight method. Therefore, the code below results a syntax error.

val layout = new SFrameLayout {
  SButton("Click").<<.Weight(1.0f).>>   // Syntax error on Weight()
}

Compared with XML layout description, Scaloid layout is simple and type-safe.

The method << is overloaded with parameters <<(width:Int, height:Int) which assignes the size of the view component. For example:

SButton("Click").<<(40 dip, WRAP_CONTENT)

Operator new and method apply

Usually, View components are referenced multiple times in an Activity. For example:

var button: SButton = null
onCreate {
  // ...
  new SLinearLayout {
    button = new SButton() text "Click"
    this += button
  }
  // ...
}
// ... uses the button somewhere in other methods 
// (e.g. changing text or adding listeners)

Prefixed classes in Scaloid (e.g. SButton) have a companion object that implements apply methods that create a new component. These methods also append the component to the layout context that enclose the component. Therefore, the code block from the above example:

button = new SButton() text "Click"
this += button

is equivalent to:

button = SButton("Click")

Because the apply methods access to the layout context, it cannot be called outside of the layout context. In this case, use the new operator instead.

Method >>

As we noted, the method << returns an object which is a type of ViewGroup.LayoutParams:

val params = SButton("Click").<<   // type LayoutParams

This class provides some setters for chaining:

val params = SButton("Click").<<.marginBottom(100).marginLeft(10)   // type LayoutParams

if we want use the SButton object again, Scaloid provides >> method returning back to the object:

val button = SButton("Click").<<.marginBottom(100).marginLeft(10).>>   // type SButton

Nested layout context

When the layout context is nested, inner-most layout's context is applied:

val layout = new SFrameLayout {
  this += new SLinearLayout {
    SButton("Click").<<.Weight(1.0f).>>   // in context of SLinearLayout
  }
}

Methods fill and wrap

When we get a LayoutParams from <<, the default values of width and height properties are width = FILL_PARENT and height = WRAP_CONTENT. You can override this when you need it:

SButton("Click").<<(FILL_PARENT, FILL_PARENT)

This is a very frequently used idiom. Therefore we provide further shorthand:

SButton("Click").<<.fill

If you want the View element to be wrapped,

SButton("Click").<<(WRAP_CONTENT, WRAP_CONTENT)

This is also shortened as:

SButton("Click").<<.wrap

Naming conventions

Scaloid follows the naming conventions of XML attributes in the Android API with some improvements.

For XML attributes, layout related properties are prefixed with layout_ and as you might have guessed, Scaloid does not need it. For boolean attributes, the default is false. However, Scaloid flags it as true when the attribute is declared explicitly without any parameter. For example:

new SRelativeLayout {
  STextView("hello").<<.centerHorizontal.alignParentBottom.>>
}

Scaloid omits unnecessary ="true" for the attribute centerHorizontal. Equivalent XML layout description for TextView is:

<TextView
    android:id="@+id/helloText"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_centerHorizontal="true"
    android:layout_alignParentBottom="true"
    android:text="hello"/>

For layout methods named with four directions (e.g. ...Top, ...Right, ...Bottom and ...Left), Scaloid provides additional methods that specifies all properties at once. For example, Because Android XML layout defines margin... properties(marginTop(v:Int), marginRight(v:Int), marginBottom(v:Int) and marginLeft(v:Int)), Scaloid provides additional margin(top:Int, right:Int, bottom:Int, left:Int) and margin(amount:Int) methods that can be used as:

STextView("hello").<<.margin(5 dip, 10 dip, 5 dip, 10 dip)

or

STextView("hello").<<.margin(10 sp)  // assigns the same value for all directions

Styles for programmers

Android SDK introduced styles to reuse common properties on XML layout. We repeatedly pointed out that XML is verbose. To apply styles in Scaloid, you do not need to learn any syntax or API library, because Scaloid layout is an ordinary Scala code. Just write a code that work as styles.

Basic: Assign it individually

Suppose the following code that repeats some properties:

SButton("first").textSize(20 dip).<<.margin(5 dip).>>
SButton("prev").textSize(20 dip).<<.margin(5 dip).>>
SButton("next").textSize(20 dip).<<.margin(5 dip).>>
SButton("last").textSize(20 dip).<<.margin(5 dip).>>

Then we can define a function that applies these properties:

def myStyle = (_: SButton).textSize(20 dip).<<.margin(5 dip).>>
myStyle(SButton("first"))
myStyle(SButton("prev"))
myStyle(SButton("next"))
myStyle(SButton("last"))

Still not satisfying? Here we have a shorter one:

def myStyle = (_: SButton).textSize(20 dip).<<.margin(5 dip).>>
List("first", "prev", "next", "last").foreach(title => myStyle(SButton(title)))

Advanced: CSS-like stylesheet

Scaloid provides SViewGroup.style(View => View) method to provide more generic component styling. The parameter is a function which receives a view requested for styleing, and returns a view which is finished applying the style. Then the example in the previous subsection becomes:

style {
  case b: SButton => b.textSize(20 dip).<<.margin(5 dip).>>
}

SButton("first")
SButton("prev")
SButton("next")
SButton("last")

Note that individually applying myStyle is reduced. Let us see another example:

style {
  case b: SButton => b.textColor(Color.RED).onClick(toast("Bang!"))
  case t: STextView => t.textSize(10 dip)
  case v => v.backgroundColor(Color.YELLOW)
}

STextView("I am 10 dip tall")
STextView("Me too")
STextView("I am taller than you").textSize(15 dip) // overriding
SEditText("Yellow input field")
SButton("Red alert!")

Similar to CSS, you can assign different styles for each classes using Scala pattern matching. Unlike Android XML styles or even CSS, Scaloid can assign some actions to the component (see onclick(toast(...))), or can do anything that you imagine. Also, you can easily override the property individually, as shown in the example above.

Last thing that you may missed: These are type-safe. If you made a mistake, compiler will check it for you.

Further readings: