模块 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.css | styles.xml / themes.xml | 全局样式 |
1. 布局基础
1.1 一个最简单的布局
<!-- 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_width和layout_height是必须属性,每个 View 都必须声明。
1.3 ID 系统
<!-- 定义 ID -->
<TextView
android:id="@+id/tv_title" <!-- @+id/ 表示创建新 ID -->
... />
<!-- 引用已有 ID(如约束布局中) -->
app:layout_constraintTop_toBottomOf="@id/tv_title" <!-- @id/ 不带 + -->// 在 Java 代码中通过 ID 查找视图(类似 document.getElementById)
TextView title = findViewById(R.id.tv_title);
Button btn = findViewById(R.id.btn_click);2. 布局容器
2.1 LinearLayout(线性布局)
最基础的布局,类似 CSS 的 Flexbox 单方向排列:
<!-- 垂直排列(类似 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 属性对照:
| XML | CSS 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 | 分配剩余空间 |
divider | gap(近似) | 子元素间距 |
2.2 ConstraintLayout(约束布局)
最推荐的布局,功能最强大,扁平化层级:
<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_toStartOf | left: 0 | 左边对齐 |
constraintEnd_toEndOf | right: 0 | 右边对齐 |
constraintTop_toBottomOf | top: Xpx (在某元素下方) | 上方约束 |
constraintBottom_toBottomOf | bottom: 0 | 下方约束 |
layout_constraintHorizontal_bias | (无直接对应) | 水平偏移比例 |
2.3 FrameLayout(帧布局)
类似 CSS 的 position: relative,子元素层层叠加:
<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 布局选择指南
| 场景 | 推荐布局 | 前端对应 |
|---|---|---|
| 简单列表/表单 | LinearLayout | Flexbox (column/row) |
| 复杂页面 | ConstraintLayout | Grid / absolute |
| 叠加层/容器 | FrameLayout | position: relative |
| 滚动内容 | ScrollView / NestedScrollView | overflow: auto |
| 列表 | RecyclerView | 虚拟列表 |
3. 常用控件
3.1 TextView(文本)
<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" /> <!-- 图标与文字间距 -->// 在代码中操作
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(输入框)
<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>// 获取输入内容
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
<!-- 基础按钮 -->
<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(图片)
<ImageView
android:id="@+id/iv_photo"
android:layout_width="200dp"
android:layout_height="200dp"
android:src="@drawable/photo" <!-- 图片资源 -->
android:scaleType="centerCrop" <!-- 缩放模式 -->
android:contentDescription="用户头像" /> <!-- 无障碍描述 -->scaleType 对照:
| scaleType | CSS 对应 | 说明 |
|---|---|---|
centerCrop | object-fit: cover | 裁切填充 |
fitCenter | object-fit: contain | 完整显示 |
centerInside | object-fit: scale-down | 不放大 |
fitXY | object-fit: fill | 拉伸填充 |
// 使用 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
<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(滚动容器)
<!-- 垂直滚动(最常用) -->
<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)
<!-- 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)
<!-- 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)
<!-- 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 类)
<!-- 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><!-- 在布局中使用样式 -->
<TextView
style="@style/TitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题文本" />5.2 主题(全局应用)
<!-- 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)
// app/build.gradle
android {
buildFeatures {
viewBinding true
}
}// 使用 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
<!-- 在布局顶部 -->
<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" />Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); // 显示返回按钮7.2 底部导航栏
<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="首页" />
<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(对话框)
// 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
// 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 起,所有通知必须分配到通知渠道。老项目如果没有创建渠道,通知不会显示:
// 来自 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. 事件处理
// 点击事件
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 | 事件到达 View | true 表示已分发/消费 |
onInterceptTouchEvent | ViewGroup 专用,决定是否拦截 | true 拦截,子 View 收不到后续事件 |
onTouchEvent | 真正处理事件 | true 消费事件,false 向上回传 |
// 来自 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. 动态创建视图
// 在代码中动态创建 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 继承现有控件(简单场景)
// 来自 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(完全自定义绘制)
// 简化自 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 次及以上
优化方法:
- 去掉不必要的背景:
getWindow().setBackgroundDrawable(null) - 避免多层
FrameLayout叠加相同颜色背景 - 使用
clipChildren/clipToPadding减少绘制区域
11.2 减少布局层级
<!-- ❌ 深层嵌套 -->
<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 一次性创建,失去复用优势:
<!-- ❌ 不要这样用 -->
<ScrollView>
<RecyclerView />
</ScrollView>
<!-- ✅ 用 RecyclerView 的多 itemType 或 ConcatAdapter 替代 -->
<RecyclerView />11.4 列表项 inflate 优化
// ❌ 每次创建新的 LayoutParams
View view = inflater.inflate(R.layout.item, parent, false);
// ✅ 复用 convertView(ListView)或 ViewHolder(RecyclerView)
// 参考模块 06 的 BaseAdapter / RecyclerView 示例12. include / merge / ViewStub
12.1 include:复用布局
<!-- 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:减少一层根布局
<!-- 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:延迟加载
<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(); // 按需加载,节省初始布局时间和内存前端对照:
include类似组件导入;merge类似 Vue/React 的 Fragment(不增加额外 DOM 节点);ViewStub类似 React 的React.lazy/ 条件渲染。
前端开发者备忘
| CSS 概念 | Android XML 对应 | 备注 |
|---|---|---|
display: flex | <LinearLayout> | 线性排列 |
flex-direction | android:orientation | 排列方向 |
justify-content | android:gravity | 子元素对齐 |
align-self | android:layout_gravity | 自身对齐 |
flex-grow | android: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-fit | android:scaleType | 图片缩放 |
font-size | android:textSize (sp) | 字体大小 |
color | android:textColor | 文字颜色 |
| CSS 类名 | style="@style/Xxx" | 样式复用 |
| CSS 变量 / :root | values/colors.xml + 主题 | 全局变量 |
下一步
掌握了 UI 布局和控件后,接下来学习 模块 06:RecyclerView 与列表渲染,这是 Android 中最重要的数据展示组件。