Module 05: UI Layouts & Controls
Objective: Master the Android XML layout system, understand how it maps to HTML/CSS, and be able to read and modify UI code in legacy projects.
Core Difference: Declarative vs Imperative
Android's UI system is XML-declarative, similar to HTML but more structured:
| Frontend | Android | Description |
|---|---|---|
| HTML tags | XML tags | UI element definitions |
| CSS styles | XML attributes | Styles inlined in tags |
<div> | <LinearLayout> / <ConstraintLayout> | Containers |
<p> / <span> | <TextView> | Text display |
<button> | <Button> | Button |
<input> | <EditText> | Input field |
<img> | <ImageView> | Image |
style.css | styles.xml / themes.xml | Global styles |
1. Layout Basics
1.1 A Simplest Layout
<!-- res/layout/activity_main.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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, World"
android:textSize="24sp"
android:textColor="@color/black" />
<Button
android:id="@+id/btn_click"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Click Me"
android:layout_marginTop="16dp" />
</LinearLayout>1.2 Size Attribute Comparison
| XML Attribute | CSS Equivalent | Description |
|---|---|---|
android:layout_width="match_parent" | width: 100% | Fill parent container |
android:layout_width="wrap_content" | width: auto (fit-content) | Auto-size to content |
android:layout_width="200dp" | width: 200px (approximate) | Fixed width |
android:layout_height="match_parent" | height: 100% | Fill parent container |
android:padding="16dp" | padding: 16px | Inner padding |
android:layout_margin="16dp" | margin: 16px | Outer margin |
Note:
layout_widthandlayout_heightare required attributes -- every View must declare them.
1.3 ID System
<!-- Define an ID -->
<TextView
android:id="@+id/tv_title" <!-- @+id/ means creating a new ID -->
... />
<!-- Reference an existing ID (e.g. in ConstraintLayout) -->
app:layout_constraintTop_toBottomOf="@id/tv_title" <!-- @id/ without + -->// Find a view by ID in Java code (similar to document.getElementById)
TextView title = findViewById(R.id.tv_title);
Button btn = findViewById(R.id.btn_click);2. Layout Containers
2.1 LinearLayout
The most basic layout, similar to CSS Flexbox with single-direction arrangement:
<!-- Vertical arrangement (like flex-direction: column) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal" <!-- Horizontally center children -->
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subtitle" />
</LinearLayout>
<!-- Horizontal arrangement (like flex-direction: row) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"> <!-- Vertically center children -->
<!-- weight attribute: similar to flex-grow -->
<EditText
android:layout_width="0dp" <!-- Set to 0dp when used with weight -->
android:layout_height="wrap_content"
android:layout_weight="1" <!-- Occupy remaining space -->
android:hint="Enter content" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send" />
</LinearLayout>LinearLayout Attribute Comparison:
| XML | CSS Flexbox | Description |
|---|---|---|
orientation="vertical" | flex-direction: column | Vertical arrangement |
orientation="horizontal" | flex-direction: row | Horizontal arrangement |
gravity="center" | justify-content + align-items: center | Child alignment |
layout_gravity="center" | align-self: center | Self alignment |
layout_weight="1" | flex-grow: 1 | Distribute remaining space |
divider | gap (approximate) | Spacing between children |
2.2 ConstraintLayout
The most recommended layout -- the most powerful, with flat view hierarchies:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<!-- Avatar: top-left corner -->
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_avatar" />
<!-- Name: to the right of the avatar -->
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toEndOf="@id/iv_avatar"
app:layout_constraintTop_toTopOf="@id/iv_avatar"
android:layout_marginStart="12dp"
android:text="John Doe"
android:textSize="18sp" />
<!-- Description: below the name -->
<TextView
android:id="@+id/tv_desc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="@id/tv_name"
app:layout_constraintTop_toBottomOf="@id/tv_name"
android:text="Android Developer"
android:textColor="@color/text_secondary" />
<!-- Button: bottom-right corner -->
<Button
android:id="@+id/btn_follow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:text="Follow" />
</androidx.constraintlayout.widget.ConstraintLayout>ConstraintLayout is similar to CSS Grid / absolute positioning: it positions elements through constraint relationships.
| Constraint Attribute | CSS Equivalent | Description |
|---|---|---|
constraintStart_toStartOf | left: 0 | Left-align |
constraintEnd_toEndOf | right: 0 | Right-align |
constraintTop_toBottomOf | top: Xpx (below an element) | Top constraint |
constraintBottom_toBottomOf | bottom: 0 | Bottom constraint |
layout_constraintHorizontal_bias | (no direct equivalent) | Horizontal bias ratio |
2.3 FrameLayout
Similar to CSS position: relative, with child elements stacked on top of each other:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<!-- Background image -->
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/banner"
android:scaleType="centerCrop" />
<!-- Text overlaid on top -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:layout_margin="16dp"
android:text="Title Overlay"
android:textColor="@color/white" />
</FrameLayout>2.4 Layout Selection Guide
| Scenario | Recommended Layout | Frontend Equivalent |
|---|---|---|
| Simple lists/forms | LinearLayout | Flexbox (column/row) |
| Complex screens | ConstraintLayout | Grid / absolute |
| Overlays/containers | FrameLayout | position: relative |
| Scrollable content | ScrollView / NestedScrollView | overflow: auto |
| Lists | RecyclerView | Virtual list |
3. Common Controls
3.1 TextView (Text)
<TextView
android:id="@+id/tv_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is a text string"
android:textSize="16sp" <!-- Font size -->
android:textColor="@color/text_primary" <!-- Font color -->
android:textStyle="bold" <!-- Bold: normal/bold/italic -->
android:maxLines="2" <!-- Max lines -->
android:ellipsize="end" <!-- Show ... when overflow -->
android:lineSpacingExtra="4dp" <!-- Line spacing -->
android:gravity="center" <!-- Text alignment -->
android:drawableStart="@drawable/ic_star" <!-- Left icon -->
android:drawablePadding="8dp" /> <!-- Spacing between icon and text -->// Manipulate in code
TextView tv = findViewById(R.id.tv_info);
tv.setText("New text");
tv.setTextColor(Color.RED);
tv.setTextSize(18);
// Set HTML content (similar to innerHTML)
tv.setText(Html.fromHtml("<b>Bold</b> <i>Italic</i> <font color='red'>Red</font>",
Html.FROM_HTML_MODE_LEGACY));
// Clickable text (similar to <a> tags)
ClickableSpan span = new ClickableSpan() {
@Override
public void onClick(View widget) {
// Handle click
}
};
SpannableString spannable = new SpannableString("Click here for details");
spannable.setSpan(span, 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(spannable);
tv.setMovementMethod(LinkMovementMethod.getInstance());3.2 EditText (Input Field)
<EditText
android:id="@+id/et_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter username" <!-- Placeholder text -->
android:inputType="text" <!-- Input type -->
android:maxLength="20" <!-- Max length -->
android:singleLine="true" <!-- Single line -->
android:textSize="16sp"
android:padding="12dp"
android:background="@drawable/bg_input" />
<!-- Password input -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter password"
android:inputType="textPassword" /> <!-- Password masking -->
<!-- Numeric input -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter amount"
android:inputType="numberDecimal" /> <!-- Decimal number -->
<!-- Material Design input (recommended) -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Username"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>// Get input content
EditText et = findViewById(R.id.et_input);
String text = et.getText().toString();
// Listen for text changes
et.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// React to input in real time (similar to React's onChange)
}
@Override
public void afterTextChanged(Editable s) {}
});Common inputType Values:
| inputType | Description |
|---|---|
text | Plain text |
textPassword | Password |
textEmailAddress | Email (keyboard shows @) |
number | Numbers only |
numberDecimal | Decimal numbers |
phone | Phone number |
textMultiLine | Multi-line text |
3.3 Button
<!-- Basic button -->
<Button
android:id="@+id/btn_submit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:text="Submit"
android:textSize="16sp"
android:backgroundTint="@color/primary"
android:textColor="@color/white" />
<!-- Material Button (recommended) -->
<com.google.android.material.button.MaterialButton
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Primary Action"
app:cornerRadius="8dp" />
<!-- Outlined button -->
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Secondary Action"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" />
<!-- Text-only button -->
<com.google.android.material.button.MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Text Button"
style="@style/Widget.MaterialComponents.Button.TextButton" />3.4 ImageView (Image)
<ImageView
android:id="@+id/iv_photo"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/photo" <!-- Image resource -->
android:scaleType="centerCrop" <!-- Scale mode -->
android:contentDescription="User avatar" /> <!-- Accessibility description -->scaleType Comparison:
| scaleType | CSS Equivalent | Description |
|---|---|---|
centerCrop | object-fit: cover | Crop to fill |
fitCenter | object-fit: contain | Show complete image |
centerInside | object-fit: scale-down | Don't upscale |
fitXY | object-fit: fill | Stretch to fill |
// Load a network image with Glide (most common approach)
ImageView iv = findViewById(R.id.iv_photo);
Glide.with(this)
.load("https://example.com/image.jpg")
.placeholder(R.drawable.ic_placeholder) // Placeholder while loading
.error(R.drawable.ic_error) // Error image on failure
.circleCrop() // Circular crop
.into(iv);3.5 CheckBox / RadioButton / Switch
<CheckBox
android:id="@+id/cb_agree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="I agree to the Terms of Service"
android:checked="false" />
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/rb_male"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Male" />
<RadioButton
android:id="@+id/rb_female"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Female" />
</RadioGroup>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_dark_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dark Mode" />3.6 ScrollView (Scrollable Container)
<!-- Vertical scroll (most common) -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- ScrollView can only have one direct child -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- All content goes here -->
</LinearLayout>
</ScrollView>
<!-- Nested scroll (when used inside RecyclerView, etc.) -->
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- ... -->
</androidx.core.widget.NestedScrollView>4. Drawable Resources
4.1 Shape
<!-- res/drawable/bg_rounded_rect.xml -->
<!-- Similar to CSS: border-radius + background-color + border -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFFFFF" /> <!-- Background color -->
<corners android:radius="8dp" /> <!-- Corner radius -->
<stroke android:width="1dp" android:color="#E0E0E0" /> <!-- Border -->
<padding
android:left="16dp"
android:top="12dp"
android:right="16dp"
android:bottom="12dp" />
</shape>4.2 Selector
<!-- res/drawable/bg_button_selector.xml -->
<!-- Similar to CSS: :hover, :active, :disabled pseudo-classes -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Pressed state -->
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="#1976D2" />
<corners android:radius="4dp" />
</shape>
</item>
<!-- Disabled state -->
<item android:state_enabled="false">
<shape android:shape="rectangle">
<solid android:color="#BDBDBD" />
<corners android:radius="4dp" />
</shape>
</item>
<!-- Default state -->
<item>
<shape android:shape="rectangle">
<solid android:color="#2196F3" />
<corners android:radius="4dp" />
</shape>
</item>
</selector>4.3 Vector Drawable
<!-- res/drawable/ic_check.xml -->
<!-- Similar to SVG, used for custom icons -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z" />
</vector>5. Styles & Themes
5.1 Styles (Similar to CSS Classes)
<!-- res/values/styles.xml -->
<resources>
<!-- Base style -->
<style name="TitleText">
<item name="android:textSize">20sp</item>
<item name="android:textColor">@color/text_primary</item>
<item name="android:textStyle">bold</item>
</style>
<!-- Style inheritance (similar to CSS inheritance) -->
<style name="TitleText.Large">
<item name="android:textSize">28sp</item>
</style>
<!-- Button style -->
<style name="PrimaryButton" parent="Widget.MaterialComponents.Button">
<item name="cornerRadius">8dp</item>
<item name="android:textSize">16sp</item>
</style>
</resources><!-- Apply a style in a layout -->
<TextView
style="@style/TitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title Text" />5.2 Themes (Global Application)
<!-- res/values/themes.xml -->
<resources>
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight">
<!-- Primary color (affects AppBar, buttons, etc.) -->
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary color -->
<item name="colorSecondary">@color/accent</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
</style>
</resources>6. ViewBinding (Recommended Replacement for findViewById)
// app/build.gradle
android {
buildFeatures {
viewBinding true
}
}// Using ViewBinding (auto-generated binding class)
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Access views directly via binding (type-safe, no findViewById needed)
binding.tvTitle.setText("Hello!");
binding.btnClick.setOnClickListener(v -> {
binding.tvTitle.setText("Button clicked");
});
}
}In legacy projects: most use
findViewById; newer projects use ViewBinding or ButterKnife.
7. Common UI Patterns
7.1 Toolbar / ActionBar
<!-- At the top of the layout -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="Home"
app:navigationIcon="@drawable/ic_back" />Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Show back button7.2 Bottom Navigation Bar
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu" /><!-- res/menu/bottom_nav_menu.xml -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/nav_home"
android:icon="@drawable/ic_home"
android:title="Home" />
<item android:id="@+id/nav_search"
android:icon="@drawable/ic_search"
android:title="Discover" />
<item android:id="@+id/nav_profile"
android:icon="@drawable/ic_person"
android:title="Profile" />
</menu>7.3 Dialog
// AlertDialog (most common)
new AlertDialog.Builder(this)
.setTitle("Confirm Delete")
.setMessage("Are you sure you want to delete this record? This action cannot be undone.")
.setPositiveButton("Delete", (dialog, which) -> {
// Perform deletion
})
.setNegativeButton("Cancel", null)
.show();
// Custom Dialog
Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_custom);
dialog.getWindow().setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
Button btnConfirm = dialog.findViewById(R.id.btn_confirm);
btnConfirm.setOnClickListener(v -> dialog.dismiss());
dialog.show();7.4 Toast & Snackbar
// Toast (simple notification)
Toast.makeText(this, "Operation successful", Toast.LENGTH_SHORT).show();
// Snackbar (notification with action, recommended)
Snackbar.make(view, "1 record deleted", Snackbar.LENGTH_LONG)
.setAction("Undo", v -> {
// Undo deletion
})
.show();7.5 Notification Channels (Android 8.0+)
Starting with Android 8.0, all notifications must be assigned to a notification channel. Legacy projects without channels won't show notifications:
// From android3 chapter08 NotifyUtil
@TargetApi(Build.VERSION_CODES.O)
public static void createNotifyChannel(Context ctx, String channelId,
String channelName, int importance) {
NotificationManager notifyMgr = (NotificationManager)
ctx.getSystemService(Context.NOTIFICATION_SERVICE);
if (notifyMgr.getNotificationChannel(channelId) == null) {
NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
channel.setSound(null, null); // silent
channel.enableLights(true); // LED
channel.enableVibration(true); // vibration
channel.setShowBadge(true); // launcher badge
channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
notifyMgr.createNotificationChannel(channel);
}
}Best practice: Initialize notification channels in
Application.onCreate(); use differentchannelIds for different importance levels so users can configure them independently.
8. Event Handling
// Click event
view.setOnClickListener(v -> { /* Handle click */ });
// Long-press event
view.setOnLongClickListener(v -> {
// Return true to indicate the event was consumed
return true;
});
// Touch event (lowest level)
view.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: // Finger down
break;
case MotionEvent.ACTION_MOVE: // Finger move
break;
case MotionEvent.ACTION_UP: // Finger up
break;
}
return true;
});
// Focus change
editText.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
// Gained focus
} else {
// Lost focus -- good place for form validation
validateInput();
}
});Touch Event Dispatch Mechanism
Android touch events travel from outside to inside, then back out if not consumed:
Activity.dispatchTouchEvent()
↓
ViewGroup.dispatchTouchEvent()
↓
ViewGroup.onInterceptTouchEvent() // whether to intercept
↓ (not intercepted)
Child View.dispatchTouchEvent()
↓
Child View.onTouchEvent() // consume eventKey rules:
| Method | When It Runs | Return Value Meaning |
|---|---|---|
dispatchTouchEvent | Event reaches the View | true means dispatched/consumed |
onInterceptTouchEvent | ViewGroup only | true intercepts; child receives no more events |
onTouchEvent | Actually handles the event | true consumes event; false bubbles up |
// Simplified from android3 chapter11 SingleTouchView
public class SingleTouchView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// record start point
break;
case MotionEvent.ACTION_MOVE:
// draw path
break;
case MotionEvent.ACTION_UP:
// trigger completion callback
break;
}
postInvalidate(); // thread-safe redraw request
return true; // must return true to keep receiving events
}
}Frontend analogy: Event dispatch is similar to DOM capture → target → bubble, but Android's
onInterceptTouchEventis unique to ViewGroup and has no direct frontend equivalent.
9. Creating Views Dynamically
// Create views dynamically in code (not recommended -- prefer XML)
LinearLayout container = findViewById(R.id.container);
TextView dynamicText = new TextView(this);
dynamicText.setText("Dynamically created text");
dynamicText.setTextSize(16);
dynamicText.setTextColor(Color.BLACK);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.setMargins(0, 16, 0, 0);
dynamicText.setLayoutParams(params);
container.addView(dynamicText);
// Inflate an XML layout into a View (recommended approach)
LayoutInflater inflater = LayoutInflater.from(this);
View itemView = inflater.inflate(R.layout.item_card, container, false);
container.addView(itemView);10. Custom Views
When system widgets aren't enough (e.g., signature pad, gauge, chart), you need a custom view.
10.1 Extending an Existing Widget (Simple Cases)
// From android3 chapter08 CustomButton: extend Button and apply a custom style
@SuppressLint("AppCompatCustomView")
public class CustomButton extends Button {
public CustomButton(Context context) {
super(context);
}
public CustomButton(Context context, AttributeSet attrs) {
// Use defStyleRes to specify default style R.style.CommonButton
this(context, attrs, 0, R.style.CommonButton);
}
public CustomButton(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
}10.2 Extending View Directly (Full Custom Drawing)
// Simplified from android3 chapter11 SingleTouchView
public class SingleTouchView extends View {
private Paint mPathPaint;
private Path mPath = new Path();
public SingleTouchView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
private void initView() {
mPathPaint = new Paint();
mPathPaint.setStrokeWidth(5);
mPathPaint.setStyle(Paint.Style.STROKE);
mPathPaint.setColor(Color.BLACK);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawPath(mPath, mPathPaint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mPath.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
mPath.lineTo(event.getX(), event.getY());
break;
}
postInvalidate(); // thread-safe redraw request
return true; // must return true to keep receiving events
}
}10.3 The Custom View Trio
| Method | Purpose | Frontend Analogy |
|---|---|---|
onMeasure() | Measure view width/height | Browser layout phase |
onLayout() | Determine child view positions | CSS positioning |
onDraw(Canvas) | Draw content | Canvas 2D / SVG render |
Note:
invalidate()must be called on the main thread; usepostInvalidate()from background threads.
11. Layout Performance and Common Pitfalls
11.1 Avoid Overdraw
Overdraw = the same pixel is drawn multiple times. Enable Developer Options → Debug GPU Overdraw to visualize:
- Blue: 1x overdraw
- Green: 2x
- Pink: 3x
- Red: 4x+
Optimization tips:
- Remove unnecessary backgrounds:
getWindow().setBackgroundDrawable(null) - Avoid stacking same-color backgrounds in multiple
FrameLayouts - Use
clipChildren/clipToPaddingto limit drawing area
11.2 Reduce Layout Depth
<!-- ❌ Deep nesting -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- ✅ Flatten with ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView />
</androidx.constraintlayout.widget.ConstraintLayout>11.3 RecyclerView Inside ScrollView/NestedScrollView
RecyclerView already handles view recycling. Wrapping it in a ScrollView forces all items to be created at once, defeating recycling:
<!-- ❌ Don't do this -->
<ScrollView>
<RecyclerView />
</ScrollView>
<!-- ✅ Use RecyclerView's multiple item types or ConcatAdapter -->
<RecyclerView />11.4 List Item Inflation Optimization
// ❌ Creating new LayoutParams every time
View view = inflater.inflate(R.layout.item, parent, false);
// ✅ Reuse convertView (ListView) or ViewHolder (RecyclerView)
// See Module 06 for BaseAdapter / RecyclerView examples12. include / merge / ViewStub
12.1 include: Reuse Layouts
<!-- res/layout/toolbar_common.xml -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<!-- Reference in another layout -->
<include layout="@layout/toolbar_common"
android:id="@+id/toolbar_main" />12.2 merge: Reduce One Root Level
<!-- res/layout/item_merge.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageView android:id="@+id/iv_icon" ... />
<TextView android:id="@+id/tv_name" ... />
</merge>merge itself does not create a View node; it merges children into the parent layout, making it ideal as the root of an <include> layout.
12.3 ViewStub: Lazy Loading
<ViewStub
android:id="@+id/stub_import"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/panel_import" />ViewStub stub = findViewById(R.id.stub_import);
stub.inflate(); // load on demand, saving initial layout time and memoryFrontend analogy:
includeis like component imports;mergeis like Vue/React Fragments (no extra DOM node);ViewStubis like React'sReact.lazy/ conditional rendering.
Frontend Developer Cheat Sheet
| CSS Concept | Android XML Equivalent | Notes |
|---|---|---|
display: flex | <LinearLayout> | Linear arrangement |
flex-direction | android:orientation | Arrangement direction |
justify-content | android:gravity | Child alignment |
align-self | android:layout_gravity | Self alignment |
flex-grow | android:layout_weight | Distribute remaining space |
position: relative | <FrameLayout> | Overlapping layers |
position: absolute | <ConstraintLayout> constraints | Precise positioning |
border-radius | <corners android:radius> | Rounded corners |
background-color | <solid android:color> | Background color |
border | <stroke> | Border |
:hover / :active | <selector> + state_pressed | State-based styles |
overflow: auto | <ScrollView> | Scrolling |
object-fit | android:scaleType | Image scaling |
font-size | android:textSize (sp) | Font size |
color | android:textColor | Text color |
| CSS class names | style="@style/Xxx" | Style reuse |
| CSS variables / :root | values/colors.xml + themes | Global variables |
Next Steps
Now that you've mastered UI layouts and controls, move on to Module 06: RecyclerView & List Rendering -- the most important data display component in Android.