第一章 Android基础入门

Android是Google公司基于Linux平台开发的手机及平板电脑的操作系统,它自问世以来,受到了前所未有的关注,并迅速成为移动平台最受欢迎的操作系统之一。

1.1 Android简介

Android操作系统最初是由安迪·鲁宾(Andy Rubin)开发出的,后来被Google收购,并于2007年11月5日正式向外界展示了这款系统。随后Google以Apache开源许可证的授权方式,发布了Android操作系统的源代码。

安卓系统的前9个大版本都用甜点的名字来取名,后面改用字母

image-20250305143645483

Dalvik虚拟机

Dalvik是Google公司设计的,用于在Android平台上运行的虚拟机,其指令集基于寄存器架构,执行其特有的dex文件来完成对象生命周期管理、堆栈管理、线程管理、安全异常管理、垃圾回收等重要功能。每一个Android应用在底层都会对应一个独立的Dalvik虚拟机实例,其代码在虚拟机的解释下得以执行,Dalvik虚拟机编译文件的过程如下图所示。

image-20250305143830206

1.2 开发环境搭建

Android Studio是Google官方推荐的Android开发工具,基于IntelliJ IDEA,集成了Android开发所需的所有功能。

安装IDE

  1. Android Studio安装包可以从Android Studio 下载文件归档 | Android Developers进行下载。这里我们以Windows 64系统为例,下载ANDROID STUDIO 3.2.0版本。Android Studio下载页面如下图所示。注意要选择语言为英文,否则不会显示对应界面。
  2. 成功下载Android Studio安装包后,双击后缀名为.exe的文件,进入Welcome to Android Studio Setup页面。
  3. 单击上一页图中的 Next 按钮,进入Choose Components页面,全部勾选
  4. 单击上一页图中的 Install 按钮进入Installing页面开始安装,可以自定义路径
  5. 等待安装完毕后,单击图中的 Finish 按钮,至此,Android Studio的安装全部完成

配置IDE

  1. 如果我们在上一页图的页面中勾选了Start Android Studio选项,安装完成之后Android Studio会自动启动,会弹出一个Complete Installation对话框(选择导入Android Studio配置文件位置的窗口)。如果之前安装过Android Studio,想要导入之前的配置文件,则可以选择第一条,此处可以根据实际情况进行选择。
  2. 我们选择上一页的第二条(不导入设置)之后会进入Android Studio的开启窗口
  3. 弹出 Android Studio First Run: Unable to access Android SDK add-on list 错误,表示未安装SDK,选择Cancel后点击Next进入Choose the type of setup you want for Android Studio,使用 Standrad
  4. 进入主题选择页面,选择自己喜欢的颜色,点击Next
  5. 确认设置Finished,并开始下载组件
  6. 等待安装完成(注意有无报错),进入主界面

模拟器创建

Android程序可以运行到手机和平板等物理设备上,当运行Android程序时,没有手机或平板等物理设备,可以使用Android系统提供的模拟器。模拟器是一个可以运行在电脑上的虚拟设备。在模拟器上可预览和测试Android应用程序。

  1. 在主界面随意创建一个项目,后在IDE右上角找到Device Manager
  2. 单击上一页图中的 + Create Virtual Device… 按钮,此时会进入选择模拟设备的Select Hardware页面,如下图所示。
image-20250305151017591
  1. 我们选择上一页图中的【Phone】【Nexus 4】选项(此选项可根据自己需求选择不同屏幕分辨率的模拟器),单击 Next 按钮进入 System Image页面,如下图所示。
image-20250305151157780
  1. 选中上一页图中的Oreo系统版本,单击Download进入License Agreement页面。
  2. 选中上一页图中的“Accept”按钮接受页面中显示的信息,并单击“Next”按钮进入Component Installer页面
  3. 等待组件安装完成,点击Finished按钮
  4. 然后回到父窗口,此时选中System Image页面中系统版本名称为Oreo的条目,单击“Next”按钮进入Android Virtual Device(AVD)页面,如下图所示。
image-20250305180723614
  1. 单击上一页图中的“Finish”按钮,完成模拟器的创建。此时在Your Virtual Devices页面中会显示创建完成的模拟器。
  2. 然后点击运行(三角形状)按钮
  3. 运行之后效果如下
image-20250305181315580

配置SDK

  1. 打开Android Studio,单击导航栏中的SDKManager,进入Default Settings窗口,如下图所示。

image-20250305181753516

  1. SDK Platforms选项卡下选择Android 8.1 (Oreo)条目,单击“OK”按钮会弹出确认安装SDK组件的Confirm Change窗口。
  2. 接着勾选Default Settings窗口右下角的Show Package Details选项,会打开Android SDK Build-Tools中的SDK版本列表信息,在列表中勾选27.0.0条目,单击“OK”按钮会弹出Confirm Change窗口,点击确认。
  3. 等待全部下载完成

1.3 第一个Android程序

直接运行示例程序,你将会得到一个程序的运行界面

image-20250305182747432

1.4 Android程序结构

熟悉Android程序结构,能够归纳Android程序中常用的文件和文件夹的作用

image-20250305183429846 image-20250305183656851

1.5 资源的管理和使用

所有的控件和(图片)资源都会被映射到R对象上,变成一个唯一的ID

图片资源

图片资源:扩展名为.png.jpg.gif.9.png等的文件。

图片资源分类:

  • 应用图标资源:存放在mipmap文件夹
  • 界面中使用的图片资源:存放在drawable文件夹

屏幕密度匹配规则: 根据对应的屏幕密度去匹配不同分辨率的图片文件夹

密度范围值 mipmap文件夹 drawable文件夹
120~160dpi mipmap_mdpi mipmap_mdpi
160~240dpi mipmap_hdpi drawable_hdpi
240~320dpi mipmap_xdpi drawable_xdpi
320~480dpi mipmap_xxdpi drawable_xxdpi
480~640dpi mipmap_xxxdpi drawable_xxxdpi

调用图片资源有两种方式:

  1. 使用Java代码:
    • 现在对应界面Activity中加入用于显示的ImageView控件
    • 在你的Activity或Fragment中,通过代码动态设置ImageView的图片资源。
import android.os.Bundle;
import android.widget.ImageView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 找到ImageView
        ImageView imageView = findViewById(R.id.imageView);

        // 设置图片资源
        imageView.setImageResource(R.drawable.image); // 替换为你的图片资源ID
    }
}
  1. XML布局文件中调用图片资源
@mipmap/ic_launcher   		//调用mipmap文件夹中的资源文件
@drawable/icon              //调用以drawable开头的文件夹中的资源文件

<ImageView
           android:id="@+id/imageView"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginBottom="492dp"
           android:src="@drawable/ic_launcher_background"  给属性设置资源
           app:layout_constraintBottom_toBottomOf="parent"
           tools:layout_editor_absoluteX="151dp" /> 

主题和样式资源

主题:包含一种或多种格式化属性的集合,在程序中调用主题资源可改变窗体的样式

主题资源定义位置:在res/values目录下的styles.xml文件中

定义主题资源的标签:

<style></style>:定义主题的标签
<item></item>:设置主题样式的标签
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
</style>

想要调用styles.xml文件中定义的主题,可以在AndroidManifest.xml文件中设置,也可以在代码中设置。

  • 在AndroidManifest.xml文件中设置主题
android:theme ="@style/AppTheme"
  • 在Java代码中设置主题
setTheme(R.style.AppTheme);

样式:设置View的宽度、高度和背景颜色等信息。

样式资源定义位置:res/values目录下的styles.xml文件中

样式的标签:

<style></style>:定义样式的标签
<item></item>:设置控件样式的标签

在XML布局文件中引用样式

<TextView
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:text="我是文本"
          style="@style/textViewStyle"
          />

布局资源

布局资源:通常用于搭建程序中的各个界面。

布局资源存放位置:res/layout文件夹中

调用布局资源的方式有2种:

  • 通过Java代码调用布局资源文件
//在Activity的onCreate()方法中调用activity_main.xml布局文件
setContentView(R.layout.activity_main);
  • 在XML布局文件中调用布局资源文件
//在XML布局文件中调用activity_main.xml布局文件
<include layout="@layout/activity_main"/>

字符串资源

字符串:用于显示界面上的文本信息。

字符串资源定义位置:res/values目录下的strings.xml文件中

字符串标签:

<string></string>:定义字符串的标签

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">My Application</string>
    <string name="hello_world">Hello, World!</string>
    <string name="welcome_message">Welcome to %1$s!</string>
</resources>

调用字符串资源的方式有2种:

  • 通过Java代码调用字符串资源
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取字符串资源
        String helloWorld = getResources().getString(R.string.hello_world);
        String welcomeMessage = getString(R.string.welcome_message, "Android");

        // 设置TextView的文本
        TextView textView = findViewById(R.id.textView);
        textView.setText(helloWorld);

        TextView textView2 = findViewById(R.id.textView2);
        textView2.setText(welcomeMessage);
    }
}
  • 在XML布局文件中调用字符串资源

<TextView
          android:id="@+id/textView2"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:text="@string/hello_world" />

颜色资源

颜色:用于显示控件的不同色彩效果。

颜色资源定义位置:res/values/colors.xml文件中

颜色标签:

<color></color>:定义颜色的标签

<resources>
    <color name="colorPrimary">#3F51B5</color>
</resources>

调用颜色资源的方式有2种:

  1. 通过Java代码调用颜色资源
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取颜色资源
        int primaryColor = getResources().getColor(R.color.primary_color);
        int secondaryColor = getResources().getColor(R.color.secondary_color);

        // 设置TextView的颜色
        TextView textView = findViewById(R.id.textView);
        textView.setTextColor(primaryColor);

        TextView textView2 = findViewById(R.id.textView2);
        textView2.setTextColor(secondaryColor);
    }
}
  1. 在XML布局文件中调用颜色资源
<TextView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:text="我是帅哥"
    android:textColor="@color/red" // 此行
    style="@style/textViewStyle"
/>

定义颜色值
在Android中,颜色值是由RGB(红、绿、蓝)三原色和一个透明度(Alpha)表示,颜色值必须以#开头,#后面显示Alpha-Red-Green-Blue形式的内容。其中,Alpha值可以省略,如果省略,表示颜色默认是完全不透明的,一般情况下,使用以下4种形式定义颜色

  • #RGB
  • #ARGB
  • #RRGGBB
  • #AARRGGBB

尺寸资源

尺寸:用于设置View的宽高和View之间的间距值。

尺寸资源定义位置:res/values/dimens.xml文件中,如果程序中没有dimens.xml文件,可自行创建。

尺寸标签:

<dimen></dimen>:定义尺寸的标签
<resources>
    <dimen name="activity_horizontal_margin">16dp</dimen>
</resources>

调用尺寸的方式:

  1. 通过java代码
import android.os.Bundle;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 获取尺寸资源
        float textSizeSmall = getResources().getDimension(R.dimen.text_size_small);
        int marginLarge = getResources().getDimensionPixelSize(R.dimen.margin_large);

        // 设置TextView的文本大小和边距
        TextView textView = findViewById(R.id.textView);
        textView.setTextSize(textSizeSmall);
        textView.setPadding(marginLarge, marginLarge, marginLarge, marginLarge);
    }
}
  1. 通过布局文件
<TextView
        android:id="@+id/testView"
        // 此处使用了资源文件,但是不是只能此属性使用
        android:layout_marginHorizontal="@dimen/activity_horizontal_margin" 
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="我是HelloWorld"
        android:textColor="@color/red"
        style="@style/textViewStyle"
    />

Android支持的尺寸单位:

  • px(pixels,像素):每个px对应屏幕上的一个点。
  • dp(Density-independent Pixels,设备独立像素):是一种与屏幕密度无关的尺寸单位。
  • sp(scaled pixels,比例像素):主要处理字体的大小,可以根据用户字体大小首选项进行缩放。
  • in(inches,英寸):标准长度单位。
  • pt(points,磅):屏幕物理长度单位,1磅为1/72英寸。
  • mm(Millimeters,毫米):屏幕物理长度单位。

1.6 程序调试

单元测试

单元测试是指在Android程序开发过程中对最小的功能模块进行测试,单元测试包括Android单元测试Junit单元测试

Android设备 JVM环境
需要连接Android设备 不需要依赖Android设备,本地就可运行
速度慢 速度快
适合调用Android API的单元测试 适合对Java代码功能进行单元测试

Android Studio 3.2版本在创建项目时,会默认在app/src/androidTestapp/src/test文件夹中创建Android单元测试类ExampleInstrumentedTest和Junit单元测试类ExampleUnitTest。

  1. Android单元测试类ExampleInstrumentedTest
  • 使用@RunWith(AndroidJUnit4.class)注解ExampleInstrumentedTest类
  • @Test注解类中的方法
  1. Junit单元测试类ExampleUnitTest
  • @Test注解类中的方法
image-20250312145114559

点击useAppContent()方法左边的箭头,运行该程序到模拟界面,在Android Studio底部导航栏中单击”Run“图标查看运行成功的结果。

image-20250312145442308

接下来修改文件ExampleUnitTest.java中的assertEquals()方法中的参数,使测试addition_isCorrect()方法时,显示错误信息,修改的具体代码如下:

assertEquals(4, 1 + 2); // 4 != 3

报错信息同样显示如下:

image-20250312145541005

Android Studio3.2版本在创建项目时,会自动在build.gradle文件中添加单元测试的支持库,如果在进行单元测试时,程序中的build.gradle文件中没有添加单元测试的支持库,则需要手动进行添加

>dependencies {
   ......
   testImplementation 'junit:junit:4.12'
   androidTestImplementation 'com.android.support.test:runner:1.0.2'
   androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
>}

日志输出LogCat

LogCat是Android中的命令行工具,用于获取程序从启动到关闭的日志信息。

Log类所输出的日志内容分为六个级别

级别 Log类中的静态方法
Verbose Log.v()
Debug Log.d()
Info Log.i()
Warning Log.w()
Error Log.e()
Assert Log.wtf()

在Java代码中,可以通过调用Log的静态方法来打印日志:

image-20250312150347436

LogCat输出的日志信息根据不同的情况显示不同的颜色:

级别 显示信息 日志信息颜色
verbose(V) 全部信息 黑色
debug(D) 调试信息 蓝色
info(I) 一般信息 绿色
warning(W) 警告信息 橙色
error(E) 错误信息 红色
assert 断言失败后的错误消息 红色

第二章 基本布局实践

2.1 View视图

所有的UI元素都是通过ViewViewGroup构建的,对于一个Android应用的用户界面来说,ViewGroup作为容器盛装界面中的控件,它可以包含普通的View控件,也可以包含ViewGroup。

image-20250312184956869

在实现Android界面效果之前,我们首先需要编写界面布局,界面布局的编写方式有2种,第1种是在XML文件中编写布局第2种是在Java代码中编写布局。

2.2 界面布局方式

推荐使用在XML文件中编写布局有效的将界面中的布局代码与Java代码隔离,使程序的结构更加清晰。

在Java代码中编写布局:在Android中所有布局和控件的对象都可以通过new关键字创建出来,将创建的View控件添加到ViewGroup布局中,从而实现View控件在布局界面中显示。

xml方式

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"  // 相关布局继承自ViewGroup
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <TextView // 继承自View
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="使用XML布局文件控制UI界面"
        android:textColor="#ff0000"
        android:textSize="18sp"
        android:layout_centerInParent="true"/>
</RelativeLayout>

java方式

RelativeLayout relativeLayout = new RelativeLayout(this);
RelativeLayout.LayoutParams params =  new RelativeLayout.LayoutParams(
    RelativeLayout.LayoutParams.WRAP_CONTENT,
    RelativeLayout.LayoutParams.WRAP_CONTENT);
params.addRule(RelativeLayout.CENTER_IN_PARENT);            //设置布局中的控件居中显示
TextView textView = new TextView(this);                     //创建TextView控件
textView.setText("Java 代码实现界面布局");                     //设置TextView的文字内容
textView.setTextColor(Color.RED);                           //设置TextView的文字颜色
textView.setTextSize(18);                                   //设置TextView的文字大小
relativeLayout.addView(textView, params);                   //添加TextView对象和TextView的布局属性
setContentView(relativeLayout);                             //设置在Activity中显示RelativeLayout

2.3 界面布局的通用属性

四种常用布局\布局类型 特点
线性布局 以水平或垂直方向排列
相对布局 通过相对定位排列
帧布局 开辟空白区域,帧里的控件(层)叠加
表格布局 表格形式排列

Android系统提供的四种常用布局直接或者间接继承自ViewGroup,因此这四种常用布局也支持在ViewGroup中定义属性,这些属性可以看作是布局的通用属性。这些通用属性如下表所示。

属性名称 功能描述
android:id 设置布局的标识
android:layout_width 设置布局的宽度
android:layout_height 设置布局的高度
android:background 设置布局的背景
android:layout_margin 设置当前布局与屏幕边界或与周围控件的距离
android:padding 设置当前布局与该布局中控件的距离
  • android:id
    1. 用于设置当前布局的唯一标识。
    2. 在XML文件中它的属性值是通过“@+id/属性名称”定义
  • android:layout_width
    1. 用于设置布局的宽度,其值可以是具体的尺寸,如50dp,也可以是系统定义的值。
    2. 系统定义的值有fill_parentmatch_parentwrap_content
  • android:layout_height
    1. 用于设置布局的高度,其值可以是具体的尺寸,如50dp,也可以是系统定义的值。
    2. 系统定义的值有fill_parentmatch_parentwrap_content
  • android:background:用于设置布局背景。其值可以引用图片资源,也可以是颜色资源。
  • android:layout_margin:用于设置当前布局与屏幕边界、周围布局或控件的距离。属性值为具体的尺寸,如45dp。
  • android:padding:用于设置当前布局内控件与该布局的距离,其值可以是具体的尺寸,如45dp。

2.4 线性布局

LinearLayout(线性布局)通常指定布局内的子控件水平或者竖直排列

在XML布局文件中定义线性布局的基本语法格式如下:

<LinearLayout 
     xmlns:android="http://schemas.android.com/apk/res/android"
     属性 = "属性值"
     ......>
</LinearLayout>

除了布局的通用属性外,LinearLayout布局还有两个比较常用的属性,分别是android:orientationandroid:layout_weight,具体介绍如下所示。

属性名称 功能描述
android:orientation 设置布局内控件的排列顺序
android:layout_weight 在布局内设置控件权重,属性值可直接写int值

属性android:orientation的值为可选值,可选值为vertical和horizontal。

  1. vertical:表示LinearLayout布局内控件依次从上到下竖直排列。
  2. horizontal:表示LinearLayout布局内控件依次从左到右水平排列。

属性android:layout_weight

  1. 该属性被称为权重,通过设置该属性值,可使布局内的控件按照权重比显示大小。
  2. 在进行屏幕适配时起到关键作用。

image-20250312190941649

线性布局示例:

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

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Button1" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:text="Button2" />

    <Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="2"
        android:text="Button3" />
</LinearLayout>

LinearLayout布局中的android:layout_width属性值不可设为wrap_content

这是因为LinearLayout的优先级比Button高,如果设置为wrap_content,则Button控件的android:layout_weight属性会失去作用

<Button
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android: layout_weight ="2"/>

注意:当控件使用权重属性时,布局宽度属性值通常设置为0dp。

2.5 相对布局

RelativeLayout(相对布局)通过相对定位的方式指定子控件的位置。在XML布局文件中定义相对布局时使用<RelativeLayout>标签,基本语法格式如下所示。

<RelativeLayout 
        xmlns:android="http://schemas.android.com/apk/res/android"
        属性 = "属性值"
        ......>
</RelativeLayout>

在RelativeLayout中的子控件具备一些属性,用于指定子控件的位置,这些子控件的属性如下表所示。

属性名称 功能描述
android:layout_centerInParent 设置当前控件位于父布局的中央位置
android:layout_centerVertical 设置当前控件位于父布局的垂直居中位置
android:layout_centerHorizontal 设置当前控件位于父控件的水平居中位置
android:layout_above 设置当前控件位于某控件上方
android:layout_below 设置当前控件位于某控件下方
android:layout_toLeftOf 设置当前控件位于某控件左侧
android:layout_toRightOf 设置当前控件位于某控件右侧
android:layout_alignParentTop 设置当前控件是否与父控件顶端对齐
android:layout_alignParentLeft 设置当前控件是否与父控件左对齐
android:layout_alignParentRight 设置当前控件是否与父控件右对齐
android:layout_alignParentBottom 设置当前控件是否与父控件底端对齐
android:layout_alignTop 设置当前控件的上边界与某控件的上边界对齐
android:layout_alignBottom 设置当前控件的下边界与某控件的下边界对齐
android:layout_alignLeft 设置当前控件的左边界与某控件的左边界对齐
android:layout_alignRight 设置当前控件的右边界与某控件的右边界对齐

注意:在RelativeLayout布局中定义的控件默认与父布局左上角对齐。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- 对齐父元素底部-->
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:text="按钮1"></Button>

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="260dp"
        android:text="按钮2"></Button>

    <Button
        android:id="@+id/btn3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/btn2"
        android:layout_alignBottom="@+id/btn2"
        android:layout_marginBottom="100dp"
        android:text="按钮3"></Button>

</RelativeLayout>

最终效果:

image-20250319133416707

2.6 表格布局

TableLayout(表格布局)采用的形式来管理控件,通过在TableLayout布局中添加TableRow布局或控件来控制表格的行数,可以在TableRow布局中添加控件来控制表格的列数。定义的基本语法格式如下所示。(类似于Qt中的栅格布局)

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
             属性 = "属性值">
    <TableRow>
        UI控件
    </TableRow>
    UI控件
    ......
</TableLayout>

TableLayout继承自LinearLayout,因此它完全支持LinearLayout所支持的属性,此外它还有其他的常用属性,这些常用属性如下表所示。

表格布局属性

属性名称 功能描述
android:stretchColumns 设置该列被拉伸
android:shrinkColumns 设置该列被收缩
android:collapseColumns 设置该列被隐藏

表格布局中控件常用的属性

属性名称 功能描述
android:layout_column 设置该单元显示位置
android:layout_span 设置该单元格占据几列,默认为1列
<?xml version="1.0" encoding="utf-8"?>
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:stretchColumns="2"> // 设置索引为2列可拉伸

    <TableRow>

        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="按钮1"
            android:layout_column="0"></Button>
        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="按钮2"
            android:layout_column="1"></Button>
    </TableRow>

    <TableRow>

        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="按钮3"
            android:layout_column="1"></Button>
        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="按钮4"
            android:layout_column="2"></Button> // 拉伸列2
    </TableRow>


    <TableRow>
        <Button
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="按钮5"
            android:layout_column="2"></Button> // 放在拉伸列
    </TableRow>
</TableLayout>

最终效果

image-20250319135143397

2.7 帧布局

FrameLayout(帧布局)用于在屏幕上创建一块空白区域,添加到该区域中的每个子控件占一帧,这些帧会一个一个叠加在一起,后加入的控件会叠加在上一个控件上层。默认情况下,帧布局中的所有控件会与左上角对齐。在XML布局文件中定义FrameLayout的基本语法格式如下所示。

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
      属性 ="属性值">
</FrameLayout>

帧布局除了前面小节介绍的通用属性外,还有两个特殊属性,FrameLayout的2个特殊属性如下表所示。

属性名称 功能描述
android:foreground 设置帧布局容器的前景图像(始终在所有子控件之上)
android:foregroundGravity 设置前景图像显示的位置

接下来,我们通过一个案例来讲解如何在帧布局中使用属性android:foregroundandroid:foregroundGravity指定控件位置。本案例中使用了帧布局FrameLayout,在帧布局中放置了2个按钮,分别是按钮1和按钮2,按钮2在按钮1的上一层进行显示,帧布局界面的效果如下图所示。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:foreground="@mipmap/ic_launcher"
    android:foregroundGravity="left">

    <Button
        android:layout_width="300dp"
        android:layout_height="450dp"
        android:text="按钮1"
        android:background="@color/purple_200"
        ></Button>

    <Button
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:text="按钮2"
        // android:background="@color/teal_200"
        android:background="#b3b4bc"
></Button>
</FrameLayout>

最终效果:

image-20250319142213136

2.8 约束布局

属性名称 描述
layout_constraintLeft_toLeftOf view1左边对齐view2的左边
layout_constraintLeft_toRightOf view1左边对齐view2的右边
layout_constraintRight_toLeftOf view1右边对齐view2的左边
layout_constraintRight_toRightOf view1右边对齐view2的右边
layout_constraintTop_toTopOf view1顶部对齐view2的顶部
layout_constraintTop_toBottomOf view1顶部对齐view2的底部
layout_constraintBottom_toTopOf view1底部对齐view2的顶部
layout_constraintBottom_toBottomOf view1底部对齐view2的底部
layout_constraintBaseline_toBaselineOf view1基准线对齐view2的基准线
layout_constraintStart_toEndOf view1起始位置对齐view2的结束位置
layout_constraintStart_toStartOf view1起始位置对齐view2的起始位置
layout_constraintEnd_toStartOf view1结束位置对齐view2的起始位置
layout_constraintEnd_toEndOf view1结束位置对齐view2的结束位置
<TextView
          android:id="@+id/text1"
          android:layout_width="100dp"
          android:layout_height="100dp"
          android:gravity="center"
          android:background="@color/blue"
          android:text="hello world"/>
<TextView
          android:id="@+id/text2"
          android:layout_width="100dp"
          android:layout_height="50dp"
          android:text="hello world"
          android:gravity="center"
          android:background="@color/red"
          app:layout_constraintLeft_toRightOf="@id/text1"
          app:layout_constraintBottom_toBottomOf="@id/text1"/>
image-20250319142646837

ConstraintLayout的边距常用属性如下:

  • android:layout_marginStart
  • android:layout_marginEnd
  • android:layout_marginLeft
  • android:layout_marginTop
  • android:layout_marginRight
  • android:layout_marginBottom

看起来跟别的布局没有什么差别,但实际上控件在ConstraintLayout里面要实现margin,必须先约束该控件在ConstraintLayout里的位置

<android.support.constraint.ConstraintLayout 
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/TextView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="10dp"   // 此行
        app:layout_constraintLeft_toLeftOf="parent" // 约束位置
        app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>

居中和偏移

在RelativeLayout中,把控件放在布局中间的方法是把layout_centerInParent设为true,而在ConstraintLayout中的写法是:

<TextView
          app:layout_constraintBottom_toBottomOf="parent"
          app:layout_constraintLeft_toLeftOf="parent"
          app:layout_constraintRight_toRightOf="parent"
          app:layout_constraintTop_toTopOf="parent"
          />

意思是把控件的上下左右约束在布局的上下左右,这样就能把控件放在布局的中间了。

同理RelativeLayout中的水平居中layout_centerHorizontal相当于在ConstraintLayout约束控件的左右为parent的左右;

RelativeLayout中的垂直居中layout_centerVertical相当于在ConstraintLayout约束控件的上下为parent的上下。

宽高比

当宽或高至少有一个尺寸被设置为0dp时,可以通过属性layout_constraintDimensionRatio设置宽高比,

<TextView
          android:id="@+id/TextView1"
          android:layout_width="0dp"
          android:layout_height="wrap_content"
          app:layout_constraintDimensionRatio="1:1"
          app:layout_constraintLeft_toLeftOf="parent"
          app:layout_constraintRight_toRightOf="parent" />

宽设置为0dp,宽高比设置为1:1,这个时候TextView1是一个正方形,

如果两个或以上控件通过下图的方式约束在一起,就可以认为是他们是一条链(图为横向的链,纵向同理)。

image-20250319144303077

可以通过 layout_constraintHorizontal_chainStylelayout_constraintVertical_chainStyle设置链式控件的样式。这个属性有点像 LinearLayout中的 weight 属性平分布局。

属性值:spread, spread_inside, packed

2.9 常用属性

layout_margin:用于设置控件边缘相对于父控件的边距

  • android:layout_marginLeft
  • android:layout_marginRight
  • android:layout_marginTop
  • android:layout_marginBottom

layout_padding:用于设置控件内容相对于控件边缘的边距

  • android:layout_paddingLeft
  • android:layout_paddingRight
  • android:layout_paddingTop
  • android:layout_paddingBottom

layout_width/height:用于设置控件的高度和宽度

  • wrap_content 内容包裹,表示这个控件的里面文字大小填充
  • fill_parent 跟随父窗口
  • match_parent

gravity:用于设置View组件里面内容的对齐方式

  • top
  • bottom
  • left
  • right
  • center等

android:layout_gravity:用于设置容器组件的对齐方式

  • top
  • bottom
  • left
  • right
  • center等

第三章 Andorid常见界面控件

3.1 TextView控件

TextView控件用于显示文本信息,我们可以在XML布局文件中以添加属性的方式来控制TextView控件的样式,TextView控件的常用属性如下表所示。

属性名称 功能描述
android:layout_width 设置控件的宽度
android:layout_height 设置控件的高度
android:id 设置控件的唯一标识
android:background 设置控件的背景
android:layout_margin 设置当前控件与屏幕边界或周围控件、布局的距离
android:padding 设置TextView控件与该控件中内容的距离
android:text 设置文本内容
android:textColor 设置文字显示的颜色
android:textSize 设置文字大小,推荐单位为sp
属性名称 功能描述
android:gravity 设置文本内容的位置
android:maxLength 设置文本最大长度,超出此长度的文本不显示
android:lines 设置文本的行数,超出此行数的文本不显示
android:maxLines 设置文本的最大行数,超出此行数的文本不显示
android:ellipsize 设置当文本超出TextView规定的范围的显示方式
android:drawableTop 在文本的顶部显示图像
android:lineSpacingExtra 设置文本的行间距
android:textStyle 设置文本样式,如bold(粗体),italic(斜体),normal(正常)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView控件显示的文本信息"
        android:textColor="#FFF79E38"
        android:textSize="25sp"
        android:gravity="center"
        android:textStyle="italic">
    </TextView>
</RelativeLayout>

3.2 EditText控件

EditText控件用于显示编辑框,它是TextView的子类,用户可在此控件中输入信息。除了支持TextView控件的属性外,EditText控件还支持一些其他的常用属性,这些常用属性如下表所示。

属性名称 功能描述
android:hint 控件中内容为空时显示的提示文本信息
android:textColorHint 控件中内容为空时显示的提示文本信息的颜色
android:inputType 设置文本类型,可以是textPassword(输入短暂显示变成黑点),phone(唤醒数字键盘)
android:maxLines 设置文本的最大行数
android:scrollHorizontally 设置文本信息超出EditText的宽度情况下,是否出现横拉条
android:editable 设置是否可编辑
<?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="horizontal">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="姓名"
        android:textSize="25sp"
        ></TextView>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="10dp"
        android:orientation="vertical"
        android:hint="请输入姓名"
        android:inputType="textPassword">
    </EditText>
</LinearLayout>

3.3 Button控件

Button控件表示按钮,它继承自TextView控件,既可以显示文本,又可以显示图片,同时也允许用户通过点击来执行操作,当Button控件被点击时,被按下与弹起的背景会有一个动态的切换效果,这个效果就是点击效果。

为按钮控件设定点击事件有三种方式:

  1. 在xml文件中指定,使用android:onClick="自定义处理函数名称(View view)",传递的参数是被点击的View
<Button
        android:id="@+id/btn1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按钮1"
        android:textSize="20sp"
        android:onClick="onClickHandler">
</Button>
package com.example.componentlearn;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    Button button1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.button);
    }

    public void onClickHandler(View view){
        button1 = findViewById(view.getId());
        button1.setText("按钮1被点击了");
    }
}
  1. 通过使用匿名内部类的方式设置Button控件的点击事件
  • 这种方式xml文件不需要指定点击事件
<Button
        android:id="@+id/btn2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="按钮2"
        android:textSize="20sp">
</Button>
  • 需要在java代码中使用setOnClickListener指定
package com.example.componentlearn;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity{
    Button button1;
    Button button2;
    Button button3;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.button);

        button2 = findViewById(R.id.btn2);

        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                button2.setText("按钮2被点击了");
            }
        });
    }
}
  • 通过将Activity实现OnClickListener接口的方式设置Button控件的点击事件
package com.example.componentlearn;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{ // 实现接口
    Button button1;
    Button button2;
    Button button3;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.button);
        
        button3 = findViewById(R.id.btn3);

        button3.setOnClickListener(this::onClick);
    }

    @Override
    public void onClick(View view) { 
        switch(view.getId()){ 	// 可以根据点击的视图的id区分使用不同处理方式
            case R.id.btn3:
                button3.setText("按钮3被点击了");
        }
    }
}

3.4 ImageView控件

ImageView控件表示图片,它继承自View,可以加载各种图片资源。ImageView控件的常用属性如下表所示。

属性名称 功能描述
android:layout_width 设置控件的宽度
android:layout_height 设置控件的高度
android:id 设置控件的唯一标识
android:background 设置控件的背景
android:layout_margin 设置当前控件与屏幕边界或周围控件的距离
android:src 设置控件需要显示的图片资源
android:scaleType 将图片资源缩放或移动,以适应ImageView控件的宽高
android:tint 将图片渲染成指定的颜色

其中scaleType选择不同的属性值有不同效果

image-20250327143157443
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:src="@drawable/bg"
        android:scaleType="fitXY"></ImageView>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/icon"
        ></ImageView>
</RelativeLayout>

3.5 RadioButton控件

RadioButton表示单选按钮,它是Button的子类。每一个单选按钮都有“选中”和“未选中”两种状态,这两种状态是通过android:checked属性指定的。当可选值为true时,表示选中状态,否则,表示未选中状态。

在XML布局文件中,RadioGroup和RadioButton配合使用的例子如下:

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

    <RadioGroup
        android:id="@+id/rbg"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
        <RadioButton
            android:id="@+id/rb1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=""
            android:textSize="25sp"></RadioButton>

        <RadioButton
            android:id="@+id/rb2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text=""
            android:textSize="25sp"></RadioButton>
    </RadioGroup>
    <TextView
        android:id="@+id/textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="25sp"
        android:text="你选择的是: ">
    </TextView>
</LinearLayout>
package com.example.componentlearn;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.RadioGroup;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity{
    RadioGroup rbg;
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.radiobutton);

        rbg = findViewById(R.id.rbg);
        textView = findViewById(R.id.textview);

        rbg.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup radioGroup, int checkedId) {
                // 判断被点击的是哪一个控件
                if (checkedId == R.id.rb1){
                    textView.setText("你选择的是:男");
                }else if (checkedId == R.id.rb2){
                    textView.setText("你选择的是:女");
                }
            }
        });
    }
}

3.6 CheckBox控件

CheckBox表示复选框,它是Button的子类,用于实现多选功能。每一个复选框都有“选中”和“未选中”两种状态,这两种状态是通过android:checked属性指定的,当该属性的值为true时,表示选中状态,否则,表示未选中状态。

给checkBox添加监听器通过实现CompoundButton.OnCheckedChangeListener接口并重写onCheckedChanged(CompoundButton compoundButton, boolean b)方法实现

package com.example.componentlearn;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.RadioGroup;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener {

    TextView hobby;
    String choosenHobby = "";

    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.checkbox);

        hobby = findViewById(R.id.hobby);
        CheckBox music_box = findViewById(R.id.music_hob);
        CheckBox basketball_box = findViewById(R.id.basketball_hob);
        CheckBox playgame_box = findViewById(R.id.playgame_hob);

        music_box.setOnCheckedChangeListener(this);
        basketball_box.setOnCheckedChangeListener(this);
        playgame_box.setOnCheckedChangeListener(this);
    }

    @Override
    public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) {
        String motion = compoundButton.getText().toString().trim(); // 获取对应爱好文字
        if(isChecked) {
            if(!choosenHobby.contains(motion)){
                choosenHobby += motion;
            }
        }else{
            choosenHobby.replace(motion,"");
        }
        hobby.setText(choosenHobby);
    }
}

3.7 Toast类

Toast是Android系统提供的轻量级信息提醒机制,用于向用户提示即时消息,它显示在应用程序界面的最上层,显示一段时间后自动消失不会打断当前操作,也不获得焦点。

基本的使用方法:

Toast.makeText(Context,Text,Time).show();

关于makeText()方法中参数的相关介绍具体如下:

  • Context:表示应用程序环境的信息,即当前组件的上下文环境。
  • Text:表示提示的字符串信息。
  • Time:表示显示信息的时长,其属性值包括Toast.LENGTH_SHORT和Toast.LENGTH_LONG,分别表示显示较短时间和较长时间。

image-20250402141304267

package com.example.toast;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void makeToast(View view) {
        Toast.makeText(this, "提示信息", Toast.LENGTH_LONG).show();
    }

    public void showAlertDialog(View view) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示")
                .setMessage("是否退出程序")
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        dialogInterface.dismiss(); // 隐藏对应的对话框
                        MainActivity.this.finish();
                    }
                }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        dialogInterface.dismiss();
                    }
                }).show();
    }
}

3.8 AlertDialog对话框

在Android开发中,AlertDialog 是一种常用的对话框组件,用于向用户显示重要信息或请求用户进行决策。以下是关于 AlertDialog 的使用笔记,包括基本用法、自定义视图、单选和多选对话框等内容:

1. 基本用法

AlertDialog 通常通过 AlertDialog.Builder 创建,以下是一个简单的示例:

new AlertDialog.Builder(this)
    .setTitle("标题")
    .setMessage("这是一个消息")
    .setPositiveButton("确定", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            // 点击确定后的操作
        }
    })
    .setNegativeButton("取消", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            // 点击取消后的操作
        }
    })
    .show();

上述代码创建了一个简单的对话框,包含标题、消息和两个按钮。

2. 自定义视图

如果需要更复杂的界面,可以通过 setView() 方法加载自定义布局:

LayoutInflater inflater = getLayoutInflater();
View dialogView = inflater.inflate(R.layout.custom_dialog, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(dialogView);
builder.create().show();

在自定义布局中,可以添加 EditTextImageView 等组件,以满足特定需求。

3. 单选和多选对话框

单选对话框

使用 setSingleChoiceItems() 方法创建单选对话框:

String[] items = {"选项1", "选项2", "选项3"};

        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("选择一个选项")
                .setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) { // 处理选项被点击的场景
                        // 更新选中的位置
                        selectedPosition = which;
                        Toast.makeText(MainActivity.this, items[which], Toast.LENGTH_LONG).show();
                    }
                })
                .setPositiveButton("确定", new DialogInterface.OnClickListener() { // 处理点击确定按钮后
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) { // 此处i是按钮的id,不是选中的项
                        // 显示选中的选项
                        Toast.makeText(MainActivity.this, items[selectedPosition], Toast.LENGTH_LONG).show();
                        // 关闭对话框
                        dialogInterface.dismiss();
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        // 关闭对话框
                        dialogInterface.dismiss();
                    }
                });

        // 显示对话框
        AlertDialog dialog = builder.create();
        dialog.show();
多选对话框

使用 setMultiChoiceItems() 方法创建多选对话框:

String[] items = {"选项1", "选项2", "选项3"};
boolean[] checkedItems = {false, false, false};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("多选对话框")
    .setMultiChoiceItems(items, checkedItems, new DialogInterface.OnMultiChoiceClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which, boolean isChecked) {
            checkedItems[which] = isChecked;
        }
    })
    .setPositiveButton("确定", null)
    .show();

4. 警示对话框

警示对话框通常用于提示用户重要信息:

AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(R.drawable.ic_alert)
    .setTitle("警告")
    .setMessage("这是一个警示对话框")
    .setPositiveButton("确定", null)
    .show();

5. 注意事项

  • 阻塞性AlertDialog 是阻塞性的,用户必须与对话框交互后才能继续操作应用。
  • 自定义样式:可以通过 setPositiveButton()setNegativeButton() 等方法设置按钮样式。
  • 避免复杂交互AlertDialog 适合简单交互,复杂场景建议使用自定义对话框。

通过以上方法,开发者可以灵活地使用 AlertDialog 来满足不同的交互需求。

6. 综合使用

package com.example.toast;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import android.content.DialogInterface;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void makeToast(View view) {
        Toast.makeText(this, "提示信息", Toast.LENGTH_LONG).show();
    }

    public void showAlertDialog(View view) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("提示")
                .setMessage("是否退出程序")
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        dialogInterface.dismiss(); // 隐藏对应的对话框
                        MainActivity.this.finish();
                    }
                }).setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        dialogInterface.dismiss(); 
                    }
                }).show();
    }
}

3.9 ListView列表控件

列表控件通常会在一个页面中展示多个条目,并且每个条目的布局风格一致,这种数据的展示方式是通过ListView控件RecyclerView控件实现的。

ListView以列表的形式展现,并且能够根据列表的高度自适应屏幕,通过属性控制列表控件的样式

属性名称 功能描述
android:listSelector 当条目被点击后,改变条目的背景颜色
android:divider 设置分割线的颜色
android:dividerHeight 设置分割线的高度
android:scrollbars 是否显示滚动条
android:fadingEdge 去掉上边和下边的黑色阴影

注意

使用ListView控件时,需要用到两个xml文件,一个用于放置ListView本体,另一个xml文件用于指定每一项的样式

例如以下代码:

/* 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">
    <TextView
        android:layout_width="match_parent"
        android:layout_height="45dp"
        android:background="@android:color/holo_red_light"
        android:gravity="center"
        android:text="超市"
        android:textColor="@android:color/white"
        android:textSize="18sp" />
    <ListView
        android:id="@+id/lv_list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="#e0e0e0"
        android:dividerHeight="1dp"/>
</LinearLayout>
/* list_item.xml 
列表控件的每一项都是这个布局 */ 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    android:background="@android:color/white">
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_centerVertical="true" />
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_marginLeft="10dp"
        android:layout_toRightOf="@+id/iv_img">
        <TextView
            android:id="@+id/tv_goods_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="#000000"
            android:textSize="20sp" />
        <TextView
            android:id="@+id/tv_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_goods_name"
            android:layout_marginTop="8dp"
            android:textColor="@android:color/holo_red_light"
            android:textSize="22sp"
            android:textStyle="bold" />
        <TextView
            android:id="@+id/tv_shop"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/tv_price"
            android:layout_marginTop="8dp"
            android:textColor="#3a3a3a"
            android:textSize="16sp" />
    </RelativeLayout>
</RelativeLayout>
public class MainActivity extends AppCompatActivity {
    private ListView mListView;
    //需要适配的数据
    private String[] names = {"手撕面包", "华夫饼", "小麻花", "每日坚果", "盐焗鸡蛋",
            "原味肉松饼"};
    private String[] prices = {"¥32.90", "¥36.90", "¥18.80", "¥19.90",
            "¥30.70", "¥34.90"};
    private String[] shops = {"良品铺子旗舰店", "百草味旗舰店", "比比赞旗舰店",
            "憨豆熊旗舰店", "无穷旗舰店", "良品铺子旗舰店"};
    //图片集合
    private int[] pictures = {R.drawable.tear_bread, R.drawable.waffle,
            R.drawable.dough_twist, R.drawable.daily_nuts,
            R.drawable.salt_baked_eggs, R.drawable.meat_floss};

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取ListView控件
        mListView = findViewById(R.id.lv_list);
        //创建一个适配器的实例
        MyBaseAdapter mAdapter = new MyBaseAdapter(this);
        //将适配器添加到列表控件上
        mListView.setAdapter(mAdapter);
    }

    class MyBaseAdapter extends BaseAdapter {
        private Context mContext;
        public MyBaseAdapter(Context context) {
            this.mContext = context;
        }

        /**
         * 获取列表条目的总数
         */
        @Override
        public int getCount() {
            return names.length;
        }

        /**
         * 获取列表条目对象
         */
        @Override
        public Object getItem(int position) {
            return names[position];
        }

        /**
         * 获取列表条目Id
         */
        @Override
        public long getItemId(int position) {
            return position;
        }

        /**
         * 获取列表条目的视图
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            // 每次都创建一个新的视图
            convertView = View.inflate(mContext, R.layout.list_item, null);
//                convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
            
            // 获取列表条目界面上的控件
            TextView tvGoodsName = view.findViewById(R.id.tv_goods_name);
            TextView tvPrice = view.findViewById(R.id.tv_price);
            TextView tvShop = view.findViewById(R.id.tv_shop);
            ImageView ivImg = view.findViewById(R.id.iv_img);
            
            // 设置数据
            tvGoodsName.setText(names[position]);
            tvPrice.setText(prices[position]);
            tvShop.setText(shops[position]);
            ivImg.setImageResource(pictures[position]);
            
            return view;
        }
    }
}

这样会不断创建新的视图,但是当数据量过大或者滑动过快的时候,会出现卡顿的情况,这是因为

  • (1)当滑动屏幕时,不断地创建列表条目对象
  • (2)不断执行findViewById()方法初始化控件

性能优化

image-20250410132614144

public class MainActivity extends AppCompatActivity {
    private ListView mListView;
    //需要适配的数据
    private String[] names = {"手撕面包", "华夫饼", "小麻花", "每日坚果", "盐焗鸡蛋",
            "原味肉松饼"};
    private String[] prices = {"¥32.90", "¥36.90", "¥18.80", "¥19.90",
            "¥30.70", "¥34.90"};
    private String[] shops = {"良品铺子旗舰店", "百草味旗舰店", "比比赞旗舰店",
            "憨豆熊旗舰店", "无穷旗舰店", "良品铺子旗舰店"};
    //图片集合
    private int[] pictures = {R.drawable.tear_bread, R.drawable.waffle,
            R.drawable.dough_twist, R.drawable.daily_nuts,
            R.drawable.salt_baked_eggs, R.drawable.meat_floss};

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取ListView控件
        mListView = findViewById(R.id.lv_list);
        //创建一个适配器的实例
        MyBaseAdapter mAdapter = new MyBaseAdapter(this);
        //将适配器添加到列表控件上
        mListView.setAdapter(mAdapter);
    }

    class MyBaseAdapter extends BaseAdapter {
        private Context mContext;
        public MyBaseAdapter(Context context) {
            this.mContext = context;
        }

        /**
         * 获取列表条目的总数
         */
        @Override
        public int getCount() {
            return names.length;
        }

        /**
         * 获取列表条目对象
         */
        @Override
        public Object getItem(int position) {
            return names[position];
        }

        /**
         * 获取列表条目Id
         */
        @Override
        public long getItemId(int position) {
            return position;
        }

        /**
         * 获取列表条目的视图
         */
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null) {
                //加载列表条目界面的布局文件list_item.xml,并生成View对象
                convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
                //获取列表条目界面上的控件
                holder = new ViewHolder();
                holder.tv_goods_name = convertView.findViewById(R.id.tv_goods_name);
                holder.tv_price = convertView.findViewById(R.id.tv_price);
                holder.tv_shop = convertView.findViewById(R.id.tv_shop);
                holder.iv_img = convertView.findViewById(R.id.iv_img);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }
            holder.tv_goods_name.setText(names[position]);
            holder.tv_price.setText(prices[position]);
            holder.tv_shop.setText(shops[position]);
            holder.iv_img.setImageResource(pictures[position]); // 更推荐使用setImageResource
            return convertView;
        }
    }

    class ViewHolder {
        TextView tv_goods_name, tv_price, tv_shop;
        ImageView iv_img;
    }
}
  1. LayoutInflater.from(mContext)
    • LayoutInflater 是 Android 提供的一个工具类,用于将布局文件(如 list_item.xml)转换为 View 对象。
    • from(mContext) 是一种静态方法,用于获取 LayoutInflater 的实例。mContext 是当前的上下文(ActivityContext)。
  2. inflate(R.layout.list_item, parent, false)
    • inflate 方法的作用是将布局文件(list_item.xml)解析为一个 View 对象。
    • 参数解释:
      • R.layout.list_item:布局文件的资源 ID,表示要加载的布局。
      • parent:父视图(ViewGroup),表示这个 View 最终会被添加到哪个容器中。
      • false:表示是否立即附加到父视图中。这里传入 false,因为我们只是创建视图,而不是立即将其添加到父视图。
  3. convertView
    • convertViewListView 提供的一个可复用的视图对象。如果 convertView == null,说明没有可复用的视图,需要通过 inflate 创建一个新的视图。

3.10 生成视图和数据适配器

1. inflate函数

1. View.inflate 方法

用法

View view = View.inflate(Context context, int resource, ViewGroup root);

参数

  • **context**:上下文对象,通常是 ActivityContext
  • **resource**:布局文件的资源 ID,例如 R.layout.your_layout
  • **root**:父视图(ViewGroup),表示这个 View 最终会被添加到哪个容器中。

行为

  • 如果 root 不为 null,生成的 View 会被附加到 root 中。
  • 如果 rootnull,生成的 View 不会被附加到任何父视图中。

适用场景

  • 适用于快速加载布局文件,并且不需要复杂的控制。
  • 如果 root 不为 null,生成的 View 会直接附加到父视图中。

注意事项

  • ListViewRecyclerViewgetView 方法中,不推荐使用 View.inflate,因为 root 参数可能会导致生成的 View 被多次附加到父视图中,从而引发异常。

示例代码

View view = View.inflate(this, R.layout.activity_main, null);
setContentView(view);

2. LayoutInflater.inflate 方法

用法

View view = LayoutInflater.from(context).inflate(int resource, ViewGroup parent, boolean attachToRoot);

参数

  • **context**:上下文对象,通常是 ActivityContext
  • **resource**:布局文件的资源 ID,例如 R.layout.your_layout
  • **parent**:父视图(ViewGroup),表示这个 View 最终会被添加到哪个容器中。
  • **attachToRoot**:布尔值,表示是否将生成的 View 立即附加到 parent 中。

行为

  • 如果 attachToRoottrue,生成的 View 会附加到 parent 中。
  • 如果 attachToRootfalse,生成的 View 不会附加到 parent 中。

适用场景

  • 适用于需要灵活控制生成 View 的附加行为的场景。
  • ListViewRecyclerViewgetView 方法中,推荐使用 LayoutInflater.inflate,并将 attachToRoot 设置为 false,以避免将 View 附加到父视图中。

注意事项

  • ListViewRecyclerViewgetView 方法中,parent 是列表本身,建议将 attachToRoot 设置为 false,以避免性能问题。

示例代码

View view = LayoutInflater.from(this).inflate(R.layout.activity_main, null, false);
setContentView(view);

总结对比

方法 简洁性 灵活性 适用场景
View.inflate 快速加载布局文件,不需要复杂控制的场景。
LayoutInflater.inflate 需要灵活控制 View 是否附加到父视图的场景,特别是在 ListViewRecyclerView 中。

推荐用法

  1. **View.inflate**:

    • 适用于简单的布局加载场景,例如在 Activity 中直接加载布局文件。
    • 示例:
      View view = View.inflate(this, R.layout.activity_main, null);
      setContentView(view);
  2. **LayoutInflater.inflate**:

    • 适用于需要灵活控制 View 是否附加到父视图的场景,特别是在 ListViewRecyclerViewgetView 方法中。
    • 示例:
      View view = LayoutInflater.from(this).inflate(R.layout.list_item, parent, false);

ListView 中的推荐用法

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        // 使用 LayoutInflater.inflate 方法加载布局文件
        convertView = LayoutInflater.from(mContext).inflate(R.layout.list_item, parent, false);
    }
    return convertView;
}

通过合理选择和使用这两种方法,可以确保布局文件的加载既高效又灵活。

2. 数据适配器

在为ListView控件添加数据时会用到数据适配器。数据适配器是数据与视图之间的桥梁,它类似于一个转换器,将复杂的数据转换成用户可以接受的方式进行呈现,将数据转化成视图,根据数据返回视图

常用的数据适配器:

  • BaseAdapter:基本的适配器
  • SimpleAdapter:继承自BaseAdapter
  • ArrayAdapter:也是BaseAdapter的子类

BaseAdapter

BaseAdapter,顾名思义,是基本的适配器。它实际上是一个抽象类,通常在自定义适配器时会继承BaseAdapter,该类拥有四个抽象方法,根据这几个抽象方法对ListView控件进行数据适配。BaseAdapter中的4个抽象方法如下表所示。

方法名称 功能描述
getCount() 获取列表条目的总数
getItem(int position) 根据position(位置)获取某个条目的对象
getItemId(int position) 根据position(位置)获取某个条目的id
getView(int position, View convertView, ViewGroup parent) 获取相应position对应的条目视图,position是当前条目的位置,convertView用于复用旧视图,parent用于加载XML布局

SimpleAdapter

SimpleAdapter继承自BaseAdapter,实现了BaseAdapter的四个抽象方法并进行封装。SimpleAdapter的构造方法的具体信息如下:

public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,int resource, String[] from, int[] to)

SimpleAdapter()构造方法中的五个参数的含义如下:

  • context:表示上下文对象。
  • data:数据集合,data中的每一项对应ListView控件中的条目数据。
  • resource:条目布局的资源id。
  • from:Map集合中的key值。
  • to:条目布局中对应的控件。

ArrayAdapter

ArrayAdapter也是BaseAdapter的子类,用法与SimpleAdapter类似,开发者只需要在构造方法里面传入相应参数即可。ArrayAdapter通常用于适配TextView控件,例如Android系统中的Setting(设置菜单)。ArrayAdapter有多个构造方法,构造方法的具体信息如下所示。

public ArrayAdapter(Context context,int resource)public ArrayAdapter(Context context,int resource, int textViewResourceId)public ArrayAdapter(Context context,int resource,T[] objects)public ArrayAdapter(Context context,int resource,int textViewResourceId,T[] objects);
public ArrayAdapter(Context context,int resource,List<T> objects)public ArrayAdapter(Context context,int resource,int textViewResourceId, List<T> objects)

image-20250410132258604

3.11 RecyclerView

RecyclerView控件ListView控件相似,同样是以列表的形式展示数据,并且数据都是通过适配器加载的。RecyclerView的功能更加强大。

image-20250416213307890

使用RecyclerView的方式和ListView的用法类似,下面使用代码说明:

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/id_recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </androidx.recyclerview.widget.RecyclerView>
</RelativeLayout>

list_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"
    android:padding="16dp"
    android:gravity="center"
    android:orientation="horizontal">
    <ImageView
        android:id="@+id/iv"
        android:layout_width="120dp"
        android:layout_height="90dp"
        android:src="@drawable/siberianhusky"/>
    <RelativeLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp">
        <TextView
            android:id="@+id/name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="20sp"
            android:textColor="#FF8F03"
            android:text="哈士奇"/>
        <TextView
            android:id="@+id/introduce"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="16sp"
            android:layout_marginTop="10dp"
            android:layout_below="@+id/name"
            android:textColor="#FF716C6D"
            android:maxLines="2"
            android:ellipsize="end"
            android:text="西伯利亚雪橇犬,常见别名哈士奇,昵称为二哈。"/>
    </RelativeLayout>
</LinearLayout>

MainActivity.java

package com.example.recyclerviewdemo;

public class MainActivity extends AppCompatActivity {
    private RecyclerView mRecyclerView;
    private HomeAdapter mAdapter;
    private String[] names = {"小猫", "哈士奇", "小黄鸭", "小鹿", "老虎"};
    private int[] icons = {R.drawable.cat, R.drawable.siberianhusky,
            R.drawable.yellowduck, R.drawable.fawn, R.drawable.tiger};
    private String[] introduces = {
            "猫,属于猫科动物,分家猫、野猫,是全世界家庭中较为广泛的宠物。",
            "西伯利亚雪橇犬,常见别名哈士奇,昵称为二哈。",
            "鸭的体型相对较小,颈短,一些属的嘴要大些。腿位于身体后方,因而步态蹒跚。",
            "鹿科是哺乳纲偶蹄目下的一科动物。体型大小不等,为有角的反刍类。",
            "虎,大型猫科动物;毛色浅黄或棕黄色,满有黑色横纹;头圆、耳短,耳背面黑色,中央有一白斑甚显著;四肢健壮有力;尾粗长,具黑色环纹,尾端黑色。"
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRecyclerView = findViewById(R.id.id_recyclerview);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mAdapter = new HomeAdapter();
        mRecyclerView.setAdapter(mAdapter);
    }

    // Adapter类就是创建一个View并初始化后返回
    class HomeAdapter extends RecyclerView.Adapter<HomeAdapter.MyViewHolder> {
        // 该方法将文件内容加载为一个View
        @Override
        public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            MyViewHolder holder = new MyViewHolder(LayoutInflater.from(MainActivity.this).inflate(
                    R.layout.list_item, parent, false));
            return holder;
        }

        // 该方法是对View的各部分初始化
        @Override
        public void onBindViewHolder(MyViewHolder holder, int position) {
            //给每个控件加载数据
            holder.name.setText(names[position]);
            holder.iv.setImageResource(icons[position]);
            holder.introduce.setText(introduces[position]);
        }

        @Override
        public int getItemCount() {
            return names.length;
        }

        class MyViewHolder extends RecyclerView.ViewHolder {
            TextView name;
            ImageView iv;
            TextView introduce;

            public MyViewHolder(View view) {
                super(view);
                name = view.findViewById(R.id.name);
                iv = view.findViewById(R.id.iv);
                introduce = view.findViewById(R.id.introduce);
            }
        }
    }
}

RevyclerView实战之相册

main_activity.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">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_list"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:scaleType="centerCrop"/>
    // adjustViewBounds: 如果希望ImageView 调整其边界以保留其 drawable的纵横比,请将此设置为 true
</LinearLayout>

AlbumAdapter.java

package com.example.album;
public class AlbumAdapter extends RecyclerView.Adapter<AlbumAdapter.ViewHolder> {
    private Context context;
    private int[] photos;

    public AlbumAdapter(Context context, int[] photos) {
        this.context = context;
        this.photos = photos;
    }

    @Override
    public AlbumAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(context).inflate(
                R.layout.list_item, parent, false);
        return new ViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(AlbumAdapter.ViewHolder holder, int position) {
        holder.iv_img.setImageResource(photos[position]);
    }

    @Override
    public int getItemCount() {
        return photos == null ? 0 : photos.length;
    }

    class ViewHolder extends RecyclerView.ViewHolder {
        private ImageView iv_img;

        public ViewHolder(View itemView) {
            super(itemView);
            iv_img = itemView.findViewById(R.id.iv_img);
        }
    }
}

MainActivity.java

package com.example.album;

public class MainActivity extends AppCompatActivity {

    private int[] photos = {R.drawable.photo_1, R.drawable.photo_2,
            R.drawable.photo_3, R.drawable.photo_4, R.drawable.photo_5,
            R.drawable.photo_6, R.drawable.photo_7, R.drawable.photo_8,
            R.drawable.photo_9, R.drawable.photo_10, R.drawable.photo_11,
            R.drawable.photo_12, R.drawable.photo_13};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
    }

    private void init() {
        RecyclerView rv_list = findViewById(R.id.rv_list);
        // StaggeredGridLayoutManager(spanCount, 方向)交错网格布局管理器
        // spanCount: 在对应方向上是几行或列
        rv_list.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL));
        // rv_list.setLayoutManager(new GridLayoutManager(MainActivity.this, 2, GridLayoutManager.VERTICAL, false));
        AlbumAdapter adapter = new AlbumAdapter(MainActivity.this, photos);
        rv_list.setAdapter(adapter);
        SpacesItemDecoration decoration = new SpacesItemDecoration(5);
        rv_list.addItemDecoration(decoration);
    }

    public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
        private int space;

        public SpacesItemDecoration(int space) {
            this.space = space;
        }

        @Override
        public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
            super.getItemOffsets(outRect, view, parent, state);
            outRect.left = space;
            outRect.right = space;
            outRect.bottom = space;
            if (parent.getChildAdapterPosition(view) == 0) {
                outRect.top = space;
            }
        }
    }
}

最终效果如下:

image-20250416221426135

3.12 自定义View

自定义View就是创建一个类继承自View类或者其子类,并重写该类的构造方法

public class Customview extends View{
    public Customview(Context context) {
        super(context);
    }
    public Customview(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}
image-20250416221744171
<com.example.album.MyView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
></com.example.album.MyView>
package com.example.album;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;

public class MyView extends View {
    // 私有Paint对象,复用避免频繁创建
    private Paint paint;

    public MyView(Context context) {
        super(context);
        init();
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    // 初始化方法
    private void init() {
        paint = new Paint();
        paint.setColor(Color.BLUE); // 设置圆的颜色
        paint.setAntiAlias(true);  // 启用抗锯齿
        paint.setStyle(Paint.Style.FILL); // 填充样式
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 计算圆心坐标和半径(确保圆不超出边界)
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;
        float radius = Math.min(centerX, centerY); // 取宽度和高度中较小值

        canvas.drawCircle(centerX, centerY, radius, paint);
    }

    // 可选:确保View有合理的默认大小
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        // 设置默认大小为200px(可根据需求调整)
        int defaultSize = 200;
        int width = resolveSize(defaultSize, widthMeasureSpec);
        int height = resolveSize(defaultSize, heightMeasureSpec);

        // 确保View是正方形
        int size = Math.min(width, height);
        setMeasuredDimension(size, size);
    }
}

第四章 Activity程序活动单元

Android中的四大组件分别是ActivityServiceContentProviderBroadcastReceiver,其中,Activity是一个负责与用户交互的组件,每个Android应用中都会用Activity来显示界面以及处理界面上一些控件的事件。

4.1 Activity的生命周期

Activity的生命周期指的是Activity从创建到销毁的整个过程,这个过程大致可以分为五种状态,分别是启动状态、运行状态、暂停状态、停止状态和销毁状态,关于这五种状态的讲解具体如下。

image-20250416231531199

4.2 生命周期方法

Activity的生命周期包括创建、可见、获取焦点、失去焦点、不可见、重新可见、销毁等环节,针对每个环节Activity都定义了相关的回调方法,Activity中的回调方法具体如下。

  • onCreate():Activity创建时调用,通常做一些初始化设置。
  • onStart():Activity即将可见时调用。
  • onResume():Activity获取焦点时调用。
  • onPause():当前Activity被其他Activity覆盖或屏幕锁屏时调用。
  • onStop():Activity对用户不可见时调用。
  • onRestart():Activity从停止状态到再次启动时调用。
  • onDestroy():Activity销毁时调用。
image-20250416231644255
package com.example.jeann.activitylifecycle;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {


    private Intent intent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i("MainActivity","onCreate");
        intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.i("MainActivity","onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.i("MainActivity","onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.i("MainActivity","onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.i("MainActivity","onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i("MainActivity","onDestroy");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.i("MainActivity","onRestart");
    }
}

4.3 创建Activity

在Android Studio中创建Activity有两种方式

  1. 在Android Studio中首先右击项目中存放Activity的包,然后选择New -> Activity -> Empty Activity选项
  2. 在Android程序中,通过创建Java类继承AppCompatActivity的方式创建Activity。例如,首先选中 cn.itcast.activitylifecycle包,右击选择New->Java class选项,创建一个SecondActivity类,然后将该类继承AppCompatActivity。

需要注意的是,一般推荐使用第一种方式创建Activity,因为通过第一种方式创建Activity,程序会自动创建对应的布局文件,并且在清单文件中自动配置Activity的信息,不需要我们手动配置。

4.4 配置Activity

通过第二种方式创建的Activity需要手动在清单文件中配置创建的Activity。

AndroidManifest.xml文件的<application>标签中配置SecondActivity,示例代码如下:

<activity android:name="cn.itcast.activitylifecycle.SecondActivity" />

如果SecondActivity所在的包与AndroidManifest.xml文件的<manifest>标签中通过package属性指定的包名一致,则android:name属性的值的格式为.Activity名称

<activity
        android:name=".MainActivity"
        android:exported="true">
    	<intent-filter>
        	<action android:name="android.intent.action.MAIN" />
        	<category android:name="android.intent.category.LAUNCHER" />
    	</intent-filter>
</activity>
  • android:exported="true" :这个属性表示是否允许其他应用的组件启动这个活动。当值为 true 时,其他应用可以通过意图(Intent)来启动这个活动;当值为 false 时,只有同一应用内的组件或者具有相同用户 ID 的应用才能启动它。

<intent-filter> 标签

  • 意图过滤器用于告诉安卓系统这个活动能够响应哪些类型的动作和类别。
  • <action android:name="android.intent.action.MAIN" />:这个动作表明当前活动是应用程序的主入口。当用户启动应用时,系统会查找带有 MAIN 动作的活动作为启动目标。
  • <category android:name="android.intent.category.LAUNCHER" /> :这个类别表示当前活动应该在系统的启动器(Launcher)中显示为一个可启动的应用图标。当用户在设备的主屏幕上看到应用的图标并点击它时,系统会启动这个带有 LAUNCHER 类别的活动。

4.5 启动和关闭Acitivity

  • 启动Activity:使用startActivity()方法
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(intent); 
  • 关闭Activity:使用finish()方法

4.6 Intent与IntentFilter

Intent被称为意图,是程序中各组件间进行交互的一种重要方式,它不仅可以指定当前组件要执行的动作,还可以在不同组件之间进行数据传递。根据开启目标组件的方式不同,Intent被分为两种类型,分别为显式Intent隐式Intent

image-20250423153012196 image-20250423153105089 image-20250423153112785

需要注意的是,在使用隐式Intent开启Activity时,系统会默认为Intent配置category,将category的属性name的值设置为android.intent.category.DEFAULT,因此为了被开启的Activity能够接收隐式Intent,必须在AndroidManifest.xml文件中的SecondActivity对应的<intent-filter>标签中,将<category/>标签中的属性android:name的值设置为android.intent.category.DEFAULT

Intent intent = new Intent();
intent.setAction("cn.itcast.START_ACTIVITY"); //  设置action动作,当与清单文件中的 action相匹配时启动目标组件
startActivity(intent); 

Android系统中常用的action常量

Intent对象不仅可以启动程序内的组件,也可以启动Android系统其他程序中的组件,包括系统内置的程序组件,当启动系统内置的程序组件时需要设置对应的权限,当需要启动Android系统内置的程序组件时,可以使用系统提供的action常量,常用的action常量如下所示。

Action 名称 说明
android.intent.action.MAIN Android 程序的入口。
android.intent.action.VIEW 显示指定数据。
android.intent.action.EDIT 编辑指定数据。
android.intent.action.DIAL 显示拨号面板。
android.intent.action.CALL 直接呼叫指定的号码。
android.intent.action.ANSWER 接听来电。
android.intent.action.SEND 向其他程序发送数据,例如彩信或邮件等。
android.intent.action.SENDTO 向他人发送短信。
android.intent.action.SEARCH 执行搜索。
android.intent.action.GET_CONTENT 让用户选择数据,并返回所选数据。

IntentFilter的过滤规则

发送一个隐式Intent后,Android系统会将它与程序中的每一个组件的过滤器进行匹配,匹配属性有actiondatacategory,需要这3个属性都匹配成功才能唤起相应的组件。

  1. action属性匹配规则

action属性用来指定Intent对象的动作,具体示例代码如下:

<intent-filter>
    <action android:name="android.intent.action.EDIT" />
    <action android:name="android.intent.action.VIEW" />
    ......
</intent-filter>

只要Intent携带的action与其中一个<intent-filter>标签中action的声明相同,action属性就匹配成功。

  1. data属性匹配规则

data属性用来指定数据的URI或者数据MIME类型,它的值通常与Intent的action属性有关联,在清单文件中设置data属性的示例代码如下:

<intent-filter>
       <data android:mimeType="video/mpeg" android:scheme="http......" />
       <data android:mimeType="audio/mpeg" android:scheme="http......" />
       ......
</intent-filter>

隐式Intent携带的data数据只要与IntentFilter中的任意一个data声明相同,data属性就匹配成功。

  1. category属性匹配规则

category属性用于为action添加额外信息,一个IntentFilter可以不声明category属性,也可以声明多个category属性,在清单文件中设置category属性的示例代码如下:

<intent-filter>
     <category android:name="android.intent.category.DEFAULT" />
     <category android:name="android.intent.category.BROWSABLE" />
     ......
</intent-filter>

隐式Intent中声明的category必须全部能够与某一个IntentFilter中的category匹配才算匹配成功。

需要注意的是,IntentFilter中罗列的category属性数量必须大于或者等于隐式Intent携带的category属性数量时,category属性才能匹配成功。如果一个隐式Intent没有设置category属性,那么他可以通过任何一个IntentFilter(过滤器)的category匹配。

4.7 Activity之间传递数据

Intent类可以在页面跳转时传递数据,有两种方式,一个是通过Intent类putExtra()方法,另一个是使用Bundle类传递数据

  • Activity之间需要传递不同类型的数据,所以Android系统提供了多个重载的putExtra()方法。
image-20250423233153328
/* 在MainActivity中设置数据 */ 
Intent intent = new Intent(); 
intent.setClass(MainActivity.this,SecondActivity.class); 
intent.putExtra("studentName","王晓明"); 
intent.putExtra("englishScore",98);      
intent.putExtra("isPassed",true);       
startActivity(intent);

/* 在SecondActivity类中接收数据 */ 
Intent intent = getIntent();
String name = intent.getStringExtra("studentName");           
int englishScore = intent.getIntExtra("englishScore",0);     
boolean isPassed = intent.getBooleanExtra("isPassed",true); 
/*  将用户名数据封装到Bundle对象中 */
Intent intent = getIntent();
int data_int = intent.getIntExtra("data_int", 0);
TextView textView = findViewById(R.id.textview1);
textView.setText("传来的数据是" + data_int);
/*  通过Bundle对象获取用户名信息 */
Bundle bundle = getIntent().getExtras();         
String account = bundle.getString("account");   

4.8 Activity的数据回传

当我们从MainActivity界面跳转到SecondActivity界面时,在SecondActivity界面上进行一些操作,当关闭SecondActivity界面时,想要从该界面返回一些数据到MainActivity界面

此时,Android系统为我们提供了一些方法用于Activity之间数据的回传。当Android版本不同时,Activity之间进行数据回传时调用的方法也是不同的,接下来根据Android系统的不同版本,介绍Activity之间数据的回传。

4.8.1 Android10之前的版本

image-20250430141804238

Activity回传数据的过程中,涉及到三个方法,分别是startActivityForResult()方法、setResult()方法和onActivityResult()方法。

  • startActivityForResult()方法:开启一个Activity,当开启的Activity销毁时,会从销毁的Activity中返回数据‘
// intent表示要跳转的意图对象,requestCode表示请求码,用于标识请求的来源
startActivityForResult(Intent intent, int requestCode)
  • setResult()方法:用于携带数据进行回传
// 表示返回码,用于标识返回的数据来自哪一个Activity
setResult(int resultCode, Intent intent)
  • onActivityResult()方法:用于接收回传的数据
// 请求码, 返回码, 回传的数据
onActivityResult(int requestCode, int resultCode, Intent data)

程序会根据传递的参数requestCode与resultCode来识别数据的来源。

接下来通过一个例子来说明数据之间的传递:

package com.example.capture4;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main)

        Button btn_huichuan = findViewById(R.id.btn_huichuan); // 测试回传案例的按钮
        btn_huichuan.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 跳转界面
                Intent intent = new Intent(MainActivity.this, ThirdActivity.class);
                startActivityForResult(intent, 1);
            }
        });
    }

    // 重写onActivityResult方法,自定义处理逻辑
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(requestCode == 1 && resultCode == 2){ // 都传回正确的编号
            String dataStr = data.getStringExtra("data");
            Toast.makeText(MainActivity.this, dataStr, Toast.LENGTH_LONG).show();
        }
    }
}

SecondActivity被销毁之后在MainActivity中回调onActivityResult()方法,接收回传的数据

public class ThirdActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);

        Intent intent = getIntent();
        int data_int = intent.getIntExtra("data_int", 0);
        TextView textView = findViewById(R.id.textview1);
        textView.setText("传来的数据是" + data_int);

        Button btn_fanhui = findViewById(R.id.btn_fanhui);
        btn_fanhui.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 在ThirdActivity中设置数据
                Intent backIntent = new Intent();
                backIntent.putExtra("data", "我是回传的数据");
                setResult(2, backIntent);
                finish(); // 销毁自身
            }
        });
    }
}

4.8.2 Android10之后的版本

使用registerForActivityResult()方法替代了startActivityForResult()方法,简化了数据回传的编写方式。还使用了launch()方法与setResult()方法,其中setResult()方法用于携带数据进行回传。

registerForActivityResult()方法:用于接收回传的数据,并获取ActivityResultLauncher类的对象,其作用是注册一个活动结果契约(ActivityResultContract)和对应的回调(ActivityResultCallback),这样当通过该契约启动的活动结束时,就会调用相应的回调函数来处理结果。

  • ActivityResultContract<I, O> contract
    • ActivityResultContract 是一个抽象类,它定义了启动活动时所需的输入类型(I)和活动结束后返回的输出类型(O)。
    • 系统提供了一些预定义的契约,像 ActivityResultContracts.StartActivityForResult 用于启动一个活动并获取其结果,ActivityResultContracts.TakePicture 用于拍摄照片等。你也能自定义契约。
  • ActivityResultCallback<O> callback
    • ActivityResultCallback 是一个函数式接口,其中只有一个 onActivityResult 方法。当通过该契约启动的活动结束时,此方法会被调用,传入活动返回的结果(类型为 O)。
/*  函数原型  */ 
registerForActivityResult(ActivityResultContract<I, O> contract,ActivityResultCallback<O> callback)

public class MainActivity extends AppCompatActivity {

    private ActivityResultLauncher<Intent> activityResultLauncher;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 注册活动结果回调
        activityResultLauncher = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            new ActivityResultCallback<ActivityResult>() {
                @Override
                public void onActivityResult(ActivityResult result) {
                    if (result.getResultCode() == RESULT_OK) {
                        Intent data = result.getData();
                        // 处理返回的数据
                        Toast.makeText(MainActivity.this, "Activity returned OK", Toast.LENGTH_SHORT).show();
                    }
                }
            });

        // 启动活动
        Intent intent = new Intent(this, SecondActivity.class);
        activityResultLauncher.launch(intent);
    }
}
  • launch()方法:launch方法用于启动Activity
/*  MainActivity  */ 
package com.example.capture4;

public class MainActivity extends AppCompatActivity {
    private ActivityResultLauncher launcher;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Android10版本以后的新办法
        launcher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
                Intent data = result.getData();
                String data_v10Str = data.getStringExtra("data_v10");
                Toast.makeText(MainActivity.this, data_v10Str, Toast.LENGTH_LONG).show();
            }
        });
        Button btn_huichuanNew = findViewById(R.id.btn_huichuanNew);
        btn_huichuanNew.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this,SecondActivity.class);
                launcher.launch(intent);
            }
        });
    }
}
package com.example.capture4;

public class SecondActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        Button btn_fanhui2 = findViewById(R.id.btn_fanhui2);
        btn_fanhui2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent backIntent = new Intent();
                backIntent.putExtra("data_v10", "AndroidV10后的回传办法");
                setResult(2, backIntent);
                finish(); // 销毁自身
            }
        });
    }
}

4.9 Activity的任务栈和启动模式

任务栈:一种用来存放Activity实例的容器

特点:"先进后出"

操作:压栈和出栈

image-20250430152410037

Activity启动模式有四种,分别是standardsingleTopsingleTasksingleInstance模式。

  1. standard模式

standard模式是Activity的默认启动方式,每启动一个Activity就会在栈顶创建一个新的实例

image-20250430152455934
  1. singleTop模式

singleTop模式会判断要启动的Activity实例是否位于栈顶,如果位于栈顶则直接复用,否则创建新的实例。

image-20250430152544010
  1. singleTask模式

singleTask模式下每次启动该Activity时,系统首先会检查栈中是否存在当前Activity实例,如果存在则直接使用,并把当前Activity之上的所有实例全部出栈

image-20250430152631667

  1. singleInstance模式

singleInstance模式会启动一个新的任务栈来管理Activity实例,无论从哪个任务栈中启动该Activity,该实例在整个系统中只有一个

image-20250430152722004