Many real-life UIs can be built using basic layouts and widgets you're already familiar with. To help you understand how to do this, we're going to dive deeper into LinearLayout usage scenarios and patterns in this topic.
ScrollView
When a view is bigger than its container, you can use ScrollView to make it scrollable. It is a container for wrapping a single child that is often a LinearLayout. You can see an example below:
Show XML as text
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceLarge"
android:text="End-user license agreement" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceMedium"
android:text="..." />
</LinearLayout>
</ScrollView>ScrollView is used for vertical scrolling and normally has a match_parent or fixed height. It expects its child's height to be set to wrap_content.
The HorizontalScrollView container is similar to ScrollView and is used for horizontal scrolling, as its name suggests.
LinearLayout baseline alignment
You already know that LinearLayout allows you to line up views vertically or horizontally. It also supports gravities and weights. So, what other features does LinearLayout have? Let's start by discussing baseline alignment.
Every line of text has a baseline — an imaginary horizontal line on which its letters "stand."
A horizontal LinearLayout is baselineAligned by default. This means that all TextView children share the same baseline. Consider the following layout example (note that CheckBox inherits TextView and also has a baseline):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="I accept terms " />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:text="(and read them carefully)" />
</LinearLayout>These TextViews each have a different textSize and a different height. No gravities have been specified, and you can see how the layout looks below (a gray line has been added to indicate the baseline):
The above is rendered nicely, but you need to be aware that baseline alignment can play a nasty trick on you when dealing with multiline text. The following screenshot is taken on a screen with smaller width:
Another case where problems can occur is with bordered components such as buttons:
Material theme makes buttons look like this: with background, shadow, and capitalized text.
In the above example, the left button expands its height to match the minHeight theme property. This results in the vertical distance between the text and the horizontal borders becoming bigger than the padding. However, the right button uses more height than minHeight, and the vertical space is caused by the padding, also from the theme. So, baseline alignment leads to an undesirable offset in this case.
The lesson here is that baseline alignment plays well with single-line pieces of text but gives unexpected results with multiline ones. It should therefore be explicitly disabled for every horizontal LinearLayout where this type of alignment is not intended.
Disabling baseline alignment is not the only cure. Buttons look better when they have equal height, and plain text can be forced to remain on a single line, as shown below:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false">
<!--
baselineAligned won't break this layout
because buttons have match_parent height.
However, it is disabled both for explicitness
and performance reasons.
-->
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Accept" />
<Button
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:text="Decline, exit, delete\u00A0account, and\u00A0uninstall" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:text="I accept terms " />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:ellipsize="middle"
android:singleLine="true"
android:text="(and read them carefully)" />
</LinearLayout>
</LinearLayout>LinearLayout dividers
LinearLayout can also draw dividers between children. To take advantage of this feature, you simply need to create an appropriate drawable (a horizontal line, in our case), set it as a divider, and specify where you want it to be placed:
<!-- drawable/divider.xml -->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#20000000" />
<size android:height="1dp" />
</shape><!-- layout/linear_with_dividers.xml -->
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:divider="@drawable/divider"
android:showDividers="middle|end">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textAppearance="?android:textAppearanceLarge"
android:text="Thing #1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:textAppearance="?android:textAppearanceLarge"
android:text="Thing #2" />
</LinearLayout>The value middle|end is used in the above example. This specifies that dividers should be placed between views and after the last one:
The other option is beginning, which draws a divider before the first view. You can specify any combination of these three options, including all three together if you wish: beginning|middle|end.
You could also use (some would say abuse) this feature to add spaces between views by setting android:showDividers="middle" and using an invisible <shape> drawable without a <solid color=…> tag. The benefit of this approach is that you don't need to decide whether to set the top view's layout_marginBotton or the bottom view's layout_marginTop. It also helps when you need to show or hide views programmatically and preserve consistent spacing.
LinearLayout weights
Views with a nonzero layout_weight expand to fill all the available space, which is distributed proportionally to their weight values. You can see the simplest example of this below:
<?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:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#E2E2E2"
app:title="The coolest app ever" />
<TextView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="…is not developed yet."
android:textSize="24sp" />
</LinearLayout>The wrap_content Toolbar fills the required space, and the TextView takes the rest. Its height is set to 0dp because it is irrelevant for a weighted view.
What else can be done with weights?
LinearLayout itself has a weightSum attribute and its value can differ from the actual sum of its children's weights. When the weightSum is smaller, the children fall outside the LinearLayout's bounds, which isn't very useful. But if the weightSum is bigger, the spare part serves as space. For example, we can specify that one child uses 30% and another uses 60% of the available space:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="1"
android:gravity="center_horizontal">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".3"
android:background="@android:color/holo_green_light" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight=".6"
android:background="@android:color/holo_red_light" />
</LinearLayout>These settings yield the following result:
There's also another interesting feature you should know about: layout_weight can be used together with the wrap_content size. If you take this approach and have some excess space available, the view will receive a part of it. Otherwise, wrap_content will behave in the usual way, and the content won't be squeezed. This is especially useful inside a ScrollView when content is small on big screens but can become scrollable on smaller ones.
You can see how to do this in the example below. The fillViewport attribute of ScrollView is also set to true so that its content is at least as large as the ScrollView itself:
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:textAppearance="?android:textAppearanceLarge"
android:text="End-user license agreement" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?android:textAppearanceMedium"
android:text="..." />
<!-- another two TextViews: title and content -->
</LinearLayout>
</ScrollView>This gives us space around titles on bigger screens and scrollable content on smaller ones:
TextView compound drawables
We often need to place an image and text next to each other: a product photo near its name, a profile photo above a username, or even a button with an icon. An obvious way to do this is to enclose a TextView and an ImageView in a LinearLayout. This is acceptable, but there's another option that is simpler and more performant.
A TextView has six XML attributes that enable you to attach images known as compound drawables next to its contents: drawable(Left|Start|Top|Right|End|Bottom). This means that a TextView is capable of showing up to four drawables on its own. It's also possible to distance drawables from text using the drawablePadding attribute.
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Play"
android:drawablePadding="4dp"
android:drawableTop="@android:drawable/ic_media_play" />There are, of course, some restrictions, which are listed below. But you can use a LinearLayout if you run into any of them:
gravitynever affects compound drawables; it only impacts text positioning.There is only one
drawablePaddingattribute for all four drawables (you can work around this by creating an XML drawable file with<inset>root tag and specifying different offsets).There's no
scaleTypeand many otherImageView-specific attributes are also missing.Also, there aren't separate click listeners to indicate when the specific drawable is clicked — an
OnClickListenercan only be applied to the wholeTextView.
The Java/Kotlin API related to compound drawables slightly differs from the XML attributes. There's a (get|set)CompoundDrawablePadding method pair for the drawablePadding attribute, and a bunch of methods for getting and setting drawables themselves. These can be expressed generally as setCompoundDrawables[Relative][WithIntrinsicBounds](4×(Drawable? or @DrawableRes Int)) and getCompoundDrawables[Relative](): Array<Drawable>.
The simplest one is setCompoundDrawablesWithIntrinsicBounds(left: Int, top: Int, right: Int, bottom: Int): you just pass drawable resource IDs, and it works. For example, setCompoundDrawablesWithIntrinsicBounds(0, android.R.drawable.ic_media_play, 0, 0) is similar to the XML version above. Relative versions use start and end instead of left and right. Also, both methods are overloaded to accept the Drawable? type for times when they are already instantiated or created programmatically.
So what does "with intrinsic bounds" mean? Some drawables have their own sizes (all vector ones do), and some of them don't (like color drawables). Shapes are even more variable: they can have width only, height only, both, or none. The WithIntrinsicBounds method versions use the sizes provided by drawables.
If you need to specify a drawable's size explicitly, you can only do this programmatically. Start by setting the drawables' bounds, and then call setCompoundDrawables[Relative](Drawable?, Drawable?, Drawable?, Drawable?), which never uses intrinsic bounds. The following snippet illustrates this approach by creating the whole TextView and a shape programmatically:
val sp = resources.displayMetrics.scaledDensity
TextView(context).apply {
text = "#8C5AFF"
setTextColor(Color.BLACK)
textSize = 24f
typeface = Typeface.MONOSPACE
setCompoundDrawables(
GradientDrawable().apply {
cornerRadius = 4 * sp
setColor(0xFF8C5AFF.toInt())
setBounds(0, 0, (24 * sp).toInt(), (24 * sp).toInt())
},
null,
null,
null,
)
compoundDrawablePadding = (4 * sp).toInt()
}It's important to pay attention to the setBounds method call: it accepts left, top, right, and bottom integers. In the above example, we have passed the desired width for the right parameter and the height for the bottom one. You can see how the result looks below:
Conclusion
You've learned a lot by reading this topic! You now know how to use various LinearLayout features, like baseline alignment, dividers, and weightSum. You're also familiar with ScrollView and its fillViewport flag. In addition, you've discovered that using an extra LinearLayout can be avoided when you just need a TextView with an image.