-

「Generated by Manus, instructions issued by binbinwang」

本章将深入探讨Android的UI开发核心概念和技术,并与iOS的UI开发进行对比。作为一名iOS开发者,了解Android UI开发的特点和差异将帮助你更快地适应Android平台。

4.1 布局系统基础

XML布局与视图层次

Android使用XML文件定义UI布局,这与iOS的XIB/Storyboard概念类似,但实现方式不同。

基本布局XML示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?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/title_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Hello Android"
android:textSize="24sp"
android:textStyle="bold" />

<EditText
android:id="@+id/input_field"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="Enter your name" />

<Button
android:id="@+id/submit_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Submit" />

</LinearLayout>

在Activity中加载布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// 获取视图引用
val titleText = findViewById<TextView>(R.id.title_text)
val inputField = findViewById<EditText>(R.id.input_field)
val submitButton = findViewById<Button>(R.id.submit_button)

// 设置事件监听
submitButton.setOnClickListener {
val name = inputField.text.toString()
if (name.isNotEmpty()) {
titleText.text = "Hello, $name"
}
}
}
}

常用布局容器

Android提供多种布局容器,每种都有特定的布局行为:

  1. LinearLayout:线性排列子视图(水平或垂直)
  2. RelativeLayout:基于相对位置排列子视图
  3. ConstraintLayout:约束布局,类似iOS的Auto Layout
  4. FrameLayout:简单的堆叠布局
  5. GridLayout:网格布局
  6. CoordinatorLayout:协调子视图交互的高级布局

ConstraintLayout示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/title_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Constraint Layout Example"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_margin="16dp" />

<Button
android:id="@+id/left_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Left"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_text"
android:layout_margin="16dp" />

<Button
android:id="@+id/right_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/left_button"
android:layout_margin="16dp" />

<ImageView
android:id="@+id/center_image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/sample_image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

与iOS布局系统对比

iOS的布局方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 代码方式布局(Frame)
- (void)setupViewsWithFrames {
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(16, 20, self.view.bounds.size.width - 32, 30)];
titleLabel.text = @"Hello iOS";
titleLabel.font = [UIFont boldSystemFontOfSize:24];
[self.view addSubview:titleLabel];

UITextField *inputField = [[UITextField alloc] initWithFrame:CGRectMake(16, 58, self.view.bounds.size.width - 32, 40)];
inputField.placeholder = @"Enter your name";
inputField.borderStyle = UITextBorderStyleRoundedRect;
[self.view addSubview:inputField];

UIButton *submitButton = [UIButton buttonWithType:UIButtonTypeSystem];
submitButton.frame = CGRectMake(16, 106, 100, 40);
[submitButton setTitle:@"Submit" forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:submitButton];
}

// Auto Layout方式
- (void)setupViewsWithAutoLayout {
UILabel *titleLabel = [[UILabel alloc] init];
titleLabel.text = @"Hello iOS";
titleLabel.font = [UIFont boldSystemFontOfSize:24];
titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:titleLabel];

UITextField *inputField = [[UITextField alloc] init];
inputField.placeholder = @"Enter your name";
inputField.borderStyle = UITextBorderStyleRoundedRect;
inputField.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:inputField];

UIButton *submitButton = [UIButton buttonWithType:UIButtonTypeSystem];
[submitButton setTitle:@"Submit" forState:UIControlStateNormal];
[submitButton addTarget:self action:@selector(submitButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
submitButton.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:submitButton];

// 添加约束
[NSLayoutConstraint activateConstraints:@[
[titleLabel.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor constant:16],
[titleLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[titleLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],

[inputField.topAnchor constraintEqualToAnchor:titleLabel.bottomAnchor constant:8],
[inputField.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16],
[inputField.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:-16],

[submitButton.topAnchor constraintEqualToAnchor:inputField.bottomAnchor constant:16],
[submitButton.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:16]
]];
}

主要差异

  1. Android使用XML文件定义布局,而iOS使用Interface Builder或代码
  2. Android的布局系统基于父容器类型,而iOS主要使用Auto Layout约束
  3. Android的ConstraintLayout类似于iOS的Auto Layout,但语法和概念有差异
  4. Android的布局属性直接在XML中设置,而iOS的Auto Layout需要创建NSLayoutConstraint对象
  5. Android的布局资源可以根据设备配置自动选择,而iOS需要使用Size Classes或代码适配

4.2 常用UI组件

基础控件

Android提供丰富的UI控件,以下是一些常用的基础控件:

文本显示

  • TextView:显示文本,类似iOS的UILabel
  • EditText:文本输入框,类似iOS的UITextField
  • AutoCompleteTextView:带自动完成功能的输入框

按钮

  • Button:标准按钮,类似iOS的UIButton
  • ImageButton:图像按钮
  • FloatingActionButton:Material Design浮动操作按钮
  • Switch:开关控件,类似iOS的UISwitch
  • CheckBox:复选框
  • RadioButton:单选按钮

列表与网格

  • RecyclerView:高效显示大量数据,类似iOS的UITableView/UICollectionView
  • ListView:列表视图(较旧,建议使用RecyclerView)
  • GridView:网格视图(较旧,建议使用RecyclerView)

其他常用控件

  • ImageView:显示图像,类似iOS的UIImageView
  • ProgressBar:进度条,类似iOS的UIProgressView
  • SeekBar:滑块控件,类似iOS的UISlider
  • RatingBar:评分控件
  • WebView:嵌入式浏览器,类似iOS的WKWebView

RecyclerView详解

RecyclerView是Android中显示列表数据的核心组件,类似于iOS的UITableView和UICollectionView的结合体。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 布局文件
// activity_recycler_view.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

// 列表项布局
// item_user.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">

<ImageView
android:id="@+id/user_avatar"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/default_avatar" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:orientation="vertical">

<TextView
android:id="@+id/user_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />

<TextView
android:id="@+id/user_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>

适配器和ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 数据模型
data class User(val name: String, val email: String, val avatarResId: Int)

// 适配器
class UserAdapter(private val users: List<User>) :
RecyclerView.Adapter<UserAdapter.UserViewHolder>() {

// 定义ViewHolder
class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val avatarImageView: ImageView = view.findViewById(R.id.user_avatar)
val nameTextView: TextView = view.findViewById(R.id.user_name)
val emailTextView: TextView = view.findViewById(R.id.user_email)
}

// 创建ViewHolder
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}

// 绑定数据到ViewHolder
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
val user = users[position]
holder.avatarImageView.setImageResource(user.avatarResId)
holder.nameTextView.text = user.name
holder.emailTextView.text = user.email

// 设置点击事件
holder.itemView.setOnClickListener {
// 处理点击事件
Toast.makeText(
holder.itemView.context,
"Clicked on ${user.name}",
Toast.LENGTH_SHORT
).show()
}
}

// 返回数据项数量
override fun getItemCount() = users.size
}

在Activity中使用RecyclerView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class RecyclerViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recycler_view)

// 准备数据
val users = listOf(
User("John Doe", "john@example.com", R.drawable.avatar1),
User("Jane Smith", "jane@example.com", R.drawable.avatar2),
User("Bob Johnson", "bob@example.com", R.drawable.avatar3)
// 更多数据...
)

// 设置RecyclerView
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = UserAdapter(users)

// 添加分割线
recyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)

// 添加动画
recyclerView.itemAnimator = DefaultItemAnimator()
}
}

与iOS UI组件对比

iOS的UITableView实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 数据模型
@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, strong) UIImage *avatar;
@end

// 表格视图控制器
@interface UserTableViewController : UITableViewController
@property (nonatomic, strong) NSArray<User *> *users;
@end

@implementation UserTableViewController

- (void)viewDidLoad {
[super viewDidLoad];

// 准备数据
self.users = @[
[[User alloc] initWithName:@"John Doe" email:@"john@example.com" avatar:[UIImage imageNamed:@"avatar1"]],
[[User alloc] initWithName:@"Jane Smith" email:@"jane@example.com" avatar:[UIImage imageNamed:@"avatar2"]],
[[User alloc] initWithName:@"Bob Johnson" email:@"bob@example.com" avatar:[UIImage imageNamed:@"avatar3"]]
// 更多数据...
];

// 注册单元格
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UserCell"];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.users.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UserCell" forIndexPath:indexPath];

User *user = self.users[indexPath.row];
cell.textLabel.text = user.name;
cell.detailTextLabel.text = user.email;
cell.imageView.image = user.avatar;

return cell;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
User *user = self.users[indexPath.row];
NSLog(@"Selected user: %@", user.name);
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}

@end

主要差异

  1. Android的RecyclerView更灵活,可以实现列表、网格等多种布局,而iOS分别使用UITableView和UICollectionView
  2. Android使用ViewHolder模式(现在是强制的),而iOS的cell重用机制类似但实现不同
  3. Android的适配器直接处理数据绑定,而iOS将数据源和委托分开(dataSource和delegate)
  4. Android的RecyclerView需要显式设置LayoutManager,而iOS的表格视图有固定布局
  5. Android的点击事件在适配器中处理,而iOS通过委托方法处理

4.3 资源管理系统

资源类型与组织

Android的资源管理系统非常强大,所有资源都存放在res目录下的特定子目录中:

  • **drawable/**:图像资源(PNG、JPG、XML矢量图等)
  • **layout/**:布局文件
  • **values/**:值资源(字符串、颜色、尺寸、样式等)
  • **mipmap/**:应用图标
  • **raw/**:原始文件(音频、视频等)
  • **xml/**:任意XML文件
  • **font/**:字体文件
  • animator/**、anim/**:动画资源
  • **menu/**:菜单资源

资源引用方式

  • 在XML中:@[包名:]资源类型/资源名称,如@drawable/icon
  • 在代码中:R.资源类型.资源名称,如R.drawable.icon

资源限定符与适配

Android使用资源限定符(Resource Qualifiers)实现不同设备配置的适配:

常用限定符

  • 屏幕尺寸small, normal, large, xlarge
  • 屏幕密度ldpi, mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi
  • 屏幕方向port(竖屏), land(横屏)
  • 语言和地区en, zh-rCN, fr-rFR
  • 夜间模式night
  • 最小宽度sw<N>dp,如sw600dp

示例目录结构

1
2
3
4
5
6
7
8
9
res/
drawable/ # 默认图像
drawable-hdpi/ # 高密度屏幕图像
drawable-xhdpi/ # 超高密度屏幕图像
layout/ # 默认布局
layout-land/ # 横屏布局
values/ # 默认值
values-zh-rCN/ # 中文(中国)值
values-night/ # 夜间模式值

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- values/strings.xml -->
<resources>
<string name="greeting">Hello</string>
</resources>

<!-- values-zh-rCN/strings.xml -->
<resources>
<string name="greeting">你好</string>
</resources>

<!-- values/dimens.xml -->
<resources>
<dimen name="text_size">16sp</dimen>
</resources>

<!-- values-sw600dp/dimens.xml -->
<resources>
<dimen name="text_size">20sp</dimen>
</resources>

与iOS资源管理对比

iOS的资源管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 图像资源
UIImage *image = [UIImage imageNamed:@"icon"];

// 本地化字符串
NSString *greeting = NSLocalizedString(@"greeting", @"Greeting message");

// 尺寸适配
CGFloat textSize;
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
textSize = 20.0;
} else {
textSize = 16.0;
}

// 暗黑模式适配
if (@available(iOS 13.0, *)) {
UIColor *textColor = [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) {
return [UIColor whiteColor];
} else {
return [UIColor blackColor];
}
}];
}