Skip to content

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:

FrontendAndroidDescription
HTML tagsXML tagsUI element definitions
CSS stylesXML attributesStyles inlined in tags
<div><LinearLayout> / <ConstraintLayout>Containers
<p> / <span><TextView>Text display
<button><Button>Button
<input><EditText>Input field
<img><ImageView>Image
style.cssstyles.xml / themes.xmlGlobal styles

1. Layout Basics

1.1 A Simplest Layout

xml
<!-- 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 AttributeCSS EquivalentDescription
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: 16pxInner padding
android:layout_margin="16dp"margin: 16pxOuter margin

Note: layout_width and layout_height are required attributes -- every View must declare them.

1.3 ID System

xml
<!-- 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 + -->
java
// 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:

xml
<!-- 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:

XMLCSS FlexboxDescription
orientation="vertical"flex-direction: columnVertical arrangement
orientation="horizontal"flex-direction: rowHorizontal arrangement
gravity="center"justify-content + align-items: centerChild alignment
layout_gravity="center"align-self: centerSelf alignment
layout_weight="1"flex-grow: 1Distribute remaining space
dividergap (approximate)Spacing between children

2.2 ConstraintLayout

The most recommended layout -- the most powerful, with flat view hierarchies:

xml
<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 AttributeCSS EquivalentDescription
constraintStart_toStartOfleft: 0Left-align
constraintEnd_toEndOfright: 0Right-align
constraintTop_toBottomOftop: Xpx (below an element)Top constraint
constraintBottom_toBottomOfbottom: 0Bottom 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:

xml
<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

ScenarioRecommended LayoutFrontend Equivalent
Simple lists/formsLinearLayoutFlexbox (column/row)
Complex screensConstraintLayoutGrid / absolute
Overlays/containersFrameLayoutposition: relative
Scrollable contentScrollView / NestedScrollViewoverflow: auto
ListsRecyclerViewVirtual list

3. Common Controls

3.1 TextView (Text)

xml
<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 -->
java
// 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)

xml
<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>
java
// 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:

inputTypeDescription
textPlain text
textPasswordPassword
textEmailAddressEmail (keyboard shows @)
numberNumbers only
numberDecimalDecimal numbers
phonePhone number
textMultiLineMulti-line text

3.3 Button

xml
<!-- 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)

xml
<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:

scaleTypeCSS EquivalentDescription
centerCropobject-fit: coverCrop to fill
fitCenterobject-fit: containShow complete image
centerInsideobject-fit: scale-downDon't upscale
fitXYobject-fit: fillStretch to fill
java
// 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

xml
<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)

xml
<!-- 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

xml
<!-- 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

xml
<!-- 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

xml
<!-- 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)

xml
<!-- 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>
xml
<!-- 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)

xml
<!-- 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>
groovy
// app/build.gradle
android {
    buildFeatures {
        viewBinding true
    }
}
java
// 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

xml
<!-- 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" />
java
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // Show back button

7.2 Bottom Navigation Bar

xml
<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" />
xml
<!-- 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

java
// 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

java
// 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:

java
// 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 different channelIds for different importance levels so users can configure them independently.

8. Event Handling

java
// 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 event

Key rules:

MethodWhen It RunsReturn Value Meaning
dispatchTouchEventEvent reaches the Viewtrue means dispatched/consumed
onInterceptTouchEventViewGroup onlytrue intercepts; child receives no more events
onTouchEventActually handles the eventtrue consumes event; false bubbles up
java
// 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 onInterceptTouchEvent is unique to ViewGroup and has no direct frontend equivalent.

9. Creating Views Dynamically

java
// 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)

java
// 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)

java
// 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

MethodPurposeFrontend Analogy
onMeasure()Measure view width/heightBrowser layout phase
onLayout()Determine child view positionsCSS positioning
onDraw(Canvas)Draw contentCanvas 2D / SVG render

Note: invalidate() must be called on the main thread; use postInvalidate() 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:

  1. Remove unnecessary backgrounds: getWindow().setBackgroundDrawable(null)
  2. Avoid stacking same-color backgrounds in multiple FrameLayouts
  3. Use clipChildren / clipToPadding to limit drawing area

11.2 Reduce Layout Depth

xml
<!-- ❌ 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:

xml
<!-- ❌ Don't do this -->
<ScrollView>
    <RecyclerView />
</ScrollView>

<!-- ✅ Use RecyclerView's multiple item types or ConcatAdapter -->
<RecyclerView />

11.4 List Item Inflation Optimization

java
// ❌ 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 examples

12. include / merge / ViewStub

12.1 include: Reuse Layouts

xml
<!-- 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

xml
<!-- 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

xml
<ViewStub
    android:id="@+id/stub_import"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout="@layout/panel_import" />
java
ViewStub stub = findViewById(R.id.stub_import);
stub.inflate();  // load on demand, saving initial layout time and memory

Frontend analogy: include is like component imports; merge is like Vue/React Fragments (no extra DOM node); ViewStub is like React's React.lazy / conditional rendering.

Frontend Developer Cheat Sheet

CSS ConceptAndroid XML EquivalentNotes
display: flex<LinearLayout>Linear arrangement
flex-directionandroid:orientationArrangement direction
justify-contentandroid:gravityChild alignment
align-selfandroid:layout_gravitySelf alignment
flex-growandroid:layout_weightDistribute remaining space
position: relative<FrameLayout>Overlapping layers
position: absolute<ConstraintLayout> constraintsPrecise positioning
border-radius<corners android:radius>Rounded corners
background-color<solid android:color>Background color
border<stroke>Border
:hover / :active<selector> + state_pressedState-based styles
overflow: auto<ScrollView>Scrolling
object-fitandroid:scaleTypeImage scaling
font-sizeandroid:textSize (sp)Font size
colorandroid:textColorText color
CSS class namesstyle="@style/Xxx"Style reuse
CSS variables / :rootvalues/colors.xml + themesGlobal 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.

A Java Android development tutorial for frontend developers