The Four Layout Types in Android Compared to iOS
The main purpose of any front-end framework to structure the UI is defining which position each element should occupy in your scene(or inside another UI element) as well as calculating its bounds in terms of size. There are 4pieces of information your application needs to know in order to draw the UI:
- Where your View's top should be pinned to
- Where your View's bottom should be pinned to
- Where your View's left edge should be pinned to
- Where your View's right edge should be pinned to.
Those insights also work to define your View's height and width as well. There may be several different terms to describe the same aspects regarding your frames. For example, in iOS we call leading the edge to the left side of your View(although it also contains a left edge that is different) and in Android we call that as start. The trailing edge in iOS also corresponds to the end in Android and so on.
There are different ways to define your layout when considering all sorts of front-end technologies, but this article aims describing the most important ones in Kotlin and how you can think of them in an iOS project(in both UIKit or SwiftUI). This article is destined to iOS developers who desire to understand how AndroidX organizes its UI, so I expect you are already familiar with Swift and UIKit/SwiftUI. Hope you enjoy ;).
Said that, let's firstly talk about what is a layout in Android/Kotlin:
Layout Containers
As you might be already an experienced front-end developer, probably you know the main difference between an UI minimum piece, such as a button, a label/text, a textfield(atoms) and a container(molecule). Containers are basically views that are the parent of other views and are meant to organize their position in some manner. In general, containers don't provide any interface to establish an interaction between the user and themselves, as the main interaction should be focused on their children. However, you can naturally apply some UI properties to them in order to customize your layout, like a background color, some orientation or maybe some style that may be applied to each child.
The containers in your front-end architecture may be aligned in a single axis(x,y), make a grid or follow any pattern that was established by your container's layout. In Android we define these containers as layouts and we are about to discuss in details each of them while tracing a parallel with iOS framework. It's worth to remember you that what exists inside the context of a layout in Android doesn't have any idea about the context its parent is inserted in, so we conclude we have 100% of encapsulation for each child.
1. ConstraintLayout
You are certainly very familiar with that since we also work with constraints in iOS(UIKit). Recapping the concepts around constraints, they are a basic unit of AutoLayout, which is an idea that makes all the information described by the constraints as a source of truth of your scene no matter the device size and model.
When defining the constraints of your View, you need to define where each edge should be pinned to define the location. Also, another associated information that should be provided is the distance between the referred edge and the outer edge where that's pinned to. We call these distances as margins. Please take a look:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/blue"
>
</View>
</androidx.constraintlayout.widget.ConstraintLayout>
As you can see, we created a simple blue view that is pinned to its parent top and end(right) edges, which results in the view being placed in the top-right corner:
Also, we defined its height and width, so no need to define other constraints for this one as we achieved our desired result. What about we want some distance to the edges its pinned to?
<View
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/blue"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
>
</View>
Now we added some space between the top and right edges, respectively 30 and 20 density pixels:
Everything is related to the parent's layout, in the case the ConstraintLayout, which is our container, but what about if we want to describe the relationship between two simblings in the same context level? Let's add a second view and change our layout a little bit:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:id="@+id/v1"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@color/blue"
android:layout_marginTop="30dp"
android:layout_marginEnd="20dp"
>
</View>
<View
android:id="@+id/v2"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toBottomOf="@id/v1"
app:layout_constraintEnd_toEndOf="@id/v1"
android:background="@color/red"
android:layout_marginTop="10dp"
>
</View>
</androidx.constraintlayout.widget.ConstraintLayout>
In this case we have two child views under our ConstraintLayout, being the blue one our original view and the second a red view whose end edge is aligned to the first view's end edge. At the same time, the red view's top edge is pinned to the blue's bottom, 10 pixels far from it:
It's basically a game where we create relationships between the edges of the views which belong to the same context. As you can see, we need an id for each view in Android in order to refer to specific pieces of UI. Certainly you are familiar with it from iOS. When we are creating storyboards, we define constraints to each dimension of our views to be respected in any sort of layout, no matter its an iPhone or iPad:
Pretty equal, right? Well, despite the way of thinking being the same, we still have some differences. We are not going to describe all of them, but it's important to know some aspects, mainly considering the way we define intrinsic content sizes in iOS.
No center constraints
In iOS, if we want a view to be center-aligned to its parent, we have center X and Y anchors to attach it to the center, but not in Android… If we want a view to be pinned only to one edge across some axis, we should define only this edge, not the second one. If we pin our view to both sides, the interface builder will interpret as it's related to both, and as a result, placed at the center:
<View
android:id="@+id/v3"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:background="@color/green"
>
</View>
If we want it pinned to the top-most edge, just remove the app:layout_constraintBottom_toBottomOf=”parent” line.
However, if you want it closer to the top edge, like in the 25% top-middle portion of the screen, you need to define a constraint vertical bias of 0.25:
<View
android:id="@+id/v3"
android:layout_width="100dp"
android:layout_height="100dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0.25"
android:background="@color/green"
>
</View>
Size Conflicts? No More
One of the most common mistakes iOS developers commit when defining constraints is pinning a view to the two opposite edges and defining height/width at the same time. However, in Android, as we already saw, constraining to both sides doesn't imply it covers the entire space, but is only a reference to them. The size of the view is always defined in the XML file, if not with the exact dimension in pixels, it's described by the inner content(as the intrinsic content size in iOS):
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Some text"
android:textStyle="bold"
android:textSize="30dp"
android:gravity="center"
android:background="@color/green"
android:layout_marginHorizontal="20dp"
>
</TextView>
We defined our width to cover the entire available space of the parent, except by a 20 pixel space from the left and right edges, and occupy as much height as the inner content(intrinsic content height):
However, if we defined a fixed height of 10 pixels, for example, it would not bring any conflict, since the content is still in there, but some of its space will be cut:
So we talked about the main aspects regarding a constraint layout in iOS and Android.
2. LinearLayout
The linear layouts have another name in iOS and other frameworks: They are the so called stacks, which correspond to UIStackView in UIKit and VStack and HStack in SwiftUI. What they are about is basically some child views that are grouped together by a parent layout and follow some axis(horizontal or vertical), being placed sequentially one after the other. They are pretty easy to write in iOS. Please take a look at this ViewCode boilerplate:
import UIKit
class ViewController: UIViewController {
private lazy var stackView: UIStackView = {
let stack = UIStackView()
stack.spacing = 12
stack.axis = .vertical
stack.translatesAutoresizingMaskIntoConstraints = false
return stack
}()
private lazy var view1: UIView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
private lazy var view2: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
private lazy var view3: UIView = {
let view = UIView()
view.backgroundColor = .green
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(stackView)
stackView.addArrangedSubview(view1)
stackView.addArrangedSubview(view2)
stackView.addArrangedSubview(view3)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
view1.heightAnchor.constraint(equalToConstant: 100),
view2.heightAnchor.constraint(equalToConstant: 100),
view3.heightAnchor.constraint(equalToConstant: 100),
])
}
}
This is what we got:
Now we have 3 views aligned in a single column through an UIStackView. By default, the stack layout separates its children with an uniform spacing measure. By default it's zero. But you can set a custom spacing after each view from iOS 11 and beyond through the setCustomSpacing method in UIStackView. Also, you can customize the organization of its children through properties like distribution and alignment.
In SwiftUI, you have HStack and VStack layouts to organize your views in columns or rows. They are more flexible than UIKit UIStackView since it's much easier to separate their children via padding. Please check the same scene we created before:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: .zero) {
Rectangle()
.fill(.blue)
.frame(height: 100)
.padding(.top, 20)
Rectangle()
.fill(.red)
.frame(height: 100)
.padding(.top, 20)
Rectangle()
.fill(.green)
.frame(height: 100)
.padding(.top, 64)
Spacer()
}
.padding(.horizontal, 20)
}
}
Noticed we added a bit more padding to the green rectangle:
We can also set the alignment of our content by injecting alignment in our VStack constructor.
In Android, we use LinearLayout to represent a Stack and we have pretty similar attributes to organize the UI elements:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="20dp"
android:layout_marginHorizontal="20dp"
android:orientation="vertical"
android:gravity="top"
>
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/blue"
android:layout_marginVertical="20dp"
>
</View>
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/red"
android:layout_marginVertical="20dp"
>
</View>
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/green"
android:layout_marginVertical="20dp"
>
</View>
</LinearLayout>
Notice we didn't configure any uniform spacing between the views, like we can in UIKit and SwiftUI. Instead, we should add layout_margin properties to each child we want some spacing. In this case, we added layout_marginVertical as we want some margin in both top and bottom edges for each view:
Notice some other properties we defined to our LinearLayout to configure its display:
- orientation: The same as the axis of the stack view
- gravity: The same as the alignment of our layout. In this case, we are shifting everything to the top(In SwiftUI we use a Spacer for this purpose).
- layout_width/layout_height: We set as match_parent to occupy the entire available space. If we wanted to define as the "intrinsic content size", we should set as wrap_content.
OBS: If we wanted to change the alignment of only one of the children, we could rely on layout_gravity , which has the same meaning as gravity, but sets the alignment of the child in relation to its parent, instead of the contrary:
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/green"
android:layout_marginVertical="20dp"
android:layout_gravity="end"
>
</View>
By setting a layout_gravity as end and a fixed width, we shifted the green view to the right edge of our LinearLayout.
Another thing to pay attention is that we use layout margins in both constraint and linear layouts. This way you can see that this is merely a concept that shifts our views in some direction instead of something exclusively related to Autolayout.
3. FrameLayout
What about if we want to place a view inside another? Of course you can do that by configuring your views to be in front of others via FrameLayout.
In UIKit you achieve this by adding a subview to a parent just after adding a previous view, like we do with UIView:
import UIKit
class ViewController: UIViewController {
private lazy var container: UIView = {
let view = UIView()
view.backgroundColor = .blue
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var view1: UIView = {
let view = UIView()
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var view2: UIView = {
let view = UIView()
view.backgroundColor = .green
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(container)
container.addSubview(view1)
container.addSubview(view2)
NSLayoutConstraint.activate([
container.centerYAnchor.constraint(equalTo: view.centerYAnchor),
container.centerXAnchor.constraint(equalTo: view.centerXAnchor),
container.widthAnchor.constraint(equalToConstant: 200),
container.heightAnchor.constraint(equalToConstant: 200),
view1.centerXAnchor.constraint(equalTo: container.centerXAnchor),
view1.centerYAnchor.constraint(equalTo: container.centerYAnchor),
view1.widthAnchor.constraint(equalToConstant: 100),
view1.heightAnchor.constraint(equalToConstant: 100),
view2.centerXAnchor.constraint(equalTo: container.centerXAnchor),
view2.centerYAnchor.constraint(equalTo: container.centerYAnchor),
view2.widthAnchor.constraint(equalToConstant: 50),
view2.heightAnchor.constraint(equalToConstant: 50),
])
}
}
We achieved the effect by adding firstly view1 before view2, both in the same container.
In SwiftUI we achieve this by using ZStack:
import SwiftUI
struct ContentView: View {
var body: some View {
ZStack {
Rectangle()
.fill(.blue)
.frame(width: 200,
height: 200)
Rectangle()
.fill(.red)
.frame(width: 100,
height: 100)
Rectangle()
.fill(.green)
.frame(width: 50,
height: 50)
Spacer()
}
.padding(.horizontal, 20)
}
}
In Android, we rely on a FrameLayout:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
>
<View
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/blue"
android:layout_gravity="center"
>
</View>
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/red"
android:layout_gravity="center"
>
</View>
<View
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@color/green"
android:layout_gravity="center"
>
</View>
</FrameLayout>
By default, all the views are placed one in front of the other by matching the top-left corner of the parent. Because of that, we need to set layout_gravity in each child view. In fact I agree the Android SDK should provide a general gravity parameter to the FrameLayout that applies to all of the children by default:
As all the other container types, FrameLayout may receive a fixed layout width and height that may cut the content instead of compressing or expanding it. However, if you place its width and height as zero or as wrap_content, it will have the same size as the largest view.
4. Relative Layout
This one may be new to most of you since iOS SDK doesn't provide a direct way to build an UI like that. However, it's a constraint layout under the hood. The only difference is that you don't configure the constraint references for each edge of your views, instead you are building your views in a structure which, as the name says, you are positioning each child related to the other. By default, each child is placed in the top-left corner of the parent, just like in a FrameLayout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/blue"
>
</View>
</RelativeLayout>
And each child position can be defined in relation to other simbling:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/v1"
android:layout_width="300dp"
android:layout_height="100dp"
android:background="@color/blue"
>
</TextView>
<View
android:id="@+id/v2"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@color/red"
android:layout_below="@id/v1"
>
</View>
<View
android:id="@+id/v3"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@color/green"
android:layout_toRightOf="@id/v2"
android:layout_below="@id/v1"
>
</View>
<View
android:id="@+id/v4"
android:layout_width="200dp"
android:layout_height="100dp"
android:background="@color/orange"
android:layout_alignEnd="@id/v1"
android:foregroundGravity="right"
>
</View>
</RelativeLayout>
What we have here:
- A blue view with 300dp width without any relative information. By default, it's placed in the top-left corner of its parent.
- A red view with 200dp width which should be placed bellow the blue one according to the layout_bellow parameter that receives the first view's id.
- A green view with 200dp width that should be placed to the right of the red one(horizontally aligned) but bellow the blue view(vertically aligned).
- An orange view with 200dp width that should align its
end
with the blue view, therefore covering some of its space(that's why we have the impression v1 is smaller than it is).
As you can see, any view inside a RelativeLayout is defined its position related to other views in the same container. You may still use the layout margins to shift them in any direction you want.
5.Absolute Layout(Extra)
I am providing this one as extra content because it's been deprecated some time ago. But the idea of an absolute layout is the same as configuring the bounds of a UIView in UIKit without any constraints, which is not recommended since we need to adapt its bounds to work in any device model:
import UIKit
class ViewController: UIViewController {
private lazy var view1: UIView = {
let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.backgroundColor = .blue
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(view1)
}
}
In Android we can achieve the same outcome with:
<?xml version="1.0" encoding="utf-8"?>
<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<View
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/blue"
android:layout_x="100dp"
android:layout_y="100dp"
>
</View>
</AbsoluteLayout>
Conclusion
This article provided a great overview regarding the four(or five?) types of layout you can place your views in Android and how you may think of them in the iOS perspective. You can position your views with constraints thinking of Autolayout(like in storyboards and View Code), you can align them in a single axis with a LinearLayout(just like stacks), you can place them one in front of the other(like with ZStacks) and also define bounds in relation to other views. I hope that improves the way you build interfaces in Android XML and you enjoyed ;).