Skip to content

模块 05:UI 布局与控件

目标:掌握 Android XML 布局系统,理解其与 HTML/CSS 的对应关系,能够阅读和修改老项目的界面代码。

核心差异:声明式 vs 命令式

Android 的 UI 系统是 XML 声明式的,类似 HTML 但更结构化:

前端Android说明
HTML 标签XML 标签UI 元素定义
CSS 样式XML 属性样式内联在标签中
<div><LinearLayout> / <ConstraintLayout>容器
<p> / <span><TextView>文本显示
<button><Button>按钮
<input><EditText>输入框
<img><ImageView>图片
style.cssstyles.xml / themes.xml全局样式

1. 布局基础

1.1 一个最简单的布局

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="你好,世界"
        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="点击我"
        android:layout_marginTop="16dp" />

</LinearLayout>

1.2 尺寸属性对照

XML 属性CSS 对应说明
android:layout_width="match_parent"width: 100%撑满父容器
android:layout_width="wrap_content"width: auto (fit-content)根据内容自适应
android:layout_width="200dp"width: 200px (近似)固定宽度
android:layout_height="match_parent"height: 100%撑满父容器
android:padding="16dp"padding: 16px内边距
android:layout_margin="16dp"margin: 16px外边距

⚠️ 注意layout_widthlayout_height必须属性,每个 View 都必须声明。

1.3 ID 系统

xml
<!-- 定义 ID -->
<TextView
    android:id="@+id/tv_title"   <!-- @+id/ 表示创建新 ID -->
    ... />

<!-- 引用已有 ID(如约束布局中) -->
app:layout_constraintTop_toBottomOf="@id/tv_title"  <!-- @id/ 不带 + -->
java
// 在 Java 代码中通过 ID 查找视图(类似 document.getElementById)
TextView title = findViewById(R.id.tv_title);
Button btn = findViewById(R.id.btn_click);

2. 布局容器

2.1 LinearLayout(线性布局)

最基础的布局,类似 CSS 的 Flexbox 单方向排列:

xml
<!-- 垂直排列(类似 flex-direction: column) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:gravity="center_horizontal"    <!-- 子元素水平居中 -->
    android:padding="16dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="标题" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="副标题" />

</LinearLayout>

<!-- 水平排列(类似 flex-direction: row) -->
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:gravity="center_vertical">      <!-- 子元素垂直居中 -->

    <!-- weight 属性:类似 flex-grow -->
    <EditText
        android:layout_width="0dp"           <!-- 配合 weight 使用时设为 0dp -->
        android:layout_height="wrap_content"
        android:layout_weight="1"            <!-- 占据剩余空间 -->
        android:hint="输入内容" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="发送" />

</LinearLayout>

LinearLayout 属性对照

XMLCSS Flexbox说明
orientation="vertical"flex-direction: column垂直排列
orientation="horizontal"flex-direction: row水平排列
gravity="center"justify-content + align-items: center子元素对齐
layout_gravity="center"align-self: center自身对齐
layout_weight="1"flex-grow: 1分配剩余空间
dividergap(近似)子元素间距

2.2 ConstraintLayout(约束布局)

最推荐的布局,功能最强大,扁平化层级:

xml
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <!-- 头像:左上角 -->
    <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" />

    <!-- 名字:头像右侧 -->
    <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="张三"
        android:textSize="18sp" />

    <!-- 描述:名字下方 -->
    <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 开发者"
        android:textColor="@color/text_secondary" />

    <!-- 按钮:右下角 -->
    <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="关注" />

</androidx.constraintlayout.widget.ConstraintLayout>

ConstraintLayout 类似 CSS Grid / absolute positioning:通过约束关系定位元素。

约束属性CSS 对应说明
constraintStart_toStartOfleft: 0左边对齐
constraintEnd_toEndOfright: 0右边对齐
constraintTop_toBottomOftop: Xpx (在某元素下方)上方约束
constraintBottom_toBottomOfbottom: 0下方约束
layout_constraintHorizontal_bias(无直接对应)水平偏移比例

2.3 FrameLayout(帧布局)

类似 CSS 的 position: relative,子元素层层叠加:

xml
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="200dp">

    <!-- 背景图 -->
    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/banner"
        android:scaleType="centerCrop" />

    <!-- 叠加在上面的文字 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|start"
        android:layout_margin="16dp"
        android:text="标题叠加层"
        android:textColor="@color/white" />

</FrameLayout>

2.4 布局选择指南

场景推荐布局前端对应
简单列表/表单LinearLayoutFlexbox (column/row)
复杂页面ConstraintLayoutGrid / absolute
叠加层/容器FrameLayoutposition: relative
滚动内容ScrollView / NestedScrollViewoverflow: auto
列表RecyclerView虚拟列表

3. 常用控件

3.1 TextView(文本)

xml
<TextView
    android:id="@+id/tv_info"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="这是一段文本"
    android:textSize="16sp"                  <!-- 字体大小 -->
    android:textColor="@color/text_primary"   <!-- 字体颜色 -->
    android:textStyle="bold"                  <!-- 粗体:normal/bold/italic -->
    android:maxLines="2"                      <!-- 最大行数 -->
    android:ellipsize="end"                   <!-- 超出显示... -->
    android:lineSpacingExtra="4dp"            <!-- 行间距 -->
    android:gravity="center"                  <!-- 文本对齐 -->
    android:drawableStart="@drawable/ic_star" <!-- 左侧图标 -->
    android:drawablePadding="8dp" />          <!-- 图标与文字间距 -->
java
// 在代码中操作
TextView tv = findViewById(R.id.tv_info);
tv.setText("新的文本");
tv.setTextColor(Color.RED);
tv.setTextSize(18);

// 设置 HTML 内容(类似 innerHTML)
tv.setText(Html.fromHtml("<b>粗体</b> <i>斜体</i> <font color='red'>红色</font>",
    Html.FROM_HTML_MODE_LEGACY));

// 可点击的文本(类似 <a> 标签)
ClickableSpan span = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        // 处理点击
    }
};
SpannableString spannable = new SpannableString("点击这里查看详情");
spannable.setSpan(span, 0, 4, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(spannable);
tv.setMovementMethod(LinkMovementMethod.getInstance());

3.2 EditText(输入框)

xml
<EditText
    android:id="@+id/et_input"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="请输入用户名"              <!-- 占位文本 -->
    android:inputType="text"                <!-- 输入类型 -->
    android:maxLength="20"                  <!-- 最大长度 -->
    android:singleLine="true"               <!-- 单行 -->
    android:textSize="16sp"
    android:padding="12dp"
    android:background="@drawable/bg_input" />

<!-- 密码输入框 -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="请输入密码"
    android:inputType="textPassword" />      <!-- 密码遮掩 -->

<!-- 数字输入 -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="请输入金额"
    android:inputType="numberDecimal" />     <!-- 带小数点的数字 -->

<!-- Material Design 输入框(推荐) -->
<com.google.android.material.textfield.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="用户名"
    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
// 获取输入内容
EditText et = findViewById(R.id.et_input);
String text = et.getText().toString();

// 监听文本变化
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 的 onChange)
    }

    @Override
    public void afterTextChanged(Editable s) {}
});

常用 inputType 值

inputType说明
text普通文本
textPassword密码
textEmailAddress邮箱(键盘显示 @)
number纯数字
numberDecimal带小数点数字
phone电话号码
textMultiLine多行文本

3.3 Button

xml
<!-- 基础按钮 -->
<Button
    android:id="@+id/btn_submit"
    android:layout_width="match_parent"
    android:layout_height="48dp"
    android:text="提交"
    android:textSize="16sp"
    android:backgroundTint="@color/primary"
    android:textColor="@color/white" />

<!-- Material Button(推荐) -->
<com.google.android.material.button.MaterialButton
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="主要操作"
    app:cornerRadius="8dp" />

<!-- 边框按钮 -->
<com.google.android.material.button.MaterialButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="次要操作"
    style="@style/Widget.MaterialComponents.Button.OutlinedButton" />

<!-- 纯文本按钮 -->
<com.google.android.material.button.MaterialButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="文字按钮"
    style="@style/Widget.MaterialComponents.Button.TextButton" />

3.4 ImageView(图片)

xml
<ImageView
    android:id="@+id/iv_photo"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:src="@drawable/photo"           <!-- 图片资源 -->
    android:scaleType="centerCrop"          <!-- 缩放模式 -->
    android:contentDescription="用户头像" />  <!-- 无障碍描述 -->

scaleType 对照

scaleTypeCSS 对应说明
centerCropobject-fit: cover裁切填充
fitCenterobject-fit: contain完整显示
centerInsideobject-fit: scale-down不放大
fitXYobject-fit: fill拉伸填充
java
// 使用 Glide 加载网络图片(最常用)
ImageView iv = findViewById(R.id.iv_photo);
Glide.with(this)
    .load("https://example.com/image.jpg")
    .placeholder(R.drawable.ic_placeholder)   // 加载中占位图
    .error(R.drawable.ic_error)                // 加载失败图
    .circleCrop()                              // 圆形裁切
    .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="我同意用户协议"
    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="男" />

    <RadioButton
        android:id="@+id/rb_female"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="女" />

</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="暗黑模式" />

3.6 ScrollView(滚动容器)

xml
<!-- 垂直滚动(最常用) -->
<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- ScrollView 只能有一个直接子元素 -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <!-- 所有内容放在这里 -->

    </LinearLayout>
</ScrollView>

<!-- 嵌套滚动(在 RecyclerView 等内部使用时) -->
<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <!-- ... -->
</androidx.core.widget.NestedScrollView>

4. Drawable 资源

4.1 形状(Shape)

xml
<!-- res/drawable/bg_rounded_rect.xml -->
<!-- 类似 CSS: border-radius + background-color + border -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">

    <solid android:color="#FFFFFF" />                    <!-- 背景色 -->
    <corners android:radius="8dp" />                     <!-- 圆角 -->
    <stroke android:width="1dp" android:color="#E0E0E0" /> <!-- 边框 -->
    <padding
        android:left="16dp"
        android:top="12dp"
        android:right="16dp"
        android:bottom="12dp" />

</shape>

4.2 选择器(Selector)

xml
<!-- res/drawable/bg_button_selector.xml -->
<!-- 类似 CSS: :hover, :active, :disabled 伪类 -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 按下状态 -->
    <item android:state_pressed="true">
        <shape android:shape="rectangle">
            <solid android:color="#1976D2" />
            <corners android:radius="4dp" />
        </shape>
    </item>

    <!-- 禁用状态 -->
    <item android:state_enabled="false">
        <shape android:shape="rectangle">
            <solid android:color="#BDBDBD" />
            <corners android:radius="4dp" />
        </shape>
    </item>

    <!-- 默认状态 -->
    <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 -->
<!-- 类似 SVG,用于自定义图标 -->
<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. 样式与主题

5.1 样式(类似 CSS 类)

xml
<!-- res/values/styles.xml -->
<resources>
    <!-- 基础样式 -->
    <style name="TitleText">
        <item name="android:textSize">20sp</item>
        <item name="android:textColor">@color/text_primary</item>
        <item name="android:textStyle">bold</item>
    </style>

    <!-- 继承样式(类似 CSS 的继承) -->
    <style name="TitleText.Large">
        <item name="android:textSize">28sp</item>
    </style>

    <!-- 按钮样式 -->
    <style name="PrimaryButton" parent="Widget.MaterialComponents.Button">
        <item name="cornerRadius">8dp</item>
        <item name="android:textSize">16sp</item>
    </style>
</resources>
xml
<!-- 在布局中使用样式 -->
<TextView
    style="@style/TitleText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="标题文本" />

5.2 主题(全局应用)

xml
<!-- res/values/themes.xml -->
<resources>
    <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight">
        <!-- 主色调(影响 AppBar、按钮等) -->
        <item name="colorPrimary">@color/primary</item>
        <item name="colorPrimaryVariant">@color/primary_dark</item>
        <item name="colorOnPrimary">@color/white</item>

        <!-- 次要色调 -->
        <item name="colorSecondary">@color/accent</item>
        <item name="colorOnSecondary">@color/black</item>

        <!-- 状态栏 -->
        <item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
    </style>
</resources>

6. ViewBinding(推荐替代 findViewById)

groovy
// app/build.gradle
android {
    buildFeatures {
        viewBinding true
    }
}
java
// 使用 ViewBinding(自动生成绑定类)
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // 直接通过 binding 访问视图(类型安全,不需要 findViewById)
        binding.tvTitle.setText("你好!");
        binding.btnClick.setOnClickListener(v -> {
            binding.tvTitle.setText("按钮被点击了");
        });
    }
}

老项目中:大部分使用 findViewById,较新的项目使用 ViewBinding 或 ButterKnife。

7. 常见 UI 模式

7.1 Toolbar / ActionBar

xml
<!-- 在布局顶部 -->
<com.google.android.material.appbar.MaterialToolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    app:title="首页"
    app:navigationIcon="@drawable/ic_back" />
java
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 显示返回按钮

7.2 底部导航栏

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="首页" />
    <item android:id="@+id/nav_search"
        android:icon="@drawable/ic_search"
        android:title="发现" />
    <item android:id="@+id/nav_profile"
        android:icon="@drawable/ic_person"
        android:title="我的" />
</menu>

7.3 Dialog(对话框)

java
// AlertDialog(最常用)
new AlertDialog.Builder(this)
    .setTitle("确认删除")
    .setMessage("确定要删除这条记录吗?此操作不可撤销。")
    .setPositiveButton("删除", (dialog, which) -> {
        // 执行删除
    })
    .setNegativeButton("取消", null)
    .show();

// 自定义 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(简单提示)
Toast.makeText(this, "操作成功", Toast.LENGTH_SHORT).show();

// Snackbar(带操作的提示,推荐)
Snackbar.make(view, "已删除 1 条记录", Snackbar.LENGTH_LONG)
    .setAction("撤销", v -> {
        // 撤销删除
    })
    .show();

7.5 通知渠道(Android 8.0+)

Android 8.0 起,所有通知必须分配到通知渠道。老项目如果没有创建渠道,通知不会显示:

java
// 来自 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);          // 静音
        channel.enableLights(true);            // 呼吸灯
        channel.enableVibration(true);         // 震动
        channel.setShowBadge(true);            // 桌面角标
        channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
        notifyMgr.createNotificationChannel(channel);
    }
}

最佳实践:在 Application.onCreate() 中初始化通知渠道;不同重要级别的通知用不同 channelId,方便用户单独设置。

8. 事件处理

java
// 点击事件
view.setOnClickListener(v -> { /* 处理点击 */ });

// 长按事件
view.setOnLongClickListener(v -> {
    // 返回 true 表示消费了事件
    return true;
});

// 触摸事件(最底层)
view.setOnTouchListener((v, event) -> {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:  // 手指按下
            break;
        case MotionEvent.ACTION_MOVE:  // 手指移动
            break;
        case MotionEvent.ACTION_UP:    // 手指抬起
            break;
    }
    return true;
});

// 焦点变化
editText.setOnFocusChangeListener((v, hasFocus) -> {
    if (hasFocus) {
        // 获得焦点
    } else {
        // 失去焦点,可以做表单验证
        validateInput();
    }
});

事件分发机制(面试高频)

Android 触摸事件从外到内再由内到外传递:

Activity.dispatchTouchEvent()

ViewGroup.dispatchTouchEvent()

ViewGroup.onInterceptTouchEvent()  // 是否拦截
    ↓ (不拦截)
子 View.dispatchTouchEvent()

子 View.onTouchEvent()             // 消费事件

关键规则:

方法触发时机返回值含义
dispatchTouchEvent事件到达 Viewtrue 表示已分发/消费
onInterceptTouchEventViewGroup 专用,决定是否拦截true 拦截,子 View 收不到后续事件
onTouchEvent真正处理事件true 消费事件,false 向上回传
java
// 来自 android3 chapter11 SingleTouchView 的简化示例
public class SingleTouchView extends View {
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 记录起点
                break;
            case MotionEvent.ACTION_MOVE:
                // 绘制路径
                break;
            case MotionEvent.ACTION_UP:
                // 触发完成回调
                break;
        }
        postInvalidate();  // 线程安全的重绘
        return true;       // 消费事件,否则收不到 MOVE/UP
    }
}

前端对照:事件分发类似 DOM 的捕获 → 目标 → 冒泡,但 Android 的 onInterceptTouchEvent 是 ViewGroup 独有的概念,没有直接前端对应。

9. 动态创建视图

java
// 在代码中动态创建 View(不推荐,优先用 XML)
LinearLayout container = findViewById(R.id.container);

TextView dynamicText = new TextView(this);
dynamicText.setText("动态创建的文本");
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 XML 布局为 View(推荐方式)
LayoutInflater inflater = LayoutInflater.from(this);
View itemView = inflater.inflate(R.layout.item_card, container, false);
container.addView(itemView);

10. 自定义 View

当系统控件无法满足需求时(如手写签名板、仪表盘、图表),需要自定义 View。

10.1 继承现有控件(简单场景)

java
// 来自 android3 chapter08 CustomButton:扩展 Button 并应用自定义样式
@SuppressLint("AppCompatCustomView")
public class CustomButton extends Button {
    public CustomButton(Context context) {
        super(context);
    }

    public CustomButton(Context context, AttributeSet attrs) {
        // 通过 defStyleRes 指定默认样式 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 直接继承 View(完全自定义绘制)

java
// 简化自 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();  // 线程安全,请求重绘
        return true;       // 必须返回 true 才能持续接收事件
    }
}

10.3 自定义 View 三件套

方法作用前端类比
onMeasure()测量 View 的宽高浏览器 layout 阶段
onLayout()确定子 View 位置CSS 定位
onDraw(Canvas)绘制内容Canvas 2D / SVG render

注意:自定义 View 的 invalidate() 只能在主线程调用;子线程需用 postInvalidate()

11. 布局性能优化与常见陷阱

11.1 避免过度绘制(Overdraw)

过度绘制 = 同一像素被绘制多次。可以通过 开发者选项 → 调试 GPU 过度绘制 查看:

  • 蓝色:1 次过度绘制
  • 绿色:2 次
  • 粉色:3 次
  • 红色:4 次及以上

优化方法:

  1. 去掉不必要的背景:getWindow().setBackgroundDrawable(null)
  2. 避免多层 FrameLayout 叠加相同颜色背景
  3. 使用 clipChildren / clipToPadding 减少绘制区域

11.2 减少布局层级

xml
<!-- ❌ 深层嵌套 -->
<LinearLayout>
    <LinearLayout>
        <LinearLayout>
            <TextView />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>

<!-- ✅ 扁平化 ConstraintLayout -->
<androidx.constraintlayout.widget.ConstraintLayout>
    <TextView />
</androidx.constraintlayout.widget.ConstraintLayout>

11.3 RecyclerView 与 NestedScrollView 嵌套

RecyclerView 内部已经实现了回收复用,套在 ScrollView/NestedScrollView 中会导致所有 item 一次性创建,失去复用优势:

xml
<!-- ❌ 不要这样用 -->
<ScrollView>
    <RecyclerView />
</ScrollView>

<!-- ✅ 用 RecyclerView 的多 itemType 或 ConcatAdapter 替代 -->
<RecyclerView />

11.4 列表项 inflate 优化

java
// ❌ 每次创建新的 LayoutParams
View view = inflater.inflate(R.layout.item, parent, false);

// ✅ 复用 convertView(ListView)或 ViewHolder(RecyclerView)
// 参考模块 06 的 BaseAdapter / RecyclerView 示例

12. include / merge / ViewStub

12.1 include:复用布局

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

<!-- 在其它布局中引用 -->
<include layout="@layout/toolbar_common"
    android:id="@+id/toolbar_main" />

12.2 merge:减少一层根布局

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 本身不会生成 View 节点,只是将子元素合并到父布局中,适合作为 <include> 的根节点。

12.3 ViewStub:延迟加载

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();  // 按需加载,节省初始布局时间和内存

前端对照include 类似组件导入;merge 类似 Vue/React 的 Fragment(不增加额外 DOM 节点);ViewStub 类似 React 的 React.lazy / 条件渲染。

前端开发者备忘

CSS 概念Android XML 对应备注
display: flex<LinearLayout>线性排列
flex-directionandroid:orientation排列方向
justify-contentandroid:gravity子元素对齐
align-selfandroid:layout_gravity自身对齐
flex-growandroid:layout_weight分配剩余空间
position: relative<FrameLayout>叠加层
position: absolute<ConstraintLayout> 约束精确定位
border-radius<corners android:radius>圆角
background-color<solid android:color>背景色
border<stroke>边框
:hover / :active<selector> + state_pressed状态样式
overflow: auto<ScrollView>滚动
object-fitandroid:scaleType图片缩放
font-sizeandroid:textSize (sp)字体大小
colorandroid:textColor文字颜色
CSS 类名style="@style/Xxx"样式复用
CSS 变量 / :rootvalues/colors.xml + 主题全局变量

下一步

掌握了 UI 布局和控件后,接下来学习 模块 06:RecyclerView 与列表渲染,这是 Android 中最重要的数据展示组件。

面向前端开发者的 Java Android 开发教程