这篇文章是我自己的《第一行代码Andorid》的阅读笔记,虽然大量参考了别人已经写好的一些笔记和代码但是也有自己的提炼和新的问题在里面,我也会放上参考文章链接。
第二章 UI
1. 文本控件-TextView🎯
1.1 基础属性
- layout_width:组件的宽度
- layout height:组件的高度
Android中所有的控件都具有这两个属性,可选值有3种:match_parent、fill_parent和wrap_content。
- match_parent和fill_parent的意义相同,现在官方更加推荐使用match_parent。
- match_parent表示让当前控件的大小和父布局的大小一样,也就是由父布局来决定当前控件的大小。
- wrap_content表示让当前控件的大小能够刚好包含住里面的内容,由控件内容决定当前控件的大小。
- id:为TextView设置一个组件id
- text:设置显示的文本内容
- textColor:设置字体颜色
- textStyle:设置字体风格,三个可选值:normal(无效果),bold加粗),italic(斜体)
- textSize:字体大小,单位一般是用sp
- background:控件的背景颜色,可以理解为填充整个控件的颜色,可以是图片
- gravity:设置控件中内容的对齐方向,TextView中是文字,ImageView中是图片等等。
使用android:gravity来指定文字的对齐方式,可选值有top、bottom、left、right、center等,可以用“|”来同时指定多个值,这里我们指定的center,效果等同于center_vertical|center_horizontal,表示文字在垂直和水平方向都居中对齐。
1.2 阴影 - android::shadowColor:设置阴影颜色,需要与shadowRadius一起使用
- android:shadowRadius:设置阴影的模胡程度,设为0.1就变成字体颜色了,建议使用3.0
- android:shadowDx:设置阴影在水平方向的偏移,就是水平方向阴影开始的横坐标位置
- android:shadowDy:设置阴影在竖直方向的偏移,就是竖直方向阴影开始的纵坐标位置
1.3 跑马灯效果
android:singleLine:内容单行显示
android:focusable是否可以获取焦点
android:focusablelnTouchMode:用于控制视图在触摸模式下是否可以聚焦
android:ellipsize:在哪里省略文本,当字数太多可以选择在前面后面增加省略号以省略文本
android:marqueeRepeatLimit:字幕动画重复的次数
2. 按钮控件-Button🎯
Button实际上是继承TextView的
2.1 大小写转换
我们在布局文件里面设置的文字是“Button”,但最终的显示结果却是“BUTTON”。这是由于系统会对Button中的所有英文字母自动进行大写转换,如果这不是你想要的效果,可以使用 android:textAllCaps="false"来禁用这一默认特性。
2.2 点击事件的两种方法
2.2.1 匿名类
我们创建的对象,没有显示的赋值给一个变量名。即为匿名对象,匿名对象只能调用一次。就像我们有时候调用有返回值的函数确不需要用到返回值一样,我们只需要用到这个对象的某个方法而不需要再对这个对象做其他处理。
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//此处添加逻辑
}
});
2.2.2 实现接口
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(this);
}
//实现View.OnClickListener接口
@Override
public void onClick(View v){
switch (v.getId()){
case R.id.button:
//此处添加逻辑
break;
default:
break;
}
}
}
2.2.3 事件处理
- 点击事件
- 长按事件
- 触摸事件
注意这三个事件的返回值。return true;时事件会被该动作函数消费,不会再执行其他函数
3. 编辑控件 EditText🎯
3.1 基础属性
- android:hint输入提示
- android:textColorHint输入提示文字的颜色
- android:inputType输入类型
- android:drawableXxxx在输入框的指定方位添加图片
- android:drawablePadding设置图片与输入内容的间距
- android:paddingXxxx设置内容与边框的间距
- android:background背景色
- android:maxLines 设置最大行数,因为如果EditText的高度指定的是wrap_content,它总能包含住里面的内容,当输入的内容过多时,界面就会变得非常难看。
通过android:maxLines指定了EditText的最大行数为两行,这样当输入的内容超过两行时,文本就会向上滚动,而EditText则不会再继续拉伸。
3.2 获取输出框内容-getText()
String inputText = editText.getText().toString();
Toast.makeText(MainActivity.this,inputText,Toast.LENGTH_SHORT).show();
4. 图片控件-ImageView🎯
ImageView是用于在界面上展示图片的一个控件,它可以让我们的程序界面变得更加丰富多彩。
图片通常都是放在以“drawable”开头的目录下的。目前我们的项目中有一个空的drawable目录,不过由于这个目录没有指定具体的分辨率,所以一般不使用它来放置图片。
这里注意,你放在文件夹里面的图片的命名不可以数字开头,一定要以字母开头
4.1 更换显示图片-setImageResource
private ImageView imageView;
imageView = (ImageView) findViewById(R.id.image_view);
imageView.setImageResource(R.drawable.img_2);
5. 进度条控件-ProgressBar🎯
ProgressBar用于在界面上显示一个进度条,表示我们的程序正在加载一些数据。
显示效果是一个圆形进度条正在旋转
5.1 Android控件的可见属性
所有的Android控件都具有这个属性,可以通过android:visibility进行指定,可选值有3种:visible、invisible和gone。
- visible表示控件是可见的,这个值是默认值,不指定android:visibility时,控件都是可见的。
- invisible表示控件不可见,但是它仍然占据着原来的位置和大小,可以理解成控件变成透明状态了。
- gone则表示控件不仅不可见,而且不再占用任何屏幕空间。
我们还可以通过代码来设置控件的可见性,使用的是setVisibility()方法,可以传入View.VISIBLE、View.INVISIBLE和View.GONE这3种值。
5.2 ProgressBar样式
刚刚是圆形进度条,通过style属性可以将它指定成水平进度条
<ProgressBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:max="100"/>
6. 对话框控件-AlertDialog🎯
AlertDialog可以在当前的界面弹出一个对话框,这个对话框是置顶于所有界面元素之上的,能够屏蔽掉其他控件的交互能力,因此AlertDialog一般都是用于提示一些非常重要的内容或者警告信息。比如为了防止用户误删重要内容,在删除前弹出一个确认对话框。
AlertDialog.Builder dialog= new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("Something important.");
dialog.setCancelable(false);//可否用Back键关闭对话框
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
});
dialog.show();
6.1 ProgressDialog
ProgressDialog和AlertDialog有点类似,都可以在界面上弹出一个对话框,都能够屏蔽掉其他控件的交互能力。不同的是,ProgressDialog会在对话框中显示一个进度条,一般用于表示当前操作比较耗时,让用户耐心地等待。它的用法和AlertDialog也比较相似。
注意:这个方法现在已经过时了,现在的做法是通过在AlertDialog里面加一个progressDialog 来实现
// 创建一个 AlertDialog.Builder
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("正在加载"); // 对话框标题
builder.setView(new ProgressBar(this)); // 添加进度条视图
// 创建 AlertDialog 并显示
AlertDialog progressDialog = builder.create();
progressDialog.show();
// 在任务完成后关闭对话框
progressDialog.dismiss(); // 请在任务完成后调用此方法来关闭对话框
注意:如果在setCancelable()中传入了false,表示ProgressDialog是不能通过Back键取消掉的,这时你就一定要在代码中做好控制,当数据加载完成后必须要调用ProgressDialog的dismiss()方法来关闭对话框,否则ProgressDialog将会一直存在。
7. 四种布局🎯
7.1 线性布局
LinearLayout又称作线性布局,是一种非常常用的布局,这个布局会将它所包含的控件在线性方向上依次排列。
在之前学习控件用法时,我们所有的控件就都是放在LinearLayout布局里的。线性不止一个方向,但是由于我们通过android:orientation属性指定了排列方向是vertical,因此我们的控件也确实是在垂直方向上线性排列的。如果指定的是horizontal,控件就会在水平方向上排列了。
7.1.1 LinearLayout
垂直线性布局
水平线性布局
注意:如果LinearLayout的排列方向是horizontal,内部的控件就绝对不能将宽度指定为match_parent,因为这样的话,单独一个控件就会将整个水平方向占满,其他的控件就没有可放置的位置了。同样的道理,如果LinearLayout的排列方向是vertical,内部的控件就不能将高度指定为match_parent。
7.1.2 android:layout_gravity属性
android:layout_gravity属性和之前学到的android:gravity属性看起来有些相似。
- android:gravity用于指定文字在控件中的对齐方式
- android:layout_gravity用于指定控件在布局中的对齐方式。
- android:layout_gravity的可选值和android:gravity差不多。
注意:当LinearLayout的排列方向是horizontal时,只有垂直方向上的对齐方式才会生效,因为此时水平方向上的长度是不固定的,每添加一个控件,水平方向上的长度都会改变,因而无法指定该方向上的对齐方式。同样的道理,当LinearLayout的排列方向是vertical时,只有水平方向上的对齐方式才会生效。
比如,当你设置目前LinearLayout的排列方向是horizontal时,控件默认显示在一行左右排列嘛,但是这一行的垂直区域是归你的空间所属的,你可以指定垂直方向的对齐方式,如下图
7.1.3 android:layout_weight属性
这个属性允许我们使用比例的方式来指定控件的大小,它在手机屏幕的适配性方面可以起到非常重要的作用。
例如:
<?xml version="1.0" encoding="utf-8"?>
<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>
- 由于我们使用了android:layout_weight属性,此时控件的宽度就不应该再由android:layout_width来决定。所以这里规范的写法应当将EditText和Button的宽度都指定成0dp。
- 然后在EditText和Button里都将android:layout_weight属性的值指定为1,这表示EditText和Button将在水平方向平分宽度。
为什么将android:layout_weight属性的值同时指定为1就会平分屏幕宽度呢?
系统会先把LinearLayout下所有控件指定的layout_weight值相加,得到一个总值,然后每个控件所占大小的比例就是用该控件的layout_weight值除以刚才算出的总值。因此如果想让EditText占据屏幕宽度的3/5, Button占据屏幕宽度的2/5,只需要将EditText的layout_weight改成3, Button的layout_weight改成2就可以了。
也就是说ndroid:layout_weight的值其实是个权重。
3. 还可以通过指定部分控件的layout_weight值来实现更好的效果。
比如只指定EditText的android:layout_weight属性,并将Button的宽度改回wrap_content,这样Button的宽度仍然按照wrap_content来计算,而EditText则会占满屏幕所有的剩余空间。(使用layout_weight实现宽度自适配效果,这种方式编写的界面,不仅在各种屏幕的适配方面会非常好,而且看起来也更加舒服。
7.2 相对布局
RelativeLayout又称作相对布局,也是一种非常常用的布局。和LinearLayout的排列规则不同,RelativeLayout显得更加随意一些,它可以通过相对定位的方式让控件出现在布局的任何位置。
也正因为如此,RelativeLayout中的属性非常多,不过这些属性都是有规律可循的。
7.2.1 相对父布局定位
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
其中两个两个结合就是左上、右下等四个角的位置
7.2.2 相对于控件进行定位
android:layout_above="@+id/button3"
android:layout_below="@+id/button3"
android:layout_toLeftOf="@+id/button3"
android:layout_toRightOf="@id/button3"
7.2.3 边缘对齐
RelativeLayout中还有另外一组相对于控件进行定位的属性。
android:layout_alignLeft表示让一个控件的左边缘和另一个控件的左边缘对齐
android:layout_alignRight表示让一个控件的右边缘和另一个控件的右边缘对齐。
此外,还有android:layout_alignTop和android:layout_alignBottom,道理都是一样的。
7.3 帧布局
FrameLayout又称作帧布局,它相比于前面两种布局就简单太多了,因此它的应用场景也少了很多。这种布局没有方便的定位方式,所有的控件都会默认摆放在布局的左上角。
后添加的空间会直接盖在先添加的空间上面,除了这种默认效果之外,还可以使用layout_gravity属性来指定控件在布局中的对齐方式,这和LinearLayout中的用法是相似的。
7.4 百分比布局
只有LinearLayout支持使用layout_weight属性来实现按比例指定控件大小的功能,其他两种布局都不支持。如果想用RelativeLayout来实现让两个按钮平分布局宽度的效果,则是比较困难的。为此,Android引入了一种全新的布局方式来解决此问题——百分比布局。在这种布局中,我们可以不再使用wrap_content、match_parent等方式来指定控件的大小,而是允许直接指定控件在布局中所占的百分比,这样的话就可以轻松实现平分布局甚至是任意比例分割布局的效果了。
百分比布局提供了PercentFrameLayout和PercentRelativeLayout这两个全新的布局。Android团队将百分比布局定义在了support库当中,我们只需要在项目的build.gradle中添加百分比布局库的依赖,就能保证百分比布局在Android所有系统版本上的兼容性了。
- 打开app/build.gradle文件,在dependencies闭包中添加如下内容:
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.percentlayout:percentlayout:1.0.0'
testImplementation 'junit:junit:4.13.2'
}
注意❗:
如果你创建项目的时候用的不是传统的Groovy 脚本 (build.gradle)而是使用了 Kotlin 脚本 (build.gradle.kts) 作为构建脚本,可以按照以下步骤来添加依赖项:
- 打开项目根目录下的 build.gradle.kts 文件。
- 在文件中找到 dependencies { } 块,这是用于添加依赖项的地方。
- 在 dependencies { } 块内,使用 Kotlin 的语法来添加您的依赖项。例如,如果要添加一个 AndroidX 库的依赖项,可以像这样添加:
dependencies {
implementation("androidx.appcompat:appcompat:1.3.1")
// 添加其他依赖项...
}
- 在上面的示例中,implementation 是依赖配置,“androidx.appcompat:appcompat:1.3.1” 是要添加的依赖项的坐标。您可以将坐标替换为您需要的依赖项的坐标。
- 完成后,保存文件。
- 在 Android Studio 中,点击 “Sync Now”(同步项目) 按钮,以确保依赖项正确地添加到您的项目中。
所以上面的dependencies 代码要这样写:
dependencies {
implementation("androidx.appcompat:appcompat:1.3.0")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation("androidx.appcompat:appcompat:1.0.0")
implementation("androidx.percentlayout:percentlayout:1.0.0")
testImplementation("junit:junit:4.13.2")
}
- 这里将依赖项的单引号替换为双引号,因为 Kotlin 构建脚本使用双引号来定义字符串。
- 同时,我也将 fileTree 方法的参数包装在 mapOf 中,以正确传递参数。
- 添加完后,请保存文件并同步项目,以确保依赖项被正确添加。
需要注意的是,每当修改了任何gradle文件时,Android Studio都会弹出一个如图:
这个提示告诉我们,gradle文件自上次同步之后又发生了变化,需要再次同步才能使项目正常工作。这里只需要点击Sync Now就可以了,然后gradle会开始进行同步,把我们新添加的百分比布局库引入到项目当中。
2. 接下来修改activity_main.xml中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<androidx.percentlayout.widget.PercentFrameLayout
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">
<Button
android:id="@+id/button1"
android:text="Button1"
android:layout_gravity="right|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%" android:layout_height="0dp" android:layout_width="0dp"/>
<Button
android:id="@+id/button2"
android:text="Button2"
android:layout_gravity="left|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%" android:layout_height="0dp" android:layout_width="0dp"/>
<Button
android:id="@+id/button3"
android:text="Button3"
android:layout_gravity="right|bottom"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%" android:layout_height="0dp" android:layout_width="0dp"/>
<Button
android:id="@+id/button4"
android:text="Button4"
android:layout_gravity="left|top"
app:layout_widthPercent="50%"
app:layout_heightPercent="50%" android:layout_height="0dp" android:layout_width="0dp"/>
</androidx.percentlayout.widget.PercentFrameLayout>
- 最外层我们使用了PercentFrameLayout,由于百分比布局并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。然后还必须定义一个app的命名空间,这样才能使用百分比布局的自定义属性。
- 在PercentFrameLayout中我们定义了4个按钮,使用app:layout_widthPercent属性将各按钮的宽度指定为布局的50%,使用app:layout_heightPercent属性将各按钮的高度指定为布局的50%。这里之所以能使用app前缀的属性就是因为刚才定义了app的命名空间,当然我们一直能使用android前缀的属性也是同样的道理。
- 不过PercentFrameLayout还是会继承FrameLayout的特性,即所有的控件默认都是摆放在布局的左上角。那么为了让这4个按钮不会重叠,这里还是借助了layout_gravity来分别将这4个按钮放置在布局的左上、右上、左下、右下4个位置。
- 现在我们已经可以重新运行程序了,不过如果你使用的是老版本的AndroidStudio,可能会在activity_main.xml中看到一些错误提示:
这是因为老版本的Android Studio中内置了布局的检查机制,认为每一个控件都应该通过android:layout_width和android:layout_height属性指定宽高才是合法的。而其实我们是通过app:layout_widthPercent和app:layout_heightPercent属性来指定宽高的,所以Android Studio没检测到。不过这个错误提示并不影响程序运行,直接忽视就可以了。
运行效果:
8. 自定义控件🎯
作用:复用,组合
8.1 控件和布局的继承结构
我们之前所用的所有控件都是直接或间接继承自View的,所用的所有布局都是直接或间接继承自ViewGroup的。
View是Android中最基本的一种UI组件,它可以在屏幕上绘制一块矩形区域,并能响应这块区域的各种事件,因此,我们使用的各种控件其实就是在View的基础之上又添加了各自特有的功能。而ViewGroup则是一种特殊的View,它可以包含很多子View和子ViewGroup,是一个用于放置控件和布局的容器。
8.2 引入布局——一键创建多个控件
一般我们的程序中可能有很多个活动都需要标题栏,标题栏里面通常由好多个控件。如果在每个活动的布局中都编写一遍同样的标题栏代码,明显就会导致代码的大量重复。这个时候我们就可以使用引入布局的方式来解决这个问题。
- 创建布局:在layout文件夹下新建一个布局title.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:background="@color/black">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title_back"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@color/design_default_color_primary"
android:text="Back"
android:textColor="#fff"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/title_text"
android:layout_gravity="center"
android:layout_weight="1"
android:text="Title Text"
android:textColor="#fff"
android:textSize="24sp"/>
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/title_edit"
android:layout_gravity="center"
android:layout_margin="5dp"
android:background="@color/design_default_color_primary"
android:text="Edit"
android:textColor="#fff"/>
</LinearLayout>
- 我们在LinearLayout中分别加入了两个Button和一个TextView,左边的Button可用于返回,右边的Button可用于编辑,中间的TextView则可以显示一段标题文本。android:background用于为布局或控件指定一个背景,可以使用颜色或图片来进行填充。
- 另外,在两个Button中我们都使用了android:layout_margin这个属性,它可以指定控件在上下左右方向上偏移的距离,当然也可以使用android:layout_marginLeft或android:layout_marginTop等属性来单独指定控件在某个方向上偏移的距离。
- 使用标题栏,修改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">
<include layout="@layout/title"/>
</LinearLayout>
使用这种方式,不管有多少布局需要添加标题栏,只需一行include语句就可以了。
8.3 自定义控件——一键为多个控件写好事件
引入布局的技巧确实解决了重复编写布局代码的问题,但是如果布局中有一些控件要求能够响应事件,我们还是需要在每个活动中为这些控件单独编写一次事件注册的代码。
比如说标题栏中的返回按钮,其实不管是在哪一个活动中,这个按钮的功能都是相同的,即销毁当前活动。而如果在每一个活动中都需要重新注册一遍返回按钮的点击事件,无疑会增加很多重复代码,这种情况最好是使用自定义控件的方式来解决。
- 新建一个java类TitleLayout继承自LinearLayout,让它成为我们自定义的标题栏控件
package com.example.uicustomviews;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
public class TitleLayout extends LinearLayout{
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
}
}
- 首先重写了LinearLayout中带有两个参数的构造函数,在布局中引入TitleLayout控件就会调用这个构造函数。
- 然后在构造函数中需要对标题栏布局进行动态加载,这就要借助LayoutInflater来实现了。通过LayoutInflater的from()方法可以构建出一个LayoutInflater对象,然后调用inflate()方法就可以动态加载一个布局文件,inflate()方法接收两个参数,第一个参数是要加载的布局文件的id,这里我们传入R.layout.title,第二个参数是给加载好的布局再添加一个父布局,这里我们想要指定为TitleLayout,于是直接传入this。
- 在布局文件中添加这个自定义控件,修改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">
<com.example.uicustomviews.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
添加自定义控件和添加普通控件的方式基本是一样的,只不过在添加自定义控件的时候,我们需要指明控件的完整类名,包名在这里是不可以省略的。
重新运行程序,发现此时效果和使用引入布局方式的效果是一样的。
3. 尝试为标题栏中的按钮注册点击事件,修改TitleLayout中的代码,如下所示:
public class TitleLayout extends LinearLayout{
public TitleLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
((Activity)getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(getContext(),"You clicked Edit button", Toast.LENGTH_SHORT).show();
}
});
}
}
首先还是通过findViewById()方法得到按钮的实例,然后分别调用setOnClickListener()方法给两个按钮注册了点击事件,当点击返回按钮时销毁掉当前的活动,当点击编辑按钮时弹出一段文本。
这样,每当我们在一个布局中引入TitleLayout时,返回按钮和编辑按钮的点击事件就已经自动实现好了,这就省去了很多编写重复代码的工作。
总结:
结合8.2和8.3当我们有很多活动需要相同的控件和功能时可以这么做:
- 建一个新的布局,把需要的控件写进来,然后用引入
- 建一个新的java类TitleLayout继承自LinearLayout,然后写好前面那些控件的功能,最后把这个类当做一个控件给加进到activity_main.xml中。
9. 最难用的控件——ListView🎯
ListView允许用户通过手指上下滑动的方式将屏幕外的数据滚动到屏幕内,同时屏幕上原有的数据则会滚动出屏幕。比如查看QQ聊天记录,翻阅微博最新消息,等等。不过比起前面介绍的几种控件,ListView的用法也相对复杂了很多。
9.1 基本用法
- 在activity_main.xml中加入ListView控件,先为ListView指定一个id,然后将宽度和高度都设置为match_parent,这样ListView也就占满了整个布局的空间。
<?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">
<ListView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/list_view"/>
</LinearLayout>
- 在MainActivity中创建数据并借助适配器将数据传递给ListView
public class MainActivity extends AppCompatActivity {
private String[] data = {"Apple","Banana","Orange","Watermelon",
"Pear","Grape","Pineapple","Strawberry", "Cherry","Mango",
"Apple","Banana","Orange","Watermelon","Pear","Grape","Pineapple","Strawberry",
"Cherry","Mango"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ArrayAdapter<String> adapter = new ArrayAdapter<String>(
MainActivity.this,android.R.layout.simple_list_item_1,data);
ListView listView = (ListView)findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
}
- 数组中的数据是无法直接传递给ListView的,我们需要借助适配器来完成。Android中提供了很多适配器的实现类,其中我认为最好用的就是ArrayAdapter。它可以通过泛型来指定要适配的数据类型,然后在构造函数中把要适配的数据传入。
- ArrayAdapter有多个构造函数的重载,你应该根据实际情况选择最合适的一种。这里由于我们提供的数据都是字符串,因此将ArrayAdapter的泛型指定为String,然后在ArrayAdapter的构造函数中依次传入当前上下文、ListView子项布局的id,以及要适配的数据。
- 注意,使用了android.R.layout.simple_list_item_1作为ListView子项布局的id,这是一个Android内置的布局文件,里面只有一个TextView,可用于简单地显示一段文本。这样适配器对象就构建好了。
- 最后,还需要调用ListView的setAdapter()方法,将构建好的适配器对象传递进去,这样ListView和数据之间的关联就建立完成了。
效果如下:
9.2 定制ListView的界面
- 首先需要准备好一组图片,分别对应上面提供的每一种水果,待会我们要让这些水果名称的旁边都有一个图样。接着定义一个实体类,作为ListView适配器的适配类型。新建类Fruit,代码如下所示:
public class Fruit {
private final int imageId;
private final String name;
//快捷键:Alt+Insert 或者 工具栏:Code->Generate
public Fruit(String name, int imageId) {
this.name = name;
this.imageId = imageId;
}
public String getName() {
return name;
}
public int getImageId() {
return imageId;
}
}
- Fruit类中只有两个字段,name表示水果的名字,imageId表示水果对应图片的资源id。然后需要为ListView的子项指定一个我们自定义的布局,在layout目录下新建fruit_item.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">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="center_vertical"
android:layout_marginLeft="10dp"/>
</LinearLayout>
在这个布局中,定义了一个ImageView用于显示水果的图片,又定义了一个TextView用于显示水果的名称,并让TextView在垂直方向上居中显示。
3. 接下来需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为Fruit类。新建类FruitAdapter,代码如下所示:
// 自定义的 FruitAdapter 继承自 ArrayAdapter<Fruit> 类,用于将 Fruit 对象绑定到 ListView 控件上
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId; // 用于保存 item 布局资源的 ID
// 构造函数,接收 Context、item 布局资源的 ID、数据源(List<Fruit> 对象)
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId; // 将传入的 item 布局资源 ID 保存到成员变量 resourceId 中
}
// 重写 getView 方法,用于创建每个列表项的视图
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Fruit fruit = getItem(position); // 获取当前项的 Fruit 实例
// 使用布局填充器(LayoutInflater)从指定的 item 布局资源创建一个视图
View view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 获取视图中的 ImageView 控件,用于显示水果的图片
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
// 设置ImageView的宽度和高度为100dp!!!
ViewGroup.LayoutParams layoutParams = fruitImage.getLayoutParams();
layoutParams.width = 100; // 设置宽度为100像素
layoutParams.height = 100; // 设置高度为100像素
fruitImage.setLayoutParams(layoutParams);
// 获取视图中的 TextView 控件,用于显示水果的名称
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
// 将当前水果对象的图片资源 ID 设置到 ImageView 控件上
fruitImage.setImageResource(fruit.getImageId());
// 将当前水果对象的名称设置到 TextView 控件上
fruitName.setText(fruit.getName());
// 返回创建好的视图作为列表项的显示
return view;
}
}
- FruitAdapter重写了父类的一组构造函数,用于将上下文、ListView子项布局的id和数据都传递进来。
- 重写了getView()方法,这个方法在每个子项被滚动到屏幕内的时候会被调用。
- 在getView()方法中,首先通过getItem()方法得到当前项的Fruit实例,然后使用LayoutInflater来为这个子项加载我们传入的布局。
- LayoutInflater的inflate()方法接收3个参数,第三个参数指定成false,表示只让在父布局中声明的layout属性生效,但不会为这个View添加父布局,因为一旦View有了父布局之后,它就不能再添加到ListView中了。这是ListView中的标准写法,当你以后对View理解得更加深刻的时候,再来读这段话就没有问题了。
- 调用View的findViewById()方法分别获取到ImageView和TextView的实例,并分别调用它们的setImageResource()和setText()方法来设置显示的图片和文字.
- 最后将布局返回,这样我们自定义的适配器就完成了
- 修改MainActivity中的代码
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();//初始化水果数据
FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit("Apple",R.drawable.pingguo);
fruitList.add(apple);
Fruit banana = new Fruit("Banana",R.drawable.xiangjiao);
fruitList.add(banana);
Fruit cherry = new Fruit("Cherry",R.drawable.yingtao);
fruitList.add(cherry);
Fruit grape = new Fruit("Grape",R.drawable.putao);
fruitList.add(grape);
Fruit mango = new Fruit("Mango",R.drawable.mangguo);
fruitList.add(mango);
Fruit orange = new Fruit("Orange",R.drawable.juzi);
fruitList.add(orange);
Fruit pear = new Fruit("Pear",R.drawable.li);
fruitList.add(pear);
Fruit watermelon = new Fruit("Watermelon",R.drawable.xigua);
fruitList.add(watermelon);
Fruit pineapple = new Fruit("Pineapple",R.drawable.boluo);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry",R.drawable.caomei);
fruitList.add(strawberry);
}
}
- 添加了一个initFruits()方法,用于初始化所有的水果数据。
- 在Fruit类的构造函数中将水果的名字和对应的图片id传入,把创建好的对象添加到水果列表中。
- 使用for循环将所有的水果数据添加了两遍,只添加一遍,数据量还不足以充满整个屏幕。
- 接着在onCreate()方法中创建了FruitAdapter对象,并将FruitAdapter作为适配器传递给ListView。
运行效果如下:
总结:
- 使用ListView显示,首先你要清楚每一行你要放些什么,把每一行要放的东西的名字写一个类放好,并且要有get方法
- 然后为这一行单独新建一个布局,把你要放的东西所属的类型的控件给做出来,并且做好布局。
- 新建一个自定义的适配器 XXXAdapter 继承自 ArrayAdapter 类,在构造函数中就获取到布局资源ID,然后在getView 方法中通过布局资源ID获取我们需要的控件,然后插入文字或图片。并且在这里面还可以设置控件的宽高等数据,可以实现图片的缩放。
- 最后在主活动代码中先实例化例化list,然后实例化我们写好的类的对象加到list里面,最后把我们的自定义的适配器实例化加到list上。
9.3 ListView的优化
目前我们有两个点做的不够好:
- 我们早就说过getView()方法在每个子项被滚动到屏幕内的时候都会被调用。那我们在该方法中的创建视图的语句也会每次都调用,就是下面这句:
View view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
因此每次都会将布局重新加载一遍,那么当ListView快速滚动的时候,这就会成为性能的瓶颈。 - 每次在getView()方法中都会调用View的findViewById()方法来获取一次控件的实例,就是下面这两句
ImageView fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
TextView fruitName = (TextView) view.findViewById(R.id.fruit_name);
注意:
为什么getView()方法在每个子项被滚动到屏幕内的时候都会被调用,而不是只调用一次就行了呢?
- 那是因为有时候子项过多,全部一次性加载完很浪费内存和时间。因此只会获取一部分子项,当列表视图需要显示新的子项时,它会调用getView方法来获取子项的视图。当一个子项被滚动到屏幕外部时,就会用我们后面提到的改进方法使得其视图不会被销毁,而是被缓存起来以备重用。这样,当新的子项需要在屏幕上显示时,可以重用之前被滚出屏幕的子项的视图,只需要更新视图的内容,而不需要重新创建整个视图。
- 这种重用机制可以大大提高列表视图的性能,特别是在有大量子项的情况下,因为它减少了创建和销毁视图的开销。因此,getView 方法会被频繁调用,但它只是更新视图内容,而不是每次都创建新的视图,这是为了优化性能的一种做法。
优化代码:
public class FruitAdapter extends ArrayAdapter<Fruit> {
private int resourceId;
public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
super(context,textViewResourceId,objects);
resourceId = textViewResourceId;
}
@Override
public View getView(int position,View convertView,ViewGroup parent) {
Fruit fruit = getItem(position);//获取当前项的Fruit实例
View view;
ViewHolder viewHolder;
if(convertView == null){//如果是第一次加载视图
view = LayoutInflater.from(getContext()).inflate(resourceId,parent,false);
viewHolder = new ViewHolder();//新建一个视图缓存区
viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
view.setTag(viewHolder);//将ViewHolder存储在View中
}else{//不是第一次加载视图
view = convertView;
viewHolder = (ViewHolder) view.getTag();//重新获取viewHolder
}
viewHolder.fruitImage.setImageResource(fruit.getImageId());
// 设置ImageView的宽度和高度为30dp
ViewGroup.LayoutParams layoutParams = viewHolder.fruitImage.getLayoutParams();
layoutParams.width = 100; // 设置宽度为30像素
layoutParams.height = 100; // 设置高度为30像素
viewHolder.fruitName.setText(fruit.getName());
return view;
}
class ViewHolder{//视图缓存区
ImageView fruitImage;
TextView fruitName;
}
}
优化一:使用getView()方法中的convertView参数,将之前加载好的布局进行缓存,以便之后可以进行重用。
优化二:新增了一个内部类ViewHolder,用于对控件的实例进行缓存。
- 当convertView为null的时候,创建一个ViewHolder对象,并将控件的实例都存放在ViewHolder里,然后调用View的setTag()方法,将ViewHolder对象存储在View中。
- 当convertView不为null的时候,则调用View的getTag()方法,把ViewHolder重新取出。
这样所有控件的实例都缓存在了ViewHolder里,就没有必要每次都通过findViewById()方法来获取控件实例了。
9.4 ListView的点击事件
- 使用setOnItemClickListener()方法为ListView注册一个监听器
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
用户点击了ListView中的任何一个子项时,就会回调onItemClick()方法。在这个方法中可以通过position参数判断出用户点击的是哪一个子项,然后获取到相应的水果,并通过Toast将水果的名字显示出来。
10. 滚动控件——RecyclerView🎯
ListView是有缺点的,比如说如果我们不使用一些技巧来提升它的运行效率,那么ListView的性能就会非常差。还有,ListView的扩展性也不够好,它只能实现数据纵向滚动的效果,如果我们想实现横向滚动的话,ListView是做不到的。为此,Android提供了一个更强大的滚动控件——RecyclerView。它可以说是一个增强版的ListView,不仅可以轻松实现和ListView同样的效果,还优化了ListView中存在的各种不足之处。
10.1 RecyclerView的基本用法
和百分比布局类似,RecyclerView也属于新增的控件,为了让RecyclerView在所有Android版本上都能使用,Android团队采取了同样的方式,将RecyclerView定义在了support库当中。
- 首先需要在项目的build.gradle中添加相应的依赖库
implementation ‘androidx.recyclerview:recyclerview:1.3.1’ - 在activity_main.xml页面中添加此控件
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 布局中加入了RecyclerView控件,先为RecyclerView指定一个id,然后将宽度和高度都设置为match_parent,这样RecyclerView也就占满了整个布局的空间。(需要注意的是,由于RecyclerView并不是内置在系统SDK当中的,所以需要把完整的包路径写出来。)
- 需要为RecyclerView准备一个适配器,新建FruitAdapter类,让这个适配器继承自
RecyclerView.Adapter,并将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类,代码如下所示:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList){
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
// 设置ImageView的宽度和高度为30dp
ViewGroup.LayoutParams layoutParams = holder.fruitImage.getLayoutParams();
layoutParams.width = 100; // 设置宽度为30像素
layoutParams.height = 100; // 设置高度为30像素
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
- 首先定义了一个内部类ViewHolder, ViewHolder要继承自RecyclerView.ViewHolder。
- 然后ViewHolder的构造函数中要传入一个View参数,这个参数通常就是RecyclerView子项的最外层布局,那么我们就可以通过findViewById()方法来获取到布局中的ImageView和TextView的实例了。
- 接着,FruitAdapter中也有一个构造函数,这个方法用于把要展示的数据源传进来,并赋值给一个全局变量mFruitList,后续的操作都将在这个数据源的基础上进行。
- 由于FruitAdapter是继承自RecyclerView.Adapter的,那么就必须重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法。
- onCreateViewHolder()方法是用于创建ViewHolder实例的,在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到构造函数当中,最后将ViewHolder的实例返回。
- onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。
- getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
- 修改MainActivity中的代码,开始使用RecyclerView了
public class MainActivity extends AppCompatActivity {
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits();//初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit("Apple",R.drawable.pingguo);
fruitList.add(apple);
Fruit banana = new Fruit("Banana",R.drawable.xiangjiao);
fruitList.add(banana);
Fruit cherry = new Fruit("Cherry",R.drawable.yingtao);
fruitList.add(cherry);
Fruit grape = new Fruit("Grape",R.drawable.putao);
fruitList.add(grape);
Fruit mango = new Fruit("Mango",R.drawable.mangguo);
fruitList.add(mango);
Fruit orange = new Fruit("Orange",R.drawable.juzi);
fruitList.add(orange);
Fruit pear = new Fruit("Pear",R.drawable.li);
fruitList.add(pear);
Fruit watermelon = new Fruit("Watermelon",R.drawable.xigua);
fruitList.add(watermelon);
Fruit pineapple = new Fruit("Pineapple",R.drawable.boluo);
fruitList.add(pineapple);
Fruit strawberry = new Fruit("Strawberry",R.drawable.caomei);
fruitList.add(strawberry);
}
}
}
- 这里使用了一个同样的initFruits()方法,用于初始化所有的水果数据。
- 接着在onCreate()方法中先获取到RecyclerView的实例
- 然后创建了一个LinearLayout-Manager对象,并将它设置到RecyclerView当中。LayoutManager用于指定RecyclerView的布局方式,这里使用的LinearLayoutManager是线性布局的意思,可以实现和ListView类似的效果。
- 接下来我们创建了FruitAdapter的实例,并将水果数据传入到FruitAdapter的构造函数中
- 最后调用RecyclerView的setAdapter()方法来完成适配器设置,这样RecyclerView和数据之间的关联就建立完成了。
最终的效果是一样的,这里就不放图了
总结:
- 使用ListView显示,首先你要清楚每一行你要放些什么,把每一行要放的东西的名字写一个类放好,
并且要有get方法。这一步是和之前一样的,我们建好的Fruit类。 - 然后为这一行单独新建一个布局,把你要放的东西所属的类型的控件给做出来,并且做好布局。也是一样的,我们建好的fruit_item.xml
- 新建一个自定义的适配器继承自RecyclerView.Adapter类,并且将泛型指定为FruitAdapter.ViewHolder。其中,ViewHolder是我们在FruitAdapter中定义的一个内部类,里面存储着我们的图片和文字的控件对象。
- 这个适配器还有一点讲究,适配器的构造函数把要展示的数据源传进来,并赋值给一个全局变量mFruitList,后续的操作都将在这个数据源的基础上进行
- 并且还要重写onCreateViewHolder()、onBindViewHolder()和getItemCount()这3个方法
- onCreateViewHolder()方法是用于创建ViewHolder实例的,在这个方法中将fruit_item布局加载进来,然后创建一个ViewHolder实例,并把加载出来的布局传入到ViewHolder的构造函数当中,最后将ViewHolder的实例返回。
- onBindViewHolder()方法是用于对RecyclerView子项的数据进行赋值的,会在每个子项被滚动到屏幕内的时候执行,这里我们通过position参数得到当前项的Fruit实例,然后再将数据设置到ViewHolder的ImageView和TextView当中即可。
- getItemCount()方法就非常简单了,它用于告诉RecyclerView一共有多少子项,直接返回数据源的长度就可以了。
- 最后在主活动代码中先实例化例化list,然后实例化我们写好的类的对象加到list里面,然后获得我们创建好的RecyclerView实例并设置上LinearLayoutManager对象,最后把我们自己定义的适配器实例化并加到RecyclerView实例上。
10.2 实现横向滚动布局
ListView的扩展性并不好,它只能实现纵向滚动的效果,如果想进行横向滚动的话,就需要靠RecyclerView了。
- 改变我们设置的控件布局为上图下字,修改fruit_item.xml中的代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="100dp"
android:layout_height="wrap_content">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"
android:layout_gravity="center_horizontal"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="center_horizontal"
android:layout_marginStart="10dp"
/>
</LinearLayout>
- 将LinearLayout改成垂直方向排列,并把宽度设为100dp。这里将宽度指定为固定值是因为每种水果的文字长度不一致,如果用wrap_content的话,RecyclerView的子项就会有长有短,非常不美观;而如果用match_parent的话,就会导致宽度过长,一个子项占满整个屏幕。
- 然后将ImageView和TextView都设置成了在布局中水平居中,并且使用layout_marginStart属性让文字和图片之间保持一些距离。
- 改变LinearLayoutManager的方向,改变MainActivity中的代码
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
//调用LinearLayoutManager的setOrientation()方法来设置布局的排列方向,默认是纵向排列的
layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
recyclerView.setLayoutManager(layoutManager);
可以用手指在水平方向上滑动来查看屏幕外的数据。
为什么ListView很难或者根本无法实现的效果在RecyclerView上这么轻松就能实现了呢?
这主要得益于RecyclerView出色的设计。ListView的布局排列是由自身去管理的,而RecyclerView则将这个工作交给了LayoutManager,LayoutManager中制定了一套可扩展的布局排列接口,子类只要按照接口的规范来实现,就能定制出各种不同排列方式的布局了。
除了LinearLayoutManager之外,RecyclerView还给我们提供了GridLayoutManager和StaggeredGridLayoutManager这两种内置的布局排列方式。GridLayoutManager可以用于实现网格布局,StaggeredGridLayoutManager可以用于实现瀑布流布局。
10.3 实现瀑布流布局
- 改变我们设置的控件布局为上图下字,修改fruit_item.xml中的代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_image"
android:layout_gravity="center_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/fruit_name"
android:layout_gravity="left"
android:layout_marginLeft="10dp"
tools:ignore="RtlHardcoded" />
</LinearLayout>
- 首先将LinearLayout的宽度由100dp改成了match_parent,因为瀑布流布局的宽度应该是根据布局的列数来自动适配的,而不是一个固定值。
- 另外使用了layout_margin属性来让子项之间互留一点间距,这样就不至于所有子项都紧贴在一起。
- 还有就是将TextView的对齐属性改成了居左对齐,因为待会我们会将文字的长度变长,如果还是居中显示就会感觉怪怪的。
- 接着修改MainActivity中的代码,如下所示
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3,StaggeredGridLayoutManager.VERTICAL);
recyclerView.setLayoutManager(layoutManager);
另外为了有更好的展示效果,我们对private void initFruits() 也进行修改:
private void initFruits() {
for (int i = 0; i < 2; i++) {
Fruit apple = new Fruit(getRandomLengthName("Apple"), R.drawable.apple_pic);
fruitList.add(apple);
Fruit banana = new Fruit(getRandomLengthName("Banana"), R.drawable.banana_pic);
fruitList.add(banana);
Fruit cherry = new Fruit(getRandomLengthName("Cherry"), R.drawable.cherry_pic);
fruitList.add(cherry);
Fruit grape = new Fruit(getRandomLengthName("Grape"), R.drawable.grape_pic);
fruitList.add(grape);
Fruit mango = new Fruit(getRandomLengthName("Mango"), R.drawable.mango_pic);
fruitList.add(mango);
Fruit orange = new Fruit(getRandomLengthName("Orange"), R.drawable.orange_pic);
fruitList.add(orange);
Fruit pear = new Fruit(getRandomLengthName("Pear"), R.drawable.pear_pic);
fruitList.add(pear);
Fruit watermelon = new Fruit(getRandomLengthName("Watermelon"), R.drawable.watermelon_pic);
fruitList.add(watermelon);
Fruit pineapple = new Fruit(getRandomLengthName("Pineapple"), R.drawable.pineapple_pic);
fruitList.add(pineapple);
Fruit strawberry = new Fruit(getRandomLengthName("Strawberry"), R.drawable.strawberry_pic);
fruitList.add(strawberry);
}
}
private String getRandomLengthName(String name){
Random random = new Random();
int length = random.nextInt(20) + 1;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(name);
}
return builder.toString();
- 我们创建了一个StaggeredGridLayoutManager的实例。StaggeredGridLayoutManager的构造函数接收两个参数,第一个参数用于指定布局的列数,传入3表示会把布局分为3列;第二个参数用于指定布局的排列方向,传入StaggeredGrid-LayoutManager.VERTICAL表示会让布局纵向排列,最后再把创建好的实例设置到RecyclerView当中就可以了
- 由于瀑布流布局需要各个子项的高度不一致才能看出明显的效果,为此我又使用了一个小技巧。这里我们把眼光聚焦在getRandomLengthName()这个方法上,这个方法使用了Random对象来创造一个1到20之间的随机数,然后将参数中传入的字符串随机重复几遍。在initFruits()方法中,每个水果的名字都改成调用getRandomLengthName()这个方法来生成,这样就能保证各水果名字的长短差距都比较大,子项的高度也就各不相同了。
10.4 RecyclerView的点击事件
不同于ListView的是,RecyclerView并没有提供类似于setOnItemClickListener()这样的注册监听器方法,而是需要我们自己给子项具体的View去注册点击事件,相比于ListView来说,实现起来要复杂一些。
这里要提一下为什么ListView有监听器方法而RecyclerView没有,那是因为这个方法有弊端。setOnItemClickListener()方法注册的是子项的点击事件,但如果我想点击的是子项里具体的某一个按钮呢?虽然ListView也是能做到的,但是实现起来就相对比较麻烦了。为此,RecyclerView干脆直接摒弃了子项点击事件的监听器,所有的点击事件都由具体的View去注册,就再没有这个困扰了。
- 要实现点击事件就要在控件对象上下功夫,所以我们修改FruitAdapter中的代码,如下所示:
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder>{
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder{
View fruitView;
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view){
super(view);
fruitView = view;
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList){
mFruitList = fruitList;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.fruit_item,parent,false);
final ViewHolder holder = new ViewHolder(view);
holder.fruitView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You clicked view " + fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
holder.fruitImage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You clicked image " + fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
holder.fruitName.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
int position = holder.getAdapterPosition();
Fruit fruit = mFruitList.get(position);
Toast.makeText(view.getContext(),"You clicked Text " + fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
return holder;
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
// 设置ImageView的宽度和高度为30dp
ViewGroup.LayoutParams layoutParams = holder.fruitImage.getLayoutParams();
layoutParams.width = 100; // 设置宽度为30像素
layoutParams.height = 100; // 设置高度为30像素
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}
- 先是修改了ViewHolder,在ViewHolder中添加了fruitView变量来保存子项最外层布局的实例,然后在onCreateViewHolder()方法中注册点击事件就可以了,注意这里是对我们的最外层布局(fruitView)注册点击事件,因为它是最外层的。这里分别为最外层布局(fruitView)和ImageView以及TextView都注册了点击事件,RecyclerView的强大之处也在这里,它可以轻松实现子项中任意控件或布局的点击事件。
- 我们在两个点击事件中先获取了用户点击的position,然后通过position拿到相应的Fruit实例,再使用Toast分别弹出两种不同的内容以示区别。现在重新运行代码,并点击香蕉的图片部分,可以看到,这时触发了ImageView的点击事件。
- 如果TextView并没有注册点击事件,那么点击文字这个事件会被子项的最外层布局捕获到
11. 编写界面的练习-聊天窗口
主要新介绍了一个Nine-Patch图片,9-patch是一种缩放不失真或变形的图片格式,常用于聊天框的实现。