UI

常用控件

基本布局

LinearLayout

LinearLayout又称作线性布局,这个布局会将它所包含的控件在线性方向上依次排列。

通过这个属性android:orientation="versical"/android:orientation="horizontal"来执行布局是垂直方向排列还是水平方向排列。默认方向是水平方向

这个布局中有两个重要的属性:

(1)android:layout_gravity属性和android:gravity属性有什么区别?

  • android:gravity用于指定文字在控件中的对齐方式(top、bottom、start、end、center等,可以用“|”来同时指定多个值)
  • android:layout_gravity用于指定控件在布局中的对齐方式

但是需要注意,当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效。因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。

(2)LinearLayout中的另一个重要属性—android:layout_weight

这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>

<Button
android:id="@+id/send"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Send"
/>

</LinearLayout>

image-20240316234645019

同时指定为1平分屏幕宽度,

其实原理很简单,将LinearLayout下所有控件的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值。上面就是1/2和1/2。因此平分。

还可以通过指定部分控件的layout_weight值来实现更好的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<EditText
android:id="@+id/input_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="Type something"
/>

<Button
android:id="@+id/send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
/>

</LinearLayout>

image-20240316234658569

RelativeLayout

相对布局,RelativeLayout显得更加随意,它可以通过相对定位的方式让控件出现在布局的任何位置,因此RelativeLayout中的属性非常多,不过都是有规律可循的。

(1)控件相当于父布局定位

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
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="Button 1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:text="Button 2" />

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true"
android:text="Button 4" />

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:text="Button 5" />

</RelativeLayout>

image-20240316234709353

(2)控件相对于控件定位

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
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Button 3" />

<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 1" />

<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 2" />

<Button
android:id="@+id/button4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toLeftOf="@id/button3"
android:text="Button 4" />

<Button
android:id="@+id/button5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/button3"
android:layout_toRightOf="@id/button3"
android:text="Button 5" />

</RelativeLayout>

image-20240316234720092

FrameLayout

这种布局没有丰富的定位方式,所有的控件都会默认摆放在布局的左上角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is TextView"
/>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button"
/>

</FrameLayout>

image-20240316234729594

可以看到,文字和按钮都位于布局的左上角。由于Button是在TextView之后添加的,因此按钮压在了文字的上面。

除了这种默认效果之外,我们还可以使用layout_gravity属性来指定控件在布局中的对齐方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:text="This is TextView"
/>

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right"
android:text="Button"
/>

</FrameLayout>

image-20240316234738640

引入布局

拿标题栏举例,一般我们程序中可能有多个Activity需要这样的标题栏,如果在每个Activity的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复。这时我们就可以使用引入布局的方式来解决这个问题

建立title.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
31
32
33
34
35
36
37
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/title_bg">

<Button
android:id="@+id/titleBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/back_bg"
android:text="Back"
android:textColor="#fff" />

<TextView
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp" />

<Button
android:id="@+id/titleEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@drawable/edit_bg"
android:text="Edit"
android:textColor="#fff" />

</LinearLayout>

android:layout_margin这个属性,它可以指定控件在上下左右方向上的间距

现在标题栏布局已经编写完成了,剩下的就是如何在程序中使用这个标题栏了

1
2
3
4
5
6
7
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<include layout="@layout/title" />

</LinearLayout>

只需要一个include标签就把标题栏布局引入进来了。@layout/title 意味着引用了 res/layout 目录下名为 title.xml 的布局文件。

最后别忘了在MainActivity中将系统自带的标题栏隐藏掉

1
2
3
4
5
6
7
8
9
10
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//调用getSupportActionBar()方法获取ActionBar的实例,再调用它的hide()方法将标题栏隐藏起来
supportActionBar?.hide()
}

}

image-20240316234748877

自定义控件

引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个Activity中为这些控件单独编写一次事件注册的代码,无疑会增加很多重复代码,这种情况最好是使用自定义控件的方式来解决。

新建TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class TitleLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {

init {
//第一个参数是要加载的布局文件的id,这里我们传入R.layout.title
//第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this
LayoutInflater.from(context).inflate(R.layout.title, this)
titleBack.setOnClickListener {
//TitleLayout中接收的context参数实际上是一个Activity的实例
//先将它转换成Activity类型,然后再调用finish()方法销毁当前的Activity
//Kotlin中的类型强制转换使用的关键字是as
val activity = context as Activity
activity.finish()
}
titleEdit.setOnClickListener {
Toast.makeText(context, "You clicked Edit button", Toast.LENGTH_SHORT).show()
}
}

}

需要在布局文件中添加这个自定义控件:

1
2
3
4
5
6
7
8
9
10
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >

//需要指明控件的完整类名
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />

</LinearLayout>

自定义控件就完成啦,这样的话,每当我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。

强大的滚动控件:RecyclerView

定义一个实体类,新建Fruit类

1
2
//name表示水果的名字,imageId表示水果对应图片的资源id。
class Fruit(val name:String, val imageId: Int)

然后需要为RecyclerView的子项指定一个我们自定义的布局,在layout目录下新建fruit_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dp">

<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>

<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp" />

</LinearLayout>

接下来需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类

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
class FruitAdapter(val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {

//定义一个内部类,主构造函数传入一个view参数,这个参数RecyclerView子项的最外层布局
inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
//所以我们就可以通过findViewById()方法来获取布局中的ImageView和TextView
val fruitImage: ImageView = view.findViewById(R.id.fruitImage)
val fruitName: TextView = view.findViewById(R.id.fruitName)
}

//由于继承了RecyclerView.Adapter,因此要重新下面三个方法
//onCreateViewHolder是用于创建ViewHolder实例的
//第二个参数表示视图的类型,RecyclerView 支持多种不同类型的视图
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
return ViewHolder(view)
}

//onBindViewHolder()方法用于对RecyclerView子项的数据进行赋值,
//会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,
//然后再将数据设置到ViewHolder的ImageView和TextView当中即可
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fruit = fruitList[position]
holder.fruitImage.setImageResource(fruit.imageId)
holder.fruitName.text = fruit.name
}

//用于告诉RecyclerView一共有多少子项
override fun getItemCount() = fruitList.size

}

适配器准备好了之后,我们就可以开始使用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
28
29
30
31
32
class MainActivity : AppCompatActivity() {

private val fruitList = ArrayList<Fruit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
val layoutManager = LinearLayoutManager(this)
//layoutManager用于指定RecyclerView的布局方式,这里是使用的线性布局
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
//完成适配器的设置,这样RecyclerView和数据之间的关联就建立完成了
recyclerView.adapter = adapter
}

private fun initFruits() {
repeat(2) {
fruitList.add(Fruit("Apple", R.drawable.apple_pic))
fruitList.add(Fruit("Banana", R.drawable.banana_pic))
fruitList.add(Fruit("Orange", R.drawable.orange_pic))
fruitList.add(Fruit("Watermelon", R.drawable.watermelon_pic))
fruitList.add(Fruit("Pear", R.drawable.pear_pic))
fruitList.add(Fruit("Grape", R.drawable.grape_pic))
fruitList.add(Fruit("Pineapple", R.drawable.pineapple_pic))
fruitList.add(Fruit("Strawberry", R.drawable.strawberry_pic))
fruitList.add(Fruit("Cherry", R.drawable.cherry_pic))
fruitList.add(Fruit("Mango", R.drawable.mango_pic))
}
}

}

image-20240316234813283

RecyclerView还支持横向滚动,这时就要把水果元素改成垂直排列才合理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="80dp"
android:layout_height="wrap_content">

<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />

<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />

</LinearLayout>

我们将ImageView和TextView都设置成了在布局中水平居中,并且使用layout_marginTop属性让文字和图片之间保持一定距离,接下来修改MainActivity中的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MainActivity : AppCompatActivity() {

private val fruitList = ArrayList<Fruit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
val layoutManager = LinearLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.HORIZONTAL
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}
...
}

image-20240316234821395

除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManager和StaggeredGridLayoutManager这两种内置的布局排列方式。

  • GridLayoutManager可以用于实现网格布局
  • StaggeredGridLayoutManager可以用于实现瀑布流布局

下面进行演示瀑布流布局:

修改fruit_item.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">

<ImageView
android:id="@+id/fruitImage"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp" />

<TextView
android:id="@+id/fruitName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left"
android:layout_marginTop="10dp" />

</LinearLayout>

LinearLayout的宽度改为match_parent,因为瀑布流布局是根据布局的列数来自动适配的。

接着修改MainActivity中的代码

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
class MainActivity : AppCompatActivity() {

private val fruitList = ArrayList<Fruit>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initFruits() // 初始化水果数据
//第一个参数用于指定布局的列数,传入3表示会把布局分为3列;
//第二个参数用于指定布局的排列方向,传入StaggeredGridLayoutManager.VERTICAL表示会让布局纵向排列
val layoutManager = StaggeredGridLayoutManager(3,
StaggeredGridLayoutManager.VERTICAL)
recyclerView.layoutManager = layoutManager
val adapter = FruitAdapter(fruitList)
recyclerView.adapter = adapter
}

private fun initFruits() {
repeat(2) {
fruitList.add(Fruit(getRandomLengthString("Apple"),
R.drawable.apple_pic))
fruitList.add(Fruit(getRandomLengthString("Banana"),
R.drawable.banana_pic))
fruitList.add(Fruit(getRandomLengthString("Orange"),
R.drawable.orange_pic))
fruitList.add(Fruit(getRandomLengthString("Watermelon"),
R.drawable.watermelon_pic))
fruitList.add(Fruit(getRandomLengthString("Pear"),
R.drawable.pear_pic))
fruitList.add(Fruit(getRandomLengthString("Grape"),
R.drawable.grape_pic))
fruitList.add(Fruit(getRandomLengthString("Pineapple"),
R.drawable.pineapple_pic))
fruitList.add(Fruit(getRandomLengthString("Strawberry"),
R.drawable.strawberry_pic))
fruitList.add(Fruit(getRandomLengthString("Cherry"),
R.drawable.cherry_pic))
fruitList.add(Fruit(getRandomLengthString("Mango"),
R.drawable.mango_pic))
}
}

private fun getRandomLengthString(str: String): String {
val n = (1..20).random()
val builder = StringBuilder()
repeat(n) {
builder.append(str)
}
return builder.toString()
}

}

img

RecyclerView的点击事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FruitAdapter(val fruitList: List<Fruit>) :
RecyclerView.Adapter<FruitAdapter.ViewHolder>() {
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.fruit_item, parent, false)
val viewHolder = ViewHolder(view)
viewHolder.itemView.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked view ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
viewHolder.fruitImage.setOnClickListener {
val position = viewHolder.adapterPosition
val fruit = fruitList[position]
Toast.makeText(parent.context, "you clicked image ${fruit.name}",
Toast.LENGTH_SHORT).show()
}
return viewHolder
}
...
}

UI
http://example.com/2024/03/16/UI/
作者
zlw
发布于
2024年3月16日
许可协议