第1章 Android Studio运行第一个程序

1.1 Android Studio下载:

1.1.1 Android开发者官网:

        https://developer.android.google.cn

1.1.2 下载Android Studio开发者工具:

  1. 进入Android开发者官网;
  2. 找到Android Studio工具下载页面;
  3. 点击“Download option”按钮选择与自己系统对应的版本(本人使用的是Windows版本);
  4. 同意“条款和条件”并点击下载;

1.2 Android Studio安装:

  1. 双击下载好的Android Studio工具;
  2. 直接点击“Next”到安装处点击“Install”安装;(中途有:欢迎安装界面-->选择上虚拟设备-->安装路径)
  3. 等待安装完成后点“Next”如下图所示表示安装完成;                                                               
  4. 运行Android Studio;(由于是第一次安装,没有配置文件,直接点击“OK”)
  5. 点击“Cancel”;                                                                                                                                                  
  6. 会弹出欢迎界面,提示会提醒是否将使用情况统计信息发送到“Google”;(单纯让谷歌知道多了一位Android开发者)
  7. 来到欢迎配置Android Studio开发环境,点击”Next“直接到License Agreement[许可协议];(【Install Type:安装类型】[Standard:标准]-->【Select Ul Theme:选择UI主题】-->【Verify Settings:验证配置】)
  8. 阅读并同意安装组件的许可协议,点击“Finish”;([License Agreement:许可协议]、[Accept:接受])
  9. 下载完成后,点击“Finish”;

1.3 Android Studio卸载:

  1. 卸载Android Studio软件;(默认安装路径:C:\Program Files\Android\Android Studio)
  2. 删除SDK文件夹;(默认路径:C:\用户\Administrator[用户名]\AppData\Local\Android\sdk)
  3. 删除相关文件夹;(默认路径:C:\用户\Administrator[用户名]\)
    删除.android 文件夹
    删除 .AndroidStudioX.X 文件夹
    删除.gradle 文件夹

具体详见:https://blog.csdn.net/weixin_45048331/article/details/111868109

1.4 第一个Android应用程序:

1.4.1 创建Android工程项目:

  1. 将Android Studio工具的快捷启动放到桌面;
  2. 点击“New Project”;
  3. 选择Phone and Table-->选择“Empty Activity”-->点击“Next”;
  4. 【Name:app的名字】-->【Package name:包名】-->【Save location:保存路径】-->【Language:语言】-->【点击“Finish”】;
  5. 然后等待下载完成Gradle配置工具;(某些用户可能会弹出防火墙的“安全中心警告”,点击“允许访问”)
  6. 打开“activity_main.xml”文件,视图效果和模拟器上运行的效果差不多

1.4.2 运行第一个Android应用程序:

  • 添加手机模拟器
  1. 点击下图右边第4个像手机一样的图标;
  2. 点击“Create virtual device”添加手机模拟器;
  3. 选上自己喜欢的设备,然后点击“Next”;(可点击“New Hardware Profile”自定义模拟设备)
  4. 点击“Download”下载模拟设备的系统镜像;
  5. 选择已下载的模拟设备系统镜像,点击“Next”;
  6. 给模拟设备命名,点击“”Finish;
  7. 点击“播放按钮”给模拟设备开机;                                                                                      
  8. 手机仿真器视图显示设置;                                                                                                             
  9. 点击运行按钮后的运行结果;                                                                                                     
  10. 编码方式与视图设计方式切换在编辑框的右上角,仿真器隐藏后在编辑框的右边栏最下方(【Code、Split、Design】、【Emulator:仿真器】);

1.4.3 仿真程序进程终止问题

1.5 Android工程结构介绍

修改工程结构显示方式,将Android修改成Project[以目录结构显示];为不影响显示效果此部分内容请使用PC端查看

├── .gradle                                                                                # AS自动生成的文件,AS会自动修改它的,项目打包时也会删除;
├── .idea                                                                                   # AS自动生成的文件,AS会自动修改它的,项目打包时也会删除;
├── app                                                                                     # 应用相关的东西都在里面,工作的核心目录 
│        ├── build                                                                         # 编译的产物。某些情况下,可以手动把它整个删掉。
│        ├── libs                                                                           # 依赖包可以放这里,比如一些jar文件。
│        ├── src                                                                            # 代码在这。非常重要。
│        │        ├── main
│        │        │        ├── java                                                      # 放Java代码的地方
│        │        │        ├── res 
│        │        │        │        ├── drawable                                    # 应用图标
│        │        │        │        ├── layout                                         # Android布局文件夹
│        │        │        │        ├── mipmap                                      # 桌面图标
│        │        │        │        ├── layout                                         # Android布局文件夹
│        │        │        │        └── values                                        # 颜色、样式、字符集配置文件夹
│        ├── .gitignore                                                                   # 版本控制
│        ├── build.gradle                                                               # 非常重要,app的构建配置。俗称“app的gradle文件”。
│        └── proguard-rules.pro                                                    # 先不管。这个是混淆配置。
├── gradle                                                                                  # 它是一个构建起配置文件
├── .gitignore                                                                             # 整个工程的版本控制
├── build.gradle                                                                         # 很重要。项目级的配置。俗称“项目gradle文件”。
├── gradle.properties                                                                 # 全局的gradle配置文件
├── gradlew                                                                                # Linux/mac上执行gradle命令
├── gradlew.bat                                                                          # Windows上执行gradle命令
├── local.properties                                                                    # 本地配置文件,一般不上传
└── settings.gradle                                                                     # gralde的项目级配置

第2章 Android的UI控件

注:控件的属性值可以【Ctrl+鼠标左键】进行查看

2.1 TextView(文本框)

本节前言:

dp(dip): device independent pixels(设备独立像素). 不同设备有不同的显示效果,这个和设备硬件有关,一般我们为了支持WVGA、HVGA和QVGA 推荐使用这个,不依赖像素。 

px: pixels(像素). 不同设备显示效果相同,一般我们HVGA代表320x480像素,这个用的比较多。 

pt: point,是一个标准的长度单位,1pt=1/72英寸,用于印刷业,非常简单易用; 

sp: scaled pixels(放大像素). 主要用于字体显示best for textsize。

2.1.1 基础属性详解: 

  • id:为TextView设置一个组件id,根据id,我们可以在Java代码中通过findViewById(R.id.tv_one)的方法获取到该对象,然后进行相关属性的设置,又或者使用RelativeLayout时,参考组件用的也是id!
  • layout_width:组件的宽度,一般写:**wrap_content**或者**match_parent(fill_parent)**,前者是控件显示的内容多大,控件就多大,而后者会填满该控件所在的父容器;当然也可以设置成特定的大小,比如我这里为了显示效果,设置成了200dp。
  • layout_height:组件的高度,内容同上。
  • gravity:设置控件中内容的对齐方向,TextView中是文字,ImageView中是图片等等。
  • text:设置显示的文本内容,一般我们是把字符串写到src/main/res/values/string.xml文件中,然后通过@String/xxx取得对应的字符串内容的,这里为了方便我直接就写到""里,不建议这样写!!!
  • textColor:设置字体颜色,同上,通过src/main/res/values/colors.xml资源来引用,然后通过@String/xxx进行引用,别直接这样写!
  • textStyle:设置字体风格,三个可选值:**normal**(无效果),**bold**(加粗),**italic**(斜体)
  • textSize:字体大小,单位一般是用sp!
  • background:控件的背景颜色,可以理解为填充整个控件的颜色,可以是图片哦!

XML代码:

    <TextView
        android:id="@+id/tv_one"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:gravity="center"
        android:text="Android学习"
        android:textColor="#FF000000"
        android:textSize="30sp"
        android:textStyle="italic"
        android:background="#FFFF0000" />

2.1.2 带阴影的TextView

  • android:shadowColor:设置阴影颜色,需要与shadowRadius一起使用哦!
  • android:shadowRadius:设置阴影的模糊程度,设为0.1就变成字体颜色了,建议使用3.0
  • android:shadowDx:设置阴影在水平方向的偏移,就是水平方向阴影开始的横坐标位置
  • android:shadowDy:设置阴影在竖直方向的偏移,就是竖直方向阴影开始的纵坐标位置

 XML代码:

    <TextView
        android:id="@+id/tv_one"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:gravity="center"

        android:shadowColor="@color/red"
        android:shadowRadius="3.0"
        android:shadowDx="10.0"
        android:shadowDy="10.0"

        android:text="@string/tv_one"
        android:textColor="#FF000000"
        android:textSize="30sp"
        android:textStyle="italic" />

2.1.3 实际开发例子——跑马灯效果

  • android:singleLine:内容单行显示
  • android:focusable:是否可以获取焦点
  • android:focusablelnTouchMode:用于控制视图在触摸模式下是否可以聚焦
  • android:ellipsize:在哪里省略文本
  • android:marqueeRepeatLimit:字幕动画重复的次数

更多详见:2.3.1 TextView(文本框)详解 | 菜鸟教程 (runoob.com)

XML代码: 

    <TextView
        android:id="@+id/tv_one"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:gravity="center"
        android:shadowColor="@color/red"
        android:shadowRadius="3.0"
        android:shadowDx="10.0"
        android:shadowDy="10.0"

        android:singleLine="true"
        android:ellipsize="marquee"
        android:marqueeRepeatLimit="marquee_forever"
        android:focusable="true"
        android:focusableInTouchMode="true"

        android:text="@string/tv_one"
        android:textColor="#FF000000"
        android:textSize="30sp"
        android:textStyle="italic" />

默认情况下没有效果:

  1. 开启可点击:android:clickable="true"
  2. 添加一个请求标签:
  3. 写一个类继承TextView实现其方法,并重写isFocused()方法返回true,然后将TextView标签给出自定义的全类名

2.2 Button(按钮)

本节前言:

Button是TextView的子类,所以TextView上很多属性也可以应用到Button 上!我们实际开发中对于Button的,无非是对按钮的几个状态做相应的操作,比如:按钮按下的时候 用一种颜色,弹起又一种颜色,或者按钮不可用的时候一种颜色这样!上述实现无非是通过 StateListDrawable这种Drawable资源来实现,即编写一个drawable的资源文件。

2.2.1 StateListDrawable介绍:

StateListDrawable是Drawable资源的一种,可以根据不同的状态,设置不同的图片效果,关键节点 < selector >,我们只需要将Button的background属性设置为该drawable资源即可轻松实现,按下 按钮时不同的按钮颜色或背景!

  • drawable:引用的Drawable位图,我们可以把他放到最前面,就表示组件的正常状态~
  • state_focused:是否获得焦点
  • state_window_focused:是否获得窗口焦点
  • state_enabled:控件是否可用
  • state_checkable:控件可否被勾选,eg:checkbox
  • state_checked:控件是否被勾选
  • state_selected:控件是否被选择,针对有滚轮的情况
  • state_pressed:控件是否被按下
  • state_active:控件是否处于活动状态,eg:slidingTab
  • state_single:控件包含多个子控件时,确定是否只显示一个子控件
  • state_first:控件包含多个子控件时,确定第一个子控件是否处于显示状态
  • state_middle:控件包含多个子控件时,确定中间一个子控件是否处于显示状态
  • state_last:控件包含多个子控件时,确定最后一个子控件是否处于显示状态

更多详见:3.3.3 Button(按钮)与ImageButton(图像按钮) | 菜鸟教程 (runoob.com)

  1.    在src/main/res/drawable中添加位矢图和new按钮选择器【btn_selecter.xml】
  2. 默认情况下新版本的AS设置背景颜色是无效果的,需要修改themes.xml文件
  3. 在src/main/res中new按钮的颜色选择器【color/btn_color_selecter.xml】

XML代码: 

    <Button
        android:text="点我有惊喜"
        android:textColor="#FFFF0000"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="@drawable/btn_selecter"
        android:backgroundTint="@color/btn_color_selecter"
        android:layout_gravity="center" />

 运行效果图: 

 

2.2.2 常见的Button事件:

  • 单击事件:setOnClickListener()
  • 长按事件:setOnLongClickListener()
  • 触摸/移动事件:setOnTouchListener()

代码中TAG的值为“leo”

运行结果1(长按+移动):setOnLongClickListener()与setOnTouchListener()的返回值为false

 运行结果2(长按+移动):setOnLongClickListener()返回值为false,setOnTouchListener()的返回值为true,会将setOnLongClickListener()取代(即触摸/移动也会触发)

 运行结果3(长按+移动):setOnLongClickListener()返回值为true,setOnTouchListener()的返回值为false,setOnClickListener()不会执行

onClickListener()的另一种写法:在activity_main.xml的Button标签中写android:onClick="leoClick",再按住Alt+Enter创建leoClick方法,优先级低于setOnClickListener()

2.3 EditText(输入框)

2.3.1 主要属性

  • android:hint输入提示
  • android:textColorHint输入提示文字的颜色
  • android:inputType输入类型
  • android:drawableXxoxx在输入框的指定方位添加图片
  • android:drawablePadding设置图片与输入内容的间距
  • android:paddingXxxx设置内容与边框的间距
  • android:background背景色

更多详见:2.3.2 EditText(输入框)详解 | 菜鸟教程 (runoob.com)

 XML代码:

    <EditText
        android:id="@+id/username"
        android:hint="请输入用户名"
        android:textColorHint="#FF95A1AA"
        android:inputType="text"
        android:background="@drawable/edit_background"
        android:layout_margin="10dp"
        android:drawableLeft="@drawable/ic_baseline_person_24"
        android:drawablePadding="5dp"
        android:padding="5dp"
        android:layout_width="300dp"
        android:layout_height="50dp" />

    <EditText
        android:id="@+id/password"
        android:hint="请输入密码"
        android:textColorHint="#FF95A1AA"
        android:inputType="textPassword"
        android:background="@drawable/edit_background"
        android:layout_margin="10dp"
        android:drawableLeft="@drawable/ic_baseline_lock_24"
        android:drawablePadding="5dp"
        android:padding="5dp"
        android:layout_width="300dp"
        android:layout_height="50dp" />

    <EditText
        android:id="@+id/phone"
        android:hint="电话"
        android:textColorHint="#FF95A1AA"
        android:inputType="phone"
        android:background="@drawable/edit_background"
        android:layout_margin="10dp"
        android:drawableLeft="@drawable/ic_baseline_phone_24"
        android:drawablePadding="5dp"
        android:padding="5dp"
        android:layout_width="300dp"
        android:layout_height="50dp" />

    <Button
        android:id="@+id/submit_btn"
        android:text="提交"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

 运行效果图:

2.3.2 实际开发例子——用户登录

运行效果图:edit_background.xml是文本框

2.4 ImageView(图像视图)

2.4.1 主要属性

  • android:src设置图片资源
  • android:scaleType设置图片缩放类型
  • android:maxHeight最大高度
  • android:maxWidth最大宽度
  • android:adjustViewBounds是否调整View的界限

 XML代码:

    <ImageView
        android:src="@drawable/ceshi"
        android:layout_width="300dp"
        android:layout_height="300dp" />

    <ImageView
        android:src="@drawable/ceshi"
        android:maxHeight="300dp"
        android:maxWidth="300dp"
        android:adjustViewBounds="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

运行效果图:

2.4.2 scaleType的属性

  • fitStart:保持宽高比缩放图片,直到较长的边与lmage的边长相等缩放完成后将图片放在ImageView的左上角
  • fitCenter:默认值,同上,缩放后放于中间
  • fitEnd:同上,缩放后放于右下角
  • fitXY:对图像的横纵方向进行独立缩放,使得该图片完全适应lmageView,但是图片的宽高比可能会发生改变
  • center:保持原图的大小,显示在ImageView的中心。 当原图的size大于ImageView的size,超过部分裁剪处理。
  • centerCrop:保持宽高比缩放图片,直到完全覆盖ImageView,可能会出现图片的显示不完全
  • centerInside:保持宽高比缩放图片,直到ImageView能够完全地显示图片
  • matrix:不改变原图的大小,从ImageView的左上角开始绘制原图,原图超过ImageView的部分作裁剪处理

更多详见:2.3.4 ImageView(图像视图) | 菜鸟教程 (runoob.com)

2.5 ProgressBar(进度条)

 2.5.1 常用属性详解

  • android:max:进度条的最大值
  • android:progress:进度条已完成进度值
  • android:indeterminate:如果设置成true,则进度条不精确显示进度
  • style="?android:attr/progressBarStyleHorizontal"水平进度条

更多详见:ProgressBar(进度条) | 菜鸟教程 (runoob.com)

 XML代码:

    <ProgressBar
        android:id="@+id/pb1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:text="显示/隐藏进度条"
        android:onClick="leoClick"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <!--水平进度条-->
    <ProgressBar
        android:id="@+id/pb2"
        style="?android:attr/progressBarStyleHorizontal"
        android:max="100"
        android:layout_width="350dp"
        android:layout_height="wrap_content" />
    <Button
        android:text="模拟下载按钮"
        android:onClick="loadClick"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <ProgressBar
        style="?android:attr/progressBarStyleHorizontal"
        android:max="100"
        android:indeterminate="true"
        android:layout_width="350dp"
        android:layout_height="wrap_content" />

 Java代码:

public class MainActivity extends AppCompatActivity {
    private ProgressBar pb1;
    private ProgressBar pb2;
    private Integer speed;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        pb1 = findViewById(R.id.pb1);
        pb2 = findViewById(R.id.pb2);
        speed = (int)(pb2.getMax() * 0.1);
    }

    public void leoClick(View view) {
        if (pb1.getVisibility() == View.GONE) {
            pb1.setVisibility(View.VISIBLE); //显示
        } else {
            pb1.setVisibility(View.GONE);
        }
    }

    public void loadClick(View view) {
        int progress = pb2.getProgress();
        pb2.setProgress(progress + speed);
    }
}

运行效果图:

2.6 Notification(状态栏通知)

2.6.1 Notification的基本使用流程

状态通知栏主要涉及到2个类:Notification 和NotificationManager

Notification:通知信息类,它里面对应了通知栏的各个属性

NotificationManager:是状态栏通知的管理类,负责发通知、清除通知等操作。

使用的基本流程:

  • Step 1. 获得NotificationManager对象: NotificationManager nManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
  • Step 2. 创建一个通知栏的Builder构造类: Notification.Builder nBuilder = new Notification.Builder(this);
    在老的版本中是使用Notification()
    新的版本是使用Notification.Builder()
    为了兼容性现在使用NotificationCompat.Builder()
  • Step 3. 对Builder进行相关的设置,比如标题,内容,图标,动作等!
  • Step 4. 调用Builder的build()方法为notification赋值
  • Step 5. 调用NotificationManager的notify()方法发送通知!
  • PS:另外我们还可以调用NotificationManager的cancel()方法取消通知

2.6.2 NotificationChannel(通知渠道)

通知渠道,Android 8.0引入了通知渠道,其允许您为要显示的每种通知类型创建用户可自定义的渠道。
通知重要程度设置,NotificationManager类中

  • IMPORTANCE_NONE:关闭通知
  • IMPORTANCE_MIN:开启通知,不会弹出,但没有提示音,状态栏中无显示
  • IMPORTANCE_LOW:开启通知,不会弹出,不发出提示音,状态栏中显示
  • IMPORTANCE_DEFAULT(默认):开启通知,不会弹出,发出提示音,状态栏中显示
  • IMPORTANCE_HIGH:开启通知,会弹出,发出提示音,状态栏中显示

2.6.3 Notification的常见方法

  • setContentTitle(String string):设置标题
  • setContentText(String string):设置文本内容
  • setSmalllcon(int icon):设置小图标
  • setLargelcon(Bitmap icon):设置通知的大图标
  • setColor(int argb):设置小图标的颜色
  • setContentlntent(Pendinglntent intent):设置点击通知后的跳转意图
  • setAutoCancel(boolean boolean):设置点击通知后自动清除通知
  • setWhen(long when):设置通知被创建的时间

注意:Android从5.0系统开始,对于通知栏图标的设计进行了修改。现在Google要求,所有应用程序的通知栏图标,应该只使用alpha图层来进行绘制,而不应该包括RGB图层。

2.6.3 实际例子——模拟通知

 XML代码:

Java代码:

运行效果:

更多详见:2.5.8 Notification(状态栏通知)详解 | 菜鸟教程 (runoob.com)

2.7 Toolbar(工具栏目)

2.7.1 常用属性详解

取消系统默认的标题栏目:src/main/res/values/themes.xml --> style标签 --> parent="Theme.MaterialComponents.DayNight.NoActionBar"

  • android:layout_width="match_parent"
  • android:layout_height=""?attr/actionBarSize":使用的是ActionBar的高度
  • android:background="#FFFF00"
  • app:navigationlcon=" @drawable/ic_baseline_arrow_back_24""
  • app:title="主标题"
  • app:titleTextColor="#FF0000"
  • app:titleMarginStart="90dp":左间距
  • app:subtitle="子标题"
  • app:subtitleTextColor="#00FFFFF"
  • app:logo="@mipmap/ic_launcher"

 使用ToolBar替换系统默认的DarkActionBar:

XML代码:

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

    <androidx.appcompat.widget.Toolbar
        android:id="@+id/tb"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
        app:title="标题"
        app:titleTextColor="@color/red"
        app:titleMarginStart="90dp"
        android:background="@color/teal_200"/>

</LinearLayout>

Java代码: 

public class MainActivity extends AppCompatActivity {

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

        Toolbar toolbar = findViewById(R.id.tb); //androidx.appcompat.widget.Toolbar
        toolbar.setNavigationOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e("leo","ToolBar被点击了!");
            }
        });
    }
}

运行效果图: 

2.7.2 标题居中显示

代码及运行效果:

2.8 AlertDialog(对话框)

2.8.1 实现方式/使用流程

  • AlertDialog.Builder builder = new AlertDialog.Builder(context); 构建Dialog的各种参数
  • Builder.setlcon(int iconld); 添加ICON
  • Builder.setTitle(CharSequence title); 添加标题
  • Builder.setMessage(CharSequence message);添加消息
  • Builder.setView(View view); 设置自定义布局
  • setPositiveButton:确定按钮
  • setNegativeButton:取消按钮
  • setNeutralButton:中间按钮
  • Builder.create(); 创建Dialog
  • Builder.show(); 显示对话框

使用流程:

  • Step 1:创建AlertDialog.Builder对象;
  • Step 2:调用setIcon()设置图标,setTitle()setCustomTitle()设置标题;
  • Step 3:设置对话框的内容:setMessage()还有其他方法来指定显示的内容;
  • Step 4:调用setPositive/Negative/NeutralButton()设置:确定,取消,中立按钮;
  • Step 5:调用create()方法创建这个对象,再调用show()方法将对话框显示出来;

更多详见:2.5.9 AlertDialog(对话框)详解 | 菜鸟教程 (runoob.com)

XML代码:

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

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="leoClick"
        android:text="显示对话框"/>

</LinearLayout>

Java代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "leo";

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

    public void leoClick(View view) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this); //androidx.appcompat.app.AlertDialog;
        builder.setIcon(R.mipmap.ic_launcher)
                .setTitle("对话框")
                .setMessage("今天天气怎么样?")
                .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.e(TAG, "点击了确定按钮");
                    }
                })
                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.e(TAG, "点击了取消按钮");
                    }
                })
                .setNeutralButton("中间/其他", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        Log.e(TAG, "点击了中间/其他按钮");
                    }
                })
                .create()
                .show(); //最后两个位置不能交换
    }
}

运行效果图1(无setView()):

 dialog_view.xml代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:background="#66FFFF00"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="今天天气很好!"
        android:textSize="18sp"/>

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/ic_baseline_tag_faces_24"
        app:tint="#00FF00" />
</LinearLayout>

 运行效果2:

2.9 PopupWindow(悬浮框)

2.9.1 常用方法

  • setContentView(View contentView):设置PopupWindow显示的view
  • showAsDropDown(View anchor):相对某个控件的位置〈正左下方),无偏移showAsDropDown(View anchor, int xoff, int yoff):相对某个控件的位置,有偏移
  • setFocusable(boolean focusable):设置是否获取焦点
  • setBackgroundDrawable(Drawable background):设置背景dismiss()关闭弹窗
  • setAnimationStyle(int animationStyle):设置加载动画
  • setTouchable(boolean touchable):设置触摸使能
  • setOutsideTouchable(boolean touchable):设置PopupWindow外面的触摸使能

更多详见:2.6.1 PopupWindow(悬浮框)的基本使用 | 菜鸟教程 (runoob.com)

XML代码:

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

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="弹出PopupWIndow"
        android:onClick="leoClick"/>

</LinearLayout>

popup_view.xml代码: 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:background="@mipmap/ic_launcher"
    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:padding="5dp"
        android:text="上海"
        android:textSize="18sp"/>

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="5dp"
        android:text="北京"
        android:textSize="18sp"/>
</LinearLayout>

Java代码:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "leo";

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

    public void leoClick(View view) {
        View popupView = getLayoutInflater().inflate(R.layout.popup_view, null);
        //视图、宽、高、是否获取焦点(点击空白处取消)
        PopupWindow popupWindow = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, true);
        popupWindow.setBackgroundDrawable(getResources().getDrawable(R.drawable.ceshi)); //设置背景
        popupWindow.showAsDropDown(view, 50,0);
        Log.e(TAG, "leoClick: 111111");

        Button btn1 = popupView.findViewById(R.id.btn1);
        Button btn2 = popupView.findViewById(R.id.btn2);
        btn1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG, "onClick: 按钮1被点击");
            }
        });
        btn2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.e(TAG, "onClick: 按钮2被点击");
            }
        });
    }
}

运行结果图:

ListView(列表条目)

使用步骤:

  • Step 1:在布局文件中添加一个LisetView
  • Step 2:新建一个条目布局文件,这里为list_item.xml,在此文件中写好每一个条目的布局内容
  • Step 3:创建item对应的JavaBean
  • Step 4:创建待填充的数据data集合并添加数据
  • Step 5:创建辅助类Adapter(适配器)需继承BaseAdapter并实现其方法,这里命名为MyAdapter,此类中包含适配的数据和Context。实现getView的时候,需要创建View并将其返回,然后需要在此方法中通过view的findById获取list_item中的组件并填充内容
  • Step 6: 创建并初始化MyAdpter适配器,然后获取布局中的ListView,通过set方法将创建好的MyAdpter适配器复值给ListView的Adapter方法就可以完成了

 inflate()方法解析:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
  • 第一个参数:要获取的布局文件,传入R.layout.xxx
  • 第二个参数:这个参数也是一个布局,是为第一个参数指定的父布局。
  • 第三个参数(如果第二个参数为null这个参数将失去作用)
    true:将第一个参数表示的布局添加到第二参数的布局中。
    false:不将第一个参数表示的布局添加到第二参数的布局中。
    既然不添加,那么为什么第二个参数不设置为null呢。
    不添加的话,这个函数就只剩下一个作用了,那就是获取布局,为了使第一个参数的宽高属性不失效,所以要为他指定一个父布局

更多详见:2.4.5 ListView简单实用 | 菜鸟教程 (runoob.com)

MyActivity.java:

public class MainActivity extends AppCompatActivity {

    private ArrayList<Bean> data = new ArrayList<>();

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

        for (int i = 0; i < 20; i++) {
            Bean bean = new Bean("享学"+i);
            data.add(bean);
        }

        ListView listView = findViewById(R.id.lv);
        listView.setAdapter(new MyAdapter(data, this));
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Log.e("TAG", "onItemClick: " + i);
            }
        });
    }
}

Bean.java:

package com.example.mylistview;

public class Bean {
    private String name;

    public Bean(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

MyAdapter.java:

public class MyAdapter extends BaseAdapter {
    private ArrayList<Bean> data;
    private Context context;

    public MyAdapter(ArrayList<Bean> data, Context context) {
        this.data = data;
        this.context = context;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int i) {
        return data.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        if (view == null) {
            view = LayoutInflater.from(context).inflate(R.layout.list_item,viewGroup,false);
        }
        TextView tv = view.findViewById(R.id.tv);
        tv.setText(data.get(i).getName());
        Log.e("TAG", "getView: " + i);
        return view;
    }
}

activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/lv"
        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="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="享学0"
        android:textSize="30sp" />

</LinearLayout>

运行效果图: 

优化后的MyAdapter.java:即用viewHolder存储传过来的view,并为view绑定绑定上viewHolder对象

public class MyAdapter extends BaseAdapter {
    private ArrayList<Bean> data;
    private Context context;

    public MyAdapter(ArrayList<Bean> data, Context context) {
        this.data = data;
        this.context = context;
    }

    @Override
    public int getCount() {
        return data.size();
    }

    @Override
    public Object getItem(int i) {
        return data.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        ViewHolder viewHolder;
        if (view == null) {
            viewHolder = new ViewHolder();
            view = LayoutInflater.from(context).inflate(R.layout.list_item,viewGroup,false);
            viewHolder.textView = view.findViewById(R.id.tv);
            view.setTag(viewHolder);
        } else{
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.textView.setText(data.get(i).getName());
        Log.e("TAG", "getView: " + i);
        return view;
    }
    
    public final class ViewHolder{
        TextView textView;
    }
}

RecyclerView(大量数据集合的列表)

1 什么是RecycylerView:

RecycylerView是support-v7包中的新组件,是一个强大的滑动组件,是一个增强版的ListView,与经典的ListView相比,同样拥
不仅可以实现和ListView同样的效果,还优化了ListView中存在的各种不足之处,这一点从它的名字Recyclerview即回收view也可以看出。ResyslerView 能够实现横向滚动,这是ListView所不能实现的

2 如何使用:

Step 1:添加RecycylerView的依赖:

Step 2:创建布局:

  • activity_main.xml
  • item.xml

Step 3:创建适配器:

  1. 创建适配器类继承RecyclerView.Adapter<>类,传入泛型为RecyclerView.ViewHolder的子类(第2点)。
  2. 创建内部类即RecyclerView.ViewHolder类的子类,并为其绑定item控件。
  3. 实现RecyclerView.Adapter类的相关方法(快捷键为Alt+Enter)。
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType);
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position);//为MyViewHolder绑定上数据
    public int getItemCount();//返回数据的条目数

Step 4:在MainActivity.java中使用RecyclerView:

  1. 获取RecyclerView对象 。
  2. 准备适配的数据 。
  3. 适配器实例化 。
  4. 设置LayoutManager布局方式。
  5. 设置Adapter 。

 MyActivity.java:

public class MainActivity extends AppCompatActivity {

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

        ArrayList<Bean> data = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
            Bean bean = new Bean("享学"+i);
            data.add(bean);
        }

        RecyclerView recyclerView = findViewById(R.id.rv);
        MyAdapter myAdapter = new MyAdapter(data, this);

        LinearLayoutManager manager = new LinearLayoutManager(this);
        manager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(manager);

        recyclerView.setAdapter(myAdapter);
    }
}

Bean.java:

public class Bean {
    private String name;

    public Bean(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

MyAdapter.java:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private ArrayList<Bean> data;
    private Context context;

    public MyAdapter(ArrayList<Bean> data, Context context) {
        this.data = data;
        this.context = context;
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = View.inflate(context, R.layout.recycler_item, null);
        MyViewHolder myViewHolder = new MyViewHolder(view);
        return myViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.textView.setText(data.get(position).getName());
    }

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

    public class MyViewHolder extends RecyclerView.ViewHolder{
        TextView textView;
        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv);
        }
    }
}

activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

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

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="享学0"
        android:textSize="25dp"/>

</RelativeLayout>

运行效果图:

RecyclerView的点击事件

RecyclerView并没有像ListView一样暴露出Item点击事件或者长按事件处理的api,也就是说使用RecyclerView时候,需要我们自己来实现Item的点击和长按等事件的处理。实现方法有多种:

  • 可以监听RecyclerView的Touch事件然后判断手势做相应的处理
  • 也可以在通过在绑定ViewHolder的时候设置监听,然后通过Apater回调出去

这里选择第二种方法,更加直观和简单。

思路为:
通过在适配器中设置一个包含onItemClick函数的接口,然后添加setOnItemClickListener方法用于接收用户创建的该接口的实现类对象,最后在onBindViewHolder函数中调用onItemClick方法执行用于传入的对象的onItemClick方法。

设置步骤:

  • Step 1:创建OnItemClickListener接口
        public interface OnItemClickListener{
            void onItemClick(View view, int position);
            void onItemLongClick(View view, int position);
        }
  • Step 2:添加setOnItemClickListener方法()
        public void setOnItemClickListener(OnItemClickListener listenser) {
            this.onItemClickListener = listener;
        }
  • Step 3:在onBindViewHolder中调用实现类对象的onItemClick函数,也可在MyAdapter.MyViewHolder中调用实现类对象的onItemClick函数
        //第1种实现方法
        @Override
        public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
            holder.textView.setText(data.get(position).getName());
            holder.itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(final View v) {
                    if(onItemClickListener != null) {
                        int pos = holder.getLayoutPosition();
                        onItemClickListener.onItemClick(holder.itemView, pos);
                    }
                }
            });
    
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    if(onItemClickListener != null) {
                        int pos = holder.getLayoutPosition();
                        onItemClickListener.onItemLongClick(holder.itemView, pos);
                    }
                    //表示此事件已经消费,不会触发单击事件
                    return true;
                }
            });
        }
        //第2种实现方法
        public class MyViewHolder extends RecyclerView.ViewHolder{
            TextView textView;
            public MyViewHolder(@NonNull View itemView) {
                super(itemView);
                textView = itemView.findViewById(R.id.tv);
                itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        if(onItemClickListener != null) {
                            onItemClickListener.onItemClick(itemView, getLayoutPosition());
                        }
                    }
                });
                itemView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View view) {
                        if(onItemClickListener != null) {
                            onItemClickListener.onItemLongClick(itemView, getLayoutPosition());
                        }
                        return false;
                    }
                });
            }
        }

  • Step 5:调用setOnItemClickListener方法传入OnItemClickListener的实现类对象
            myAdapter.setOnItemClickListener(new MyAdapter.OnItemClickListener() {
                @Override
                public void onItemClick(View view, int position) {
                    Log.e("TAG", "onItemClick: " + position);
                }
    
                @Override
                public void onItemLongClick(View view, int position) {
                    Log.e("TAG", "onLongItemClick: " + position);
                }
            });

MyAdapter.java的完整代码:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private ArrayList<Bean> data;
    private Context context;

    public MyAdapter(ArrayList<Bean> data, Context context) {
        this.data = data;
        this.context = context;
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = View.inflate(context, R.layout.recycler_item, null);
        MyViewHolder myViewHolder = new MyViewHolder(view);
        return myViewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.textView.setText(data.get(position).getName());
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });

        holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemLongClick(holder.itemView, pos);
                }
                //表示此事件已经消费,不会触发单击事件
                return true;
            }
        });
    }

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

    public interface OnItemClickListener{
        void onItemClick(View view, int position);
        void onItemLongClick(View view, int position);
    }
    private OnItemClickListener onItemClickListener;
    public void setOnItemClickListener(MyAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }

    public class MyViewHolder extends RecyclerView.ViewHolder{
        TextView textView;
        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv);
//            itemView.setOnClickListener(new View.OnClickListener() {
//                @Override
//                public void onClick(View view) {
//                    if(onItemClickListener != null) {
//                        onItemClickListener.onItemClick(itemView, getLayoutPosition());
//                    }
//                }
//            });
//            itemView.setOnLongClickListener(new View.OnLongClickListener() {
//                @Override
//                public boolean onLongClick(View view) {
//                    if(onItemClickListener != null) {
//                        onItemClickListener.onItemLongClick(itemView, getLayoutPosition());
//                    }
//                    return false;
//                }
//            });
        }
    }
}

recycler_itme.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">

    <TextView
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="享学0"
        android:textSize="25dp"/>

</RelativeLayout>

运行效果图:

第3章 Android的布局方式

3.1 LinearLayout(线性布局)

3.1.1 常用属性

  • orientation:布局中组件的排列方式
  • gravity:控制组件所包含的子元素的对齐方式,可多个组合
  • layout_gravity:控制该组件在父容器里的对其方式
  • background:为该组件设置一个背景图片,或者是直接用颜色覆盖
  • divider:分割线(可以使用View来设置分割线的效果)
  • showDividers:设置分割线所在的位置,none(无),beginning(开始),end(结束),middle(每两个组件间)
  • dividerPadding:设置分割线的padding
  • layout_weight(权重):该属性是用来按比例划分剩余的区域,按控件的对齐方式

更多详见:2.2.1 LinearLayout(线性布局) | 菜鸟教程 (runoob.com)

XML代码:

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

    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="0dp"
        android:layout_weight="3"
        android:background="#FF0000"/>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#000000"
        android:layout_marginVertical="10dp"/>

    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="0dp"
        android:layout_weight="2"
        android:background="#00FF00"/>

    <LinearLayout
        android:layout_width="300dp"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#0000FF"/>

</LinearLayout>

运行效果图:

3.2 RelativeLayout(相对布局)

3.2.1 相对父容器定位

  • layout_alignParentLeft:左对齐
  • layout_alignParentRight:右对齐
  • layout_alignParentTop:顶部对齐
  • layout_alignParentBottom:底部对齐
  • layout_centerHorizontal:水平居中
  • layout_centerVertical:垂直居中
  • layout_centerInParent:中间位置
     

3.2.2 相对兄弟容器定位

  • layout_toLeftOf:放置于参考组件的左边
  • layout_toRightOf:放置于参考组件的右边
  • layout_above:放置于参考组件的上方
  • layout_below:放置于参考组件的下方
  • layout_alignTop:对齐参考组件的上边界
  • layout_alignBottom:对齐参考组件的下边界
  • layout_alignLeft:对齐参考组件的左边界
  • layout_alignRight:对齐参考组件的右边界

3.2.3 通用属性

margin设置组件与父容器的边距

  • layout_margin:上下左右偏移
  • layout_marginLeft:左边距
  • layout_marginRight:右边距
  • layout_marginTop:上边距
  • layout_margiBottom:下边距

padding设置组件与子容器的边距

  • layout_padding:上下左右偏移
  • layout_paddingLeft:左边距
  • layout_paddingRight:右边距
  • layout_paddingTop:上边距
  • layout_paddingBottom:下边距

更多详见:2.2.2 RelativeLayout(相对布局) | 菜鸟教程 (runoob.com)

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

    <RelativeLayout
        android:id="@+id/rl1"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:layout_centerInParent="true"
        android:background="#FF0000"/>

    <RelativeLayout
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_above="@+id/rl1"
        android:background="#00FF00"/>

    <RelativeLayout
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_alignTop="@+id/rl1"
        android:background="#0000FF"/>

    <RelativeLayout
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:layout_marginLeft="100dp"
        android:background="#FFFF00"/>
</RelativeLayout>

运行效果图: 

3.3 TableLayout(表格布局)

3.3.1 常见属性

  • android:collapseColumns:设置需要被隐藏的列的序号,从0开始
  • android:stretchColumns:设置允许被拉伸的列的列序号,从0开始,要有被拉伸的空间才有效
  • ndroid:shrinkColumns:设置允许被收缩的列的列序号,从0开始,要有超出的空间才有效

3.3.2 子控件设置属性

  • android:layout_column:显示在第几列
  • android:layout_span:横向跨几列

更多详见:2.2.3 TableLayout(表格布局) | 菜鸟教程 (runoob.com)

XML代码:

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

    <TableRow>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第0个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第1个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第2个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第3个"/>
    </TableRow>

    <TableRow>
        <Button
            android:layout_column="1"
            android:layout_span="2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第0个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第1个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第2个"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="第3个"/>
    </TableRow>
</TableLayout>

运行效果图: 

3.4 FrameLayout(帧布局)

3.4.1 常用属性

  • android:foreground设置前景
  • android:foregroundGravity设置前景位置

更多详见:2.2.4 FrameLayout(帧布局) | 菜鸟教程 (runoob.com)

XML代码:

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

    <FrameLayout
        android:layout_width="400dp"
        android:layout_height="400dp"
        android:background="#FF0000"/>

    <FrameLayout
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:background="#00FF00"
        android:foreground="@drawable/ceshi"
        android:foregroundGravity="right|bottom"/>

    <FrameLayout
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="#0000FF"/>
</FrameLayout>

运行效果图:

3.5 GridLayout(网格布局)

3.5.1 常见属性

  • android:orientation:设置水平显示还是垂直显示
  • android:columnCount:设置行的显示个数
  • android:rowCount:设置列的显示个数

3.5.2 子控件属性

  • android:layout_column:显示在第几列
  • android:layout_columnSpan:横向跨几列
  • android:layout_columnWeight:横向剩余空间分配方式
  • android:layout_gravity:在网格中的显示位置
  • android:layout_row:显示在第几行
  • android:layout_rowSpan:横向跨几行
  • android:layout_rowWeight:纵向剩余空间分配方式

更多详见:2.2.5 GridLayout(网格布局) | 菜鸟教程 (runoob.com)

XML代码:

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

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第0个"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第1个"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_column="1"
        android:layout_row="1"
        android:text="第2个"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_rowWeight="1"
        android:text="第3个"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="第4个"/>
</GridLayout>

运行效果图:

第4章 ConstraintLayout(约束布局)

ConstraintLayout采用方向约束的方式对控件进行定位,至少要保证水平和垂直方向都至少有一个约束才能确定控件的位置

4.1 布局的分类 

4.1.1 相对定位

我自己的XX位置和谁的YY位置对齐
注意:前四个不能与后四个的对应(Left-->Start、Right-->End)效果相同,但是它们不能交叉使用,否则后者无效果,后面内容同理

  • layout_constraintLeft_toLeftOf
  • layout_constraintLeft_toRightOf
  • layout_constraintRight_toLeftOf
  • layout_constraintRight_toRightOf
  • layout_constraintTop_toTopOf
  • layout_constraintTop_toBottomOf
  • layout_constraintBottom_toTopOf
  • layout_constraintBottom_toBottomOf
  • layout_constraintBaseline_toBaselineOf:内容的底部对齐
  • layout_constraintStart_toEndOf
  • layout_constraintStart_toStartOf
  • layout_constraintEnd_toStartOf
  • layout_constraintEnd_toEndOf

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/tv1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="20"
        android:textColor="@color/black"
        android:textSize="50sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="¥"
        android:textColor="@color/black"
        android:textSize="20sp"
        app:layout_constraintBaseline_toBaselineOf="@id/tv1"
        app:layout_constraintStart_toEndOf="@id/tv1" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果图: 

4.1.2 外边距、内边距及goneMargin

外边距(margin):

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

内边距(padding):

  • android:padding
  • android:paddingLeft
  • android:paddingTop
  • android:paddingRight
  • android:paddingBottom
  • android:paddingStart
  • android:paddingEnd

goneMargin:

goneMargin是一个依赖于目标的属性,当依赖的目标隐藏(android:visibility="gone")时才会生效,例如A被B依赖约束,当A隐藏时B设置的goneMargin就会生效,为不影响布局,往往会为B控件设置goneMargin,属性如下:

  • layout_goneMarginLeft
  • layout_goneMarginTop
  • layout_goneMarginRight
  • layout_goneMarginBottom
  • layout_goneMarginStart
  • layout_goneMarginEnd

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/A"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B"
        android:layout_width="60dp"
        android:layout_height="40dp"
        android:gravity="center"
        android:background="#FF0000"
        android:text="B"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintBottom_toBottomOf="@id/A"
        app:layout_constraintStart_toEndOf="@id/A"
        app:layout_constraintTop_toTopOf="@id/A"
        app:layout_goneMarginStart="130dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果: 

4.1.3 偏移

  • layout_constraintHorizontal_bias:水平偏移,默认值0.5(水平居中)
  • layout_constraintVertical_bias:垂直偏移,默认值0.5(垂直居中)

4.1.4 角度约束

  • app:layout_constraintCircle:目标控件id
  • app:layout_constraintCircleRadius:到目标中心的距离
  • app:layout_constraintCircleAngle:对于目标的角度(0-360),正上方为0

4.1.5 尺寸约束及0dp值

约束控件的最小、最大尺寸:

  • android:minwidth
  • android:minHeight
  • android:maxWidth
  • android:maxHeight

控件大小的0dp值:

设置控件的大小除了传统的wrap_content 、指定具体尺寸、match_parent 外,ConstraintLayout还可以设置为0dp (MATCH_CONSTRAINT),并且 0dp的作用会根据设置的类型而产生不同的作用,进行设置类型的属性是:

  • layout_constraintWidth_default:水平方向控制
  • layout_constraintHeight_default:垂直方向控制

其属性可取值如下:

  • spread(默认):固定占用所有符合约束限制的空间,内容占用的空间不会侵占margin部分
  • wrap:匹配内容大小但不超过约束限制,内容占用的空间不会侵占margin部分
  • percent:父布局尺寸的一定比例,该模式需要配合layout_constrainWidth_prcent使用,但是写了layout_constrainWidth_prcent后,layout_constraintWidth_default="percent"其实就可以省略掉了,内容占用的空间不够时会侵占margin部分

注意:当空间的尺寸设置成wrap_content时内容占用的空间会去侵占margin部分,可通过以下属性是否强制限制

  • app:layout_constrainedWidth="true|false"
  • app:layout_constrainedHeight="true|false" 

0dp下的其他属性值:

  • layout_constraintWidth_min和layout_constraintHeight_min:设置维度的最小大小
  • layout_constraintWidth_max和layout_constraintHeight_max:设置维度的最大大小
  • layout_constraintWidth_percent和layout_constraintHeight_percent:会将维度的大小设置为父维度的百分比

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/A1"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="30dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="spread"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="spread" />

    <TextView
        android:id="@+id/A2"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="100dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="app:layout_constraintWidth_defaultspread"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="spread" />

    <TextView
        android:id="@+id/B1"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="180dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="wrap"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="wrap" />

    <TextView
        android:id="@+id/B2"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="260dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="app:layout_constraintWidth_default=wrap"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="wrap" />

    <TextView
        android:id="@+id/C1"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="340dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="percent"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="percent"/>

    <TextView
        android:id="@+id/C2"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="420dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="app:layout_constraintWidth_default=percent"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_default="percent"/>

    <TextView
        android:id="@+id/C3"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="100dp"
        android:layout_marginTop="500dp"
        android:layout_marginEnd="100dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="app:layout_constraintWidth_percent=0.5"
        android:textColor="@color/black"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintWidth_percent="0.5"/>

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果:

4.1.6 宽高比例约束(Ratio)

ConstraintLayout中可以对宽高设置比例,前提是至少有一个约束维度设置为0dp,这样比例才会生效,若两个都设置为0dp则以父元素的宽作为参考,该属性可使用两种设置:

  • app:layout_constraintDimensionRatio="宽度:高度":表示宽度和高度之间形式的比率
  • app:layout_constraintDimensionRatio="浮点值":表示宽度和高度之间的比率

注意:两个属性都有两个参数,第一个参数(W/H)可以省略,W表示宽度通过壁纸计算得来,H表示高度通过壁纸计算得来

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/A"
        android:layout_width="160dp"
        android:layout_height="0dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintDimensionRatio="16:9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果图:

4.1.7 Chains(链)

Chains(链)它是将许多个控件在水平或者垂直方向,形成一条链,用于平衡这些控件的位置,设置成链的方式如下:

  • layout_constraintHorizontal_chainStyle:水平成链
  • layout_constraintVertical_chainStyle:垂直成链

可以属性如下:

  • spread(默认):均分剩余空间
  • packed:所有控件贴紧居中
  • spread_inside:两侧的控件贴近两边,剩余的控件均分剩余空间

Chains(链)还支持weight(权重)的配置,使用如下属性进行设置链元素的权重:

  • layout_constraintHorizontal_weight:水平方向权重分配
  • layout_constraintVertical_weight:垂直方向权重分配

注意:若需要进行权重设置,则该方向的上的尺寸设置为0dp

Chains(链)还支持bias(偏移)的配置,使用如下属性进行设置链元素的偏移:

  • layout_constraintHorizontal_bias:水平方向偏移,0.5表示居中
  • layout_constraintVertical_bias:垂直方向的偏移,0.5表示居中
  • 组合使用:

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/A"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/B"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="B"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/A"
        app:layout_constraintEnd_toStartOf="@id/C"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/C"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="C"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/B"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/A1"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginTop="100dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A1"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_chainStyle="spread_inside"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/B1"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B1"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="100dp"
        android:gravity="center"
        android:text="B1"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/A1"
        app:layout_constraintEnd_toStartOf="@id/C1"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/C1"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="100dp"
        android:gravity="center"
        android:text="C1"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/B1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/A2"
        android:layout_width="0dp"
        android:layout_height="80dp"
        android:layout_marginTop="200dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_weight="1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/B2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B2"
        android:layout_width="0dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="200dp"
        android:gravity="center"
        android:text="B2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintStart_toEndOf="@id/A2"
        app:layout_constraintEnd_toStartOf="@id/C2"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/C2"
        android:layout_width="0dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="200dp"
        android:gravity="center"
        android:text="C2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_weight="2"
        app:layout_constraintStart_toEndOf="@id/B2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/A3"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginTop="300dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A3"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/B3"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B3"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="300dp"
        android:gravity="center"
        android:text="B3"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/A3"
        app:layout_constraintEnd_toStartOf="@id/C3"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/C3"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="300dp"
        android:gravity="center"
        android:text="C3"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/B3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/A4"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginTop="400dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A4"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintHorizontal_chainStyle="packed"
        app:layout_constraintHorizontal_bias="0.3"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/B4"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B4"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="400dp"
        android:gravity="center"
        android:text="B4"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/A4"
        app:layout_constraintEnd_toStartOf="@id/C4"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/C4"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:layout_marginTop="400dp"
        android:gravity="center"
        android:text="C4"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toEndOf="@id/B4"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果图: 

4.2 辅助类

ConstraintLayout为了解决嵌套问题还提供了一系列的辅助控件帮助开发者布局

4.2.1 Guideline(参考线)

Guideline是一条参考线,可以帮助开发者进行辅助定位,它并不会真正显示在布局中,像是数学几何中的辅助线一样

  • android:orientation="horizontal|vertical" :辅助线的方向(垂直|水平)
  • app:layout_constraintGuide_percent:距离父级宽度或高度的百分比(0-1的小数形式)
  • app:layout_constraintGuide_begin:距离父级起始位置的距离(左侧或顶部)
  • app:layout_constraintGuide_end:距离父级结束位置的距离(右侧或底部)

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/A"
        android:layout_width="120dp"
        android:layout_height="80dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="@+id/guideline1"
        app:layout_constraintTop_toTopOf="@+id/guideline2" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="60dp" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="120dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果图:

4.2.2 Barrier(屏障)

当我们创建Android布局时,有时我们会遇到布局可以根据本地化而变化的情况。下面是一个来自于约束布局 (constraintlayout.com)非常简单的示例:

这里有三个文本视图:左边的textView1textView2;右边的textView3textView3被限制在textView1的末尾,这工作得很好——它完全根据我们需要来定位和大小textView3。然而,如果我们需要支持多种语言,事情会变得更加复杂。如果我们添加德语翻译,那么我们就会遇到一个问题,因为在英文版本中,textView1中的文本比textView2中的文本长,而在德语中,textView2中的文本比textView1长:

这里的问题在于textView3仍然是相对于textView1的,所以textView2直接插入了textView3中。在设计视图里看起来更明显(白色背景的那个)。比较直接的解决办法是使用TableLayout,或者把 textView1 & textView2 包裹在一个垂直的,android:layout_width="wrap_content"LinearLayout中。然后让textView3约束在这个LinearLayout的后面。但是我们有更好的办法:Barriers
Barriers的配置属性如下:

  • app:barrierDirection="top|bottom|left|right|start|end":用于控制Barrier相对于给定的View的位置
  • app:constraint_referenced_ids="id,id":取值是要依赖的控件的id,Barrier将会使用ids中最大的一个的宽/高作为自己的位置

4.2.3 Group(组)

ConstraintLayout中的Group的作用是对一组控件同时隐藏或显示,没有其他的作用,它的属性如下:

  • app:constraint_referenced_ids="id1,id2":加入组的控件id

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/A1"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:layout_marginTop="60dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="A1"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/A2"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:layout_marginTop="160dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="A2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.1"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B1"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:layout_marginTop="60dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="B2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B2"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:layout_marginTop="60dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="B2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/B3"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:layout_marginTop="160dp"
        android:background="#0FF0F0"
        android:gravity="center"
        android:text="B2"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.widget.Group
        android:id="@+id/group1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:constraint_referenced_ids="A1,A2" />

    <androidx.constraintlayout.widget.Group
        android:id="@+id/group2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="visible"
        app:constraint_referenced_ids="B1,B2,B3" />

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="28dp"
        android:layout_marginTop="300dp"
        android:text="A组隐藏/显示"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="272dp"
        android:layout_marginTop="300dp"
        android:text="B组隐藏/显示"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Java代码: 

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Button btn1,btn2;
    Group group1,group2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_group);
        btn1 = findViewById(R.id.btn1);
        group1 = findViewById(R.id.group1);
        btn2 = findViewById(R.id.btn2);
        group2 = findViewById(R.id.group2);
        btn1.setOnClickListener(this);
        btn2.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == btn1.getId()){
            group1.setVisibility(group1.getVisibility()==Group.VISIBLE?Group.INVISIBLE:Group.VISIBLE);
        } else{
            group2.setVisibility(group2.getVisibility()==Group.VISIBLE?Group.INVISIBLE:Group.VISIBLE);
        }
    }
}

运行效果图:

4.2.4 Placeholder(占位符)

Placeholder的作用就是占位,它可以在布局中占好位置,通过app:content=""属性,或者动态调用setContent()设置内容,来让某个控件移动到此占位符中

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/constraintLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/A"
        android:layout_width="100dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="AAA"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <androidx.constraintlayout.widget.Placeholder
        android:id="@+id/p"
        android:layout_width="100dp"
        android:layout_height="60dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="150dp"
        android:layout_marginTop="200dp"
        android:text="移动"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    Placeholder placeholder;
    TextView tv1,tv2;
    ConstraintLayout mConstraintLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_placeholder);
        Button btn1 = findViewById(R.id.btn1);
        placeholder = findViewById(R.id.p);
        tv1 = findViewById(R.id.A);
        tv2 = findViewById(R.id.B);
        mConstraintLayout = findViewById(R.id.constraintLayout);
        btn1.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        TransitionManager.beginDelayedTransition(mConstraintLayout);
        if (placeholder.getContent() == null) {
            System.out.println(tv1.getId());
            placeholder.setContentId(tv1.getId());
        }else {
            if (placeholder.getContent().getId()==tv1.getId()){
                placeholder.setContentId(tv2.getId());
            }else {
                placeholder.setContentId(tv1.getId());
            }
        }
    }
}

运行效果图: 

 注意:设置文本内容时样式发生了改变,而且placeholder不知如何还原初态

4.2.5 Flow(流式虚拟布局)

Flow是用于构建链的新虚拟布局,当链用完时可以缠绕到下一行甚至屏幕的另一部分。当您在一个链中布置多个项目,但是您不确定容器在运行时的大小时,这很有用。您可以使用它来根据应用程序中的动态尺寸(例如旋转时的屏幕宽度)构建布局。

1. 链约束(针对的时超出屏幕部分)

Flowconstraint_referenced_ids关联的控件是没有设置约束的,这一点和普通的链是不一样的,这种排列方式是Flow的默认方式none,我们可以使用app:flow_wrapMode=""属性来设置排列方式,并且我们还可以使用flow_horizontalGapflow_verticalGap分别设置两个view在水平和垂直方向的间隔,下面我们再添加几个控件来展示三种排列方式:

  • none(默认值):所有引用的view形成一条链,水平居中,超出屏幕两侧的view不可见
  • chian:所引用的view形成一条链,超出部分会自动换行,同行的view会平分宽度,纵横平均分配Flow空间
  • aligned:所引用的view形成一条链,超出部分会自动换行,同行的view会平分宽度,纵向平均分配Flow空间,横向的view会从start处开始排列,默认从左往右排列

 当flow_wrapMode的值是chianaligned时,我们还可以针对不同的链进行配置,这里就不一一展示效果了,具体的属性如下:

  • app:flow_horizontalStyle="packed|spread|spread_inside"  所有水平链的配置(app:flow_wrapMode="chian"
  • app:flow_verticalStyle="packed|spread|spread_inside"    所有垂直链的配置(app:flow_wrapMode="aligned"
  • app:flow_firstHorizontalStyle="packed|spread|spread_inside" 第一条水平链的配置,其他条不生效
  • app:flow_firstVerticalStyle="packed|spread|spread_inside"   第一条垂直链的配置,其他条不生效
  • app:flow_lastHorizontalStyle="packed|spread|spread_inside"  最后一条水平链的配置,其他条不生效 
  • app:flow_lastVerticalStyle="packed|spread|spread_inside"    最后一条垂直链的配置,其他条不生效 

XML代码: 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <TextView
        android:id="@+id/A"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="A"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/B"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="B"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/C"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="C"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/D"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="D"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/E"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="E"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/F"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="F"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/G"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="G"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <TextView
        android:id="@+id/H"
        android:layout_width="60dp"
        android:layout_height="60dp"
        android:background="#00FF00"
        android:gravity="center"
        android:text="E"
        android:textColor="@color/black"
        android:textSize="25sp"
        android:textStyle="bold" />

    <androidx.constraintlayout.helper.widget.Flow
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:constraint_referenced_ids="A,B,C,D,E,F,G,H"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

2. 对齐约束

view的大小不同时,Flow也提供了相应的属性进行配置(flow_wrapMode="aligned"时,我试着没有效果)。

使用flow_verticalAlign时,要求orientation的方向是horizontal

使用flow_horizontalAlign时,要求orientation的方向是vertical

  • app:flow_verticalAlign="top|bottom|center|baseline"
  • app:flow_horizontalAlign="start|end|center"

 horizontal 水平排列

  • top

    <androidx.constraintlayout.helper.widget.Flow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:flow_wrapMode="chain"
        android:orientation="horizontal"
        app:flow_verticalAlign="top"
        app:constraint_referenced_ids="A,B,C,D,E,F,G,H"
        app:layout_constraintTop_toTopOf="parent" />
  • button
    <androidx.constraintlayout.helper.widget.Flow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:flow_wrapMode="chain"
        android:orientation="horizontal"
        app:flow_verticalAlign="bottom"
        app:constraint_referenced_ids="A,B,C,D,E,F,G,H"
        app:layout_constraintTop_toTopOf="parent" />
  •  center(默认)
    <androidx.constraintlayout.helper.widget.Flow
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:flow_wrapMode="chain"
        android:orientation="horizontal"
        app:flow_verticalAlign="center"
        app:constraint_referenced_ids="A,B,C,D,E,F,G,H"
        app:layout_constraintTop_toTopOf="parent" />
  • baseline(我这里无效果)

 垂直方向排列同理

3. 数量约束

flow_wrapMode属性为alignedchian时,通过flow_maxElementsWrap属性控制每行最大的子View数量,例如我们设置为flow_maxElementsWrap=4,效果图如下:

 

4.2.6 ImageFilterButton & ImageFilterView

ImageFilterButtonImageFilterView是两个控件,他们之间的关系就和ImageButtonImageView一样,即ImageFilterButton继承ImageButton,而ImageFilterView继承ImageView,所以这里就只拿ImageFilterView来做讲解。它们的大致作用有两部分,一是可以用来做圆角图片,二是可以叠加图片资源进行混合过滤,下面一一展示:

1. 圆角图片

ImageFilterButtonImageFilterView可以使用两个属性来设置图片资源的圆角,分别是roundPercentroundroundPercent接受的值类型是0-1的小数,根据数值的大小会使图片在方形和圆形之间按比例过度,round可以设置具体圆角的大小。下面请看例子:

2. 图片过滤

ImageFilterButtonImageFilterView不但可以使用src来设置图片资源,还可以使用altSrc来设置第二个图片资源,altSrc提供的资源将会和src提供的资源通过crossfade属性形成交叉淡化效果,默认情况下,crossfade=0altSrc所引用的资源不可见,取值在0-1。下面看例子:

XML代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:layout_width="100dp"
        android:layout_height="100dp"
        android:src="@drawable/mi"
        app:layout_constraintBottom_toTopOf="@+id/guideline3"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:roundPercent="0.7" />

    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        app:layout_constraintGuide_begin="300dp" />

    <androidx.constraintlayout.utils.widget.ImageFilterView
        android:id="@+id/view1"
        android:layout_width="280dp"
        android:layout_height="280dp"
        android:src="@drawable/jetpack"
        app:altSrc="@drawable/bg1"
        app:crossfade="0.5"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@+id/guideline3" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行效果图:

除此之外,还有以下属性(取值范围为:0-10):
warmth:调节色温
brightness:调节亮度
saturation:调节饱和度
contrast:调节对比度

第5章 动画效果

5.1 帧动画

Step 1:创建frame.xml。

  • 创建animation-list并编写标签,属性为:drable="资源",duration="持续时间"

Step 2:在activity_main.xml文件中引用frame资源为View的背景。

Step 3:获取View的背景强转为AnimationDrawable。

  • AnimationDrawable animationDrawable = (AnimationDrawable) relativeLayout.getBackground();

Step 3:调用AnimationDrawable的方法进行控制

  • start(); 启动
  • stop(); 停止

drawable/frame.xml

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/frame1" android:duration="100"/>
    <item android:drawable="@drawable/frame2" android:duration="100"/>
    <item android:drawable="@drawable/frame3" android:duration="100"/>
    <item android:drawable="@drawable/frame4" android:duration="100"/>
</animation-list>

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

    <Button
        android:id="@+id/btn_start"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始" />

    <Button
        android:id="@+id/btn_stop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="停止" />

    <ImageView
        android:id="@+id/frame"
        android:layout_width="300dp"
        android:layout_height="300dp"
        android:layout_gravity="center"
        android:background="@drawable/frame" />

</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

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

        Button btn_start = findViewById(R.id.btn_start);
        Button btn_stop = findViewById(R.id.btn_stop);

        ImageView imageView = findViewById(R.id.frame);
        AnimationDrawable animationDrawable = (AnimationDrawable) imageView.getBackground();

        btn_start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                animationDrawable.start();
            }
        });

        btn_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                animationDrawable.stop();
            }
        });
    }
}

运行效果图: 

5.2 补间动画

5.2.1 补间动画的介绍

补间动画是一种设定动画开始状态、结束状态,其中间的变化由系统计算补充。

补间动画由Animation类来实现具体效果,包括透明度(AlphaAnimation)、旋转(RotateAnimation)、缩放(ScaleAnimation)、平移(TranslateAnimation)四个子类,四种变化。

5.2.2 透明度(AlphaAnimation)

透明度渐变效果,创建时许指定开始以及结束透明度,还有动画的持续 时间,透明度的变化范围(0,1),0是完全透明,1是完全不透明;对应<alpha/>标签 

步骤如下:

Step 1:编写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">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:layout_centerHorizontal="true"
        android:maxWidth="300dp"
        android:maxHeight="300dp"
        android:src="@drawable/ceshi"/>

    <Button
        android:id="@+id/btn_alpha"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="400dp"
        android:text="透明" />

</RelativeLayout>

Step 2:res/创建anim目录(Directory)/新建alpha.xml文件(Animation Resource File)

Step 3:编写alpha.xml文件

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:duration="5000"/>
</set>

Step 4:1在ActivityMain.java中获取需要设置alpha的imageview控件,然后加载xml动画设置文件来创建一个 Animation对象,通过控件的imageview.startAnimation(animation)方法来启动动画

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private ImageView imageView;

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

        imageView = findViewById(R.id.iv);

        Button btn_alpha = findViewById(R.id.btn_alpha);

        btn_alpha.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        //通过加载xml动画设置文件来创建一个 Animation对象
        Animation animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.alpha);
        Log.e("TAG", "onClick: " + view.getId());
        imageView.startAnimation(animation);
    }
}

Step 5:运行效果图:

5.2.3 旋转(RotateAnimation)

旋转渐变效果,创建时指定动画起始以及结束的旋转角度,以及动画 持续时间和旋转的轴心;对应<rotate/>标签

 步骤:同上

rotate.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <rotate android:pivotX="50%" android:pivotY="50%"
        android:fromDegrees="0" android:toDegrees="360"
        android:duration="5000"/>
</set>

5.2.4 缩放(ScaleAnimation)

缩放渐变效果,创建时需指定开始以及结束的缩放比,以及缩放参考点, 还有动画的持续时间;对应<scale/>标签

 步骤:同上 

scale.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <scale android:pivotY="50%" android:pivotX="50%"
        android:fromXScale="0.2" android:fromYScale="0.2"
        android:toXScale="1" android:toYScale="1"
        android:duration="5000"/>
</set>

5.2.5 平移(TranslateAnimation)

位移渐变效果,创建时指定起始以及结束位置,并指定动画的持续时间即可;对应<translate/>标签

 步骤:同上 

translate.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate android:fromXDelta="-40%" android:fromYDelta="0%"
        android:toXDelta="60%" android:toYDelta="0%"
        android:duration="5000"/>
</set>

5.2.6 组合(AnimationSet

组合渐变,就是前面多种渐变的组合,对应<set/>标签。以最大的时间为渐变时间

 步骤:同上

sets.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha android:fromAlpha="0" android:toAlpha="1"
        android:duration="5000"/>
    <rotate android:pivotX="50%" android:pivotY="50%"
        android:fromDegrees="0" android:toDegrees="360"
        android:duration="5000"/>
    <scale android:pivotY="50%" android:pivotX="50%"
        android:fromXScale="0.2" android:fromYScale="0.2"
        android:toXScale="1" android:toYScale="1"
        android:duration="5000"/>
    <translate android:fromXDelta="-40%" android:fromYDelta="0%"
        android:toXDelta="60%" android:toYDelta="0%"
        android:duration="5000"/>
</set>

更多详见:8.4.2 Android动画合集之补间动画 | 菜鸟教程 (runoob.com)

activity.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">

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="100dp"
        android:layout_centerHorizontal="true"
        android:maxWidth="300dp"
        android:maxHeight="300dp"
        android:src="@drawable/ceshi"/>

    <Button
        android:id="@+id/btn_alpha"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="400dp"
        android:text="透明" />

    <Button
        android:id="@+id/btn_rotate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="110dp"
        android:layout_marginTop="400dp"
        android:text="旋转" />

    <Button
        android:id="@+id/btn_scale"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="210dp"
        android:layout_marginTop="400dp"
        android:text="缩放" />

    <Button
        android:id="@+id/btn_translate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="310dp"
        android:layout_marginTop="400dp"
        android:text="平移" />

    <Button
        android:id="@+id/btn_sets"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="460dp"
        android:text="上诉组合" />
</RelativeLayout>

ActivityMain.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private ImageView imageView;

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

        imageView = findViewById(R.id.iv);

        Button btn_alpha = findViewById(R.id.btn_alpha);
        Button btn_rotate = findViewById(R.id.btn_rotate);
        Button btn_scale = findViewById(R.id.btn_scale);
        Button btn_translate = findViewById(R.id.btn_translate);
        Button btn_sets = findViewById(R.id.btn_sets);

        btn_alpha.setOnClickListener(this);
        btn_rotate.setOnClickListener(this);
        btn_scale.setOnClickListener(this);
        btn_translate.setOnClickListener(this);
        btn_sets.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Animation animation = null;
        switch (view.getId()){
            case R.id.btn_alpha:
                animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.alpha);
                break;
            case R.id.btn_rotate:
                animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.rotate);
                break;
            case R.id.btn_scale:
                animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.scale);
                break;
            case R.id.btn_translate:
                animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.translate);
                break;
            case R.id.btn_sets:
                animation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.sets);
                break;
        }
        Log.e("TAG", "onClick: " + view.getId());
        imageView.startAnimation(animation);
    }
}

运行效果图:

5.3 属性动画

详见:8.4.3 Android动画合集之属性动画-初见 | 菜鸟教程 (runoob.com)

第6章 ViewPager(视图滑动切换)

6.1 ViewPager的简单介绍

ViewPager就是一个简单的页面切换组件,我们可以往里面填充多个View,然后我们可以左 右滑动,从而切换不同的View,我们可以通过setPageTransformer()方法为我们的ViewPager 设置切换时的动画效果,和前面学的ListView,GridView一样,我们也需要一个Adapter (适配器)将我们的View和ViewPager进行绑定,而ViewPager则有一个特定的Adapter—— PagerAdapter!另外,Google官方是建议我们使用Fragment来填充ViewPager的,这样 可以更加方便的生成每个Page,以及管理每个Page的生命周期!给我们提供了两个Fragment 专用的Adapter:FragmentPageAdapterFragmentStatePagerAdapter 我们简要的来分析下这两个Adapter的区别:

  • FragmentPageAdapter:和PagerAdapter一样,只会缓存当前的Fragment以及左边一个,右边 一个,即总共会缓存3个Fragment而已,假如有1,2,3,4四个页面:
    处于1页面:缓存1,2
    处于2页面:缓存1,2,3
    处于3页面:销毁1页面,缓存2,3,4
    处于4页面:销毁2页面,缓存3,4
    更多页面的情况,依次类推~
  • FragmentStatePagerAdapter:当Fragment对用户不 见得时,整个Fragment会被销毁, 只会保存Fragment的状态!而在页面需要重新显示的时候,会生成新的页面!

综上,FragmentPageAdapter适合固定的页面较少的场合;而FragmentStatePagerAdapter则适合 于页面较多或者页面内容非常复杂(需占用大量内存)的情况!

使用步骤:

Step 1:在activity_main.xml中编写ViewPage控件

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

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Step 2:在layout/创建多个布局layout.xml

Step 3:编写MyPagerAdapter.java继承PagerAdapter并实现其两个方法

  • public int getCount();
  • public boolean isViewFromObject(@NonNull View view, @NonNull Object object);

Step 4:1

6.2 PagerAdapter的使用

我们先来介绍最普通的PagerAdapter,如果想使用这个PagerAdapter需要重写下面的四个方法: 当然,这只是官方建议,实际上我们只需重写getCount()和isViewFromObject()就可以了~

  • getCount():获得viewpager中有多少个view
  • destroyItem():移除一个给定位置的页面。适配器有责任从容器中删除这个视图。 这是为了确保在finishUpdate(viewGroup)返回时视图能够被移除。

而另外两个方法则是涉及到一个key的东东:

  • instantiateItem():
    ①将给定位置的view添加到ViewGroup(容器)中,创建并显示出来
    ②返回一个代表新增页面的Object(key),通常都是直接返回view本身就可以了,当然你也可以 自定义自己的key,但是key和每个view要一一对应的关系
  • isViewFromObject(): 判断instantiateItem(ViewGroup, int)函数所返回来的Key与一个页面视图是否是 代表的同一个视图(即它俩是否是对应的,对应的表示同一个View),通常我们直接写 return view == object!

6.3 ViewPager的简单实例

Step 1:在activity_main.xml中编写ViewPage控件

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

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Step 2:在layout/创建多个布局layouti.xm。

Step 3:编写MyPagerAdapter.java继承PagerAdapter并实现其方法。使用这个PagerAdapter需要重写下面的四个方法: 当然,这只是官方建议,实际上我们只需重写getCount()和isViewFromObject()就可以了

  • public int getCount();
    获得viewpager中有多少个view
  • public boolean isViewFromObject(@NonNull View view, @NonNull Object object);
    移除一个给定位置的页面。适配器有责任从容器中删除这个视图,这是为了确保在finishUpdate(viewGroup)返回时视图能够被移除。

 而另外两个方法则是涉及到一个key的东西:

  • public Object instantiateItem(@NonNull ViewGroup container, int position);
    ①将给定位置的view添加到ViewGroup(容器)中,创建并显示出来
    ②返回一个代表新增页面的Object(key),通常都是直接返回view本身就可以了,当然你也可以 自定义自己的key,但是key和每个view要一一对应的关系
  • public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object);
    判断instantiateItem(ViewGroup, int)函数所返回来的Key与一个页面视图是否是 代表的同一个视图(即它俩是否是对应的,对应的表示同一个View),通常我们直接写 return view == object

 MyViewPagerAdapter.java

public class MyPagerAdapter extends PagerAdapter {

    private ArrayList<View> listView;

    public MyPagerAdapter(ArrayList<View> listView) {
        this.listView = listView;
    }

    @Override
    public int getCount() {
        return listView.size();
    }

    @Override
    public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
        return view == object;
    }

    @NonNull
    @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        container.addView(listView.get(position), 0);
        return listView.get(position);
    }

    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        container.removeView(listView.get(position));
    }
}

Step 4:获取加载布局的系统服务。 

Step 5:准备数据。利用LayoutInflater对象的inflate()方法获取切换的布局并且添加到列表集合中。

  • LayoutInflater. inflate() 这个方法的作用类似于 findViewById() 。不同点是 inflate() 是用来找 res/layout/ 下的 xml 布局文件,并且实例化,而 findViewById() 是找 xml 布局文件下的具体 widget 控件

Step 6:实例化 PagerAdapter适配器。

MainActivity.java

public class MainActivity extends AppCompatActivity {

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

        //获取加载布局的系统服务
        LayoutInflater layoutInflater = LayoutInflater.from(this);

        View view1 = layoutInflater.inflate(R.layout.layout1, null);
        View view2 = layoutInflater.inflate(R.layout.layout2, null);
        View view3 = layoutInflater.inflate(R.layout.layout3, null);
        ArrayList<View> listView = new ArrayList<>();
        listView.add(view1);
        listView.add(view2);
        listView.add(view3);

        ViewPager viewPager = findViewById(R.id.vp);
        MyPagerAdapter myPagerAdapter = new MyPagerAdapter(listView);
        viewPager.setAdapter(myPagerAdapter);
    }
}

运行效果图:

更多详见:2.6.3 ViewPager的简单使用 | 菜鸟教程 (runoob.com)

第7章 Activity(活动)

7.1 Activity的介绍

7.1.1 什么是Activity

定义:Activity是Android的四大组件之一。是用户操作的可视化界面;它为用户提供了一个完成操作指令的窗口。当我们创建完毕Activity之后,需要调用setContentView()方法来完成界面的显示;以此来为用户提供交互的入口。在Android App 中只要能看见的几乎都要依托于Activity,所以Activity是在开发中使用最频繁的一种组件。

7.1.2 Activity的声明

要使应用能使用Activity就需要在AndroidManifest.xml文件中进行声明及其设置特定属性,步骤为:

  1. 打开AndroidManifest.xml配置文件
  2. 在<manifest>标签下的<application>标签中添加<activity>元素并指明android:name=".类名"

注意:其中'.'代表<manifest>标签下的package的属性值(包名),或者name属性值也可写Activity的全类名

7.2 Activity的生命周期

7.2.1 Activity的生命周期介绍

在Android中会维持一个Activity Stack(Activity栈),当一个新的Activity创建时,它就会放到栈顶,这个Activity就处于运行状态。当再有一个新的Activity被创建后,会重新压人栈顶,而之前的Activity则会在这个新的Activity底下,就像枪梭压入子弹一样。而且之前的Activity就会进入后台。
一个Activity实质上有四种状态:

  1. 运行中(Running/Active):这时Activity位于栈顶,是可见的,并且可以用户交互。
  2. 暂停(Paused):当Activity失去焦点,不能跟用户交互了,但依然可见,就处于暂停状态。当一个新的非全屏的Activity或者一个透明的Activity放置在栈顶,Activity就处于暂停状态;这个时候Activity的各种数据还被保持着;只有在系统内存在极低的状态下,系统才会自动的去销毁Activity。
  3. 停止(Stoped):当一个Activity被另一个Activity完全覆盖,或者点击HOME键退入了后台,这时候Activity处于停止状态。这里有些是跟暂停状态相似的:这个时候Activity的各种数据还被保持着;当系统的别的地方需要用到内容时,系统会自动的去销毁Activity。
  4. 销毁(Detroyed):当我们点击返回键或者系统在内存不够用的情况下就会把Activity从栈里移除销毁,被系统回收,这时候,Activity处于销毁状态。

7.2.2 Activity的生命周期状态

  • onCreate:表示窗口正在被创建,比如加载layout布局文件啊(setContentView)。 所以我们可以在这个方法中,做一些初始化的操作。
  • onStart:表示Activity正在被启动,即将开始,此时的窗口已经可见了,但是还没有出现在前台,所以无法和用户进行交互。也就是说此时的窗口正处在 不可见—>可见 的过程中。
  • onRestart:表示窗口正在重新启动。在什么场景下会调用这个呢?比如:从A页面进入B页面,然后点击BACK键(或者自己的返回上一页的按钮)回到A页面,那么就会调用A页面的onRestart方法了。(当前也牵扯到A和B页面的其他生命周期方法的调用,这个我们后面再详细说明)。再比如:点击HOME键回到桌面,然后通过点击任务栏或者点击应用图标再次进入A页面,都可以触发调用这个方法
  • onResume:表示此时的窗口已经可见了,显示在前台并且进行活动了,我们也可以与窗口进行交互了。
  • onPause:表示窗口正在停止,这是我们可以做一些存储数据、或者停止动画等一些不太耗时的操作,因为会影响到下一个Activity的显示。onPause执行完成之后,新的Activity的onResume才会执行。
  • onStop:表示窗口即将停止,此时,可以做一些稍微重量级的回收工作,但是也不能太耗时哈。
  • onDestroy:表示窗口即将被销毁。这是Activity生命周期中的最后一步了。这里,我们可以做一些回收工作和最终的资源释放工作。

7.2.3 Activity的生命周期调用

  • 启动一个Activity
    onCreate()–>onStart()–>onResume()
  • 在Activity创建完成后,点击Home回调主界面时
    onPause()–>onStop()
  • 当点击Home键后,点击App回到Activity时
    onRestart()–>onStart()–>onResume()
  • 在原有的Activity的基础上打新的Activity时
    开启新的Activity,原Activity生命周期执行方法顺序为:onPause()–>onStop(),事实上跟点击home键是一样的。
    但是这里有点要注意的是如果新的Activity使用了透明主题,那么当前Activity不会回调onStop方法。同时我们发现新Activity(SecondActivity)生命周期方法是在原Activity的onPause方法执行完成后才可以被回调,这也就是前面我们为什么说在onPause方法不能操作耗时任务的原因了。
  • 回退到原来的Activity时不会调用不会调用onRestart()
    onCreate()–>onStart()–>onResume()
  • 点击Back键回退时,相当于退出了当前Activity,Activity将被销毁
    onPause()–>onStop()–>onDestroy()

7.3 Intent和IntentFilter

7.3.1 Intent的基本概念

Intent中文意思指”意图”,按照Android的设计理念,Android使用Intent来封装程序的”调用意图”,不管启动Activity(活动)、Service(服务)、BroadcastReceiver(广播接收器),Android都使用统一的Intent对象来封装这一”启动意图”。此外,Intent也是应用程序组件之间通信的重要媒介。在Android中指定的了具体是某个组件,那么就是显性意图;如果只是提出要求没有指定具体的某个人,在Android中即没有指定某个具体的组件,那么就是隐式意图;所有Intent页面跳转的方式又分为显示跳转和隐式跳转。

7.3.2 Intent和三大组件

Android应用程序包含三种重要组件:Activity(活动)、Service(服务)、BroadcastReceiver(广播接收器),应用程序采用一致的方式启动它们,都是依靠Intent来进行启动的,Intent中封装了程序要启动的意图。

下面是Intent启动不同组件的部分方法:

  • Activity组件:
    startActivity(Intent intent);
    startActivityForResult(Intent intent,int requestCode);
  • Service组件:
    startService(Intent intent);
    bindService(Intent intent,ServiceConnection conn,int flags);
  • BroadcastReceiver组件:
    sendBroadcast(Intent intent);
    sendOrderedBroadcast(Intent intent,String receiverPermission);

7.3.3 Intent的跳转方式

  1. Intent显式跳转页面:
    显示意图的跳转是Intent对象中包含了目标的class文件
    Intent intent = new Intent(this, SecondActivity.class);
    startActivity(intent);
    如果要传递数据也可以通过Intent对象使用putExtra方法来传递数据。
    这里的目标文件必须是要在AndroidManifest.xml里面注册。
  2. Intent隐式跳转页面:
    隐式Intent不会明确指出需要激活的目标组件,它被广泛地应用在不同应用程序之间传递消息。Android系统会使用IntentFilter匹配相应的组件,匹配的属性主要包括以下三个:
    action:表示Intent对象要完成的意图动作。
    data:指定数据的URL或者数据MIME类型他的值通常与Intent的action属性有关联。
    category:表示activity的动作的类别。

注意:在隐式跳转中,匹配的每一个属性可以罗列多个

7.3.3 IntentFilter的基本概念

IntentFilter的意思是“意图过滤器”当我们隐式的启动系统组件的时候,就会根据IntentFilter来匹配相应的组件进行启动。

7.3.4 IntentFilter的属性

Intent通过下面的属性来描述的某个意图:

  • action(动作): 用来表示意图的动作,如:查看,发邮件,打电话
  • category(类别): 用来表示动作的类别。
  • data(数据): 表示与动作要操作的数据。如:查看指定的联系人
  • type(数据类型): 对data类型的描述。
  • extras(附加信息): 附加信息。如:详细资料,一个文件,某事。
  • component(目标组件): 目标组件。

7.3.5 action(动作)属性

动作很大程度上决定了Intent如何构建,特别是数据和附加信息,就像一个方法名决定了参数和返回值一样,所以应该尽可能明确地指定动作,并紧密关联到其他的Intent字段,如Category和Data。

常用动作 
最常用的是Action_MAIN(作为初始的Activity启动,没有数据的输入输出) 

  1. ACTION_MAIN 作为一个主要的进入口,而并不期望去接受数据
  2. ACTION_VIEW 向用户去显示数据
  3. ACTION_ATTACH_DATA 别用于指定一些数据应该附属于一些其他的地方,例如,图片数据应该附属于联系人 
  4. ACTION_EDIT 访问已给的数据,提供明确的可编辑
  5. ACTION_GET_CONTENT 允许用户选择特殊种类的数据,并返回(特殊种类的数据:照一张相片或录一段音)
  6. ACTION_DIAL 拨打一个指定的号码,显示一个带有号码的用户界面,允许用户去启动呼叫
  7. ACTION_CALL 根据指定的数据执行一次呼叫(有缺陷,使用ACTION_DIAL) 
  8. ACTION_SEND 传递数据,被传送的数据没有指定,接收的action请求用户发数据
  9. ACTION_SENDTO 发送一条信息到指定的某人
  10. ACTION_ANSWER 处理一个打进电话呼叫
  11. ACTION_INSERT 插入一条空项目到已给的容器
  12. ACTION_DELETE 从容器中删除已给的数据
  13. ACTION_SEARCH 执行一次搜索
  14. ACTION_WEB_SEARCH 执行一次web搜索

上面的动作都是Intent对象引用才有实际意义的。 
setAction(String action) 用来设置Intent的动作,参数可以为常量 
getAction() 方法用来获取Intent动作名称 
上面的Action都是系统定义好的,具有一定意义的动作指向的动作。 
Intent的Action对象其实就是一个字符串常量,系统的Action对象是系统定义好的字符串常量,我们也可以自己定义自己的Action作为字符串常量。就像上面的例子使用到了自定义的Action字符串对象。

7.3.6 category(类别)属性

Intent的action、category属性都是普通的字符串,其中action表示Intent需要完成的一个抽象”动作”,而category则为action添加额外的类别信息,通常action和category一块使用。 
需要指出的是,一个Intent中只能包含一个action属性,但可以包含多个category属性。当程序创建Intent时,该Intent默认启动常量值为andorid.intent.category.DEFAULT的组件。这里的一个Intent中只能包含一个action属性,并不是Activity中xml的设置规范,而是你要跳转到的页面去,你只能设置一个Action的值。 
常用的Category: 

  1. CATEGORY_DEFAULT:Android系统中默认的执行方式,按照普通Activity的执行方式执行。  
  2. CATEGORY_HOME:设置该组件为Home Activity。 
  3. CATEGORY_PREFERENCE:设置该组件为Preference。  
  4. CATEGORY_LAUNCHER:设置为当前应用程序优先级最高的Activity,通常与ACTION_MAIN配合使用。  
  5. CATEGORY_BROWSABLE:设置该组件可以使用浏览器启动。  
  6. CATEGORY_GADGET:设置该组件可以内嵌到另外的Activity中。 

上面的类别都是Intent对象引用才有实际意义的。

7.3.7 data(数据)属性

Data数据用来向Action属性提供动作的数据。这里的Data不是Intent里面的数据,而是指明动作的具体数据,比如说动作是打电话,那么打给具体的某一个人,就用到了date里面的数据来指定。同样发邮件、或打开具体的某一个网址也是通过Data数据。
Data属性只接受Uri对象,Uri对象是统一资源标识符。对应概念不用太多理解,只需知道里面某些具体值的表现形式就可以了。
Uri其实就是相当于一个网址,如图所示:

网址只是Uri其中一种格式的字符串,要使用它还要把它解析后转化为Uri类型。
为Intent对象添加Data数据,代码:
intent.setData(Uri.parse(“http://www.baidu.com“));
这里的Uri的有两个没显示的属性:port的值是8080,path的值是/index
通过下面三句代码就可以跳转到百度主页面:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);

7.3.8 type(数据类型)属性

与Data有关的,这个不是Intent的数据类型,是Intent的Action的Data数据的类型。 
比如: 
{“.mp3”, “audio/x-mpeg”}, 
{“.mp4”, “video/mp4”}, 
{“.gif”, “image/gif”}, 
{“.rmvb”, “audio/x-pn-realaudio”}, 
这里只是做几个简单的示例介绍,如果是打开gif格式的数据文件,设置type=“image/gif”

7.3.9 extras(附加信息)属性

Extras属性主要用于传递目标组件所需要的额外的数据。这个数据是可以通过Intent来保存的数据和Intent对象来获取数据。

通过putExtras()方法设置。保存数据
通过putExtras()方法设置。获取数据
通常我们使用Intent来直接传递Bundle对象,但也可以传递其他系统内置的一些参数。 
如果要传递是是对象,那么对象必须实现序列化。

7.3.10 component(目标组件)属性

这个属性用得比较少,最好不用。如果是显示调用直接指定目标类的class文件名就可以使用了。

7.3.11 Intent的Action属性的部分常量值

  1. ACTION_MAIN:Android Application的入口,每个Android应用必须且只能包含一个此类型的Action声明。
  2. ACTION_VIEW:系统根据不同的Data类型,通过已注册的对应Application显示数据。
  3. ACTION_EDIT:系统根据不同的Data类型,通过已注册的对应Application编辑示数据。
  4. ACTION_DIAL:系统默打开拨号程序,如果Data中设置电话号码,则拨号框中会显示此号码。
  5. ACTION_CALL:直接呼叫Data中所带的号码。
  6. ACTION_ANSWER:接听来电。
  7. ACTION_SEND:由用户指定发送方式进行数据发送操作。
  8. ACTION_SENDTO:系统根据不同的Data类型,通过已注册的对应Application进行数据发送操作。
  9. ACTION_BOOT_COMPLETED:Android系统在启动完毕后发出带有此Action的广播(Broadcast)。
  10. ACTION_TIME_CHANGED:Android系统的时间发生改变后发出带有此Action的广播(Broadcast)。
  11. ACTION_PACKAGE_ADDED:Android系统安装了新的App之后发出带有此Action的广播(Broadcast)。
  12. ACTION_PACKAGE_CHANGED:Android系统中已存在的App发生改变之后(如更新)发出带有此Action的广播(Broadcast)。
  13. ACTION_PACKAGE_REMOVED:Android系统卸载App之后发出带有此Action的广播(Broadcast)。

7.3.12 Intent的Category属性的部分常量值

  1. CATEGORY_DEFAULT:Android系统中默认的执行方式,按照普通Activity的执行方式执行。
  2. CATEGORY_PREFERENCE:设置该组件为Preference。
  3. CATEGORY_LAUNCHER:设置为当前应用程序优先级最高的Activity,通常与ACTION_MAIN配合使用。
  4. CATEGORY_BROWSABLE:设置该组件可以使用浏览器启动。
  5. CATEGORY_GADGET:设置该组件可以内嵌到另外的Activity中。

7.3.13 Intent的Data属性的部分常量值

  1. tel:号码数据格式,后跟电话号码。 
  2. mailto:邮件数据格式,后跟邮件收件人地址。
  3. smsto:短息数据格式,后跟短信接收号码。
  4. content:内容数据格式,后跟需要读取的内容。 
  5. file:文件数据格式,后跟文件路径。
  6. market:search?q=pname:pkgname:市场数据格式,在Google Market里搜索包名为pkgname的应用。
  7. geo:latitude, longitude:经纬数据格式,在地图上显示经纬度所指定的位置。

7.3.14 Intent的Extras属性的部分常量值

  1. EXTRA_BCC:存放邮件密送人地址的字符串数组。
  2. EXTRA_CC:存放邮件抄送人地址的字符串数组。
  3. EXTRA_EMAIL:存放邮件地址的字符串数组。
  4. EXTRA_SUBJECT:存放邮件主题字符串。
  5. EXTRA_TEXT:存放邮件内容。
  6. EXTRA_KEY_EVENT:以KeyEvent对象方式存放触发Intent的按键。
  7. EXTRA_PHONE_NUMBER:存放调用ACTION_CALL时的电话号码。

7.3.15 Intent的MimeType属性的部分常量值

{“.3gp”, “video/3gpp”},
{“.apk”, “application/vnd.android.package-archive”},
{“.asf”, “video/x-ms-asf”},
{“.avi”, “video/x-msvideo”},
{“.bin”, “application/octet-stream”},
{“.bmp”, “image/bmp”},
{“.c”, “text/plain”},
{“.class”, “application/octet-stream”},
{“.conf”, “text/plain”},
{“.cpp”, “text/plain”},
{“.doc”, “application/msword”},
{“.docx”, “application/vnd.openxmlformats-officedocument.wordprocessingml.document”},
{“.xls”, “application/vnd.ms-excel”},
{“.xlsx”, “application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”},
{“.exe”, “application/octet-stream”},
{“.gif”, “image/gif”},
{“.gtar”, “application/x-gtar”},
{“.gz”, “application/x-gzip”},
{“.h”, “text/plain”},
{“.htm”, “text/html”},
{“.html”, “text/html”},
{“.jar”, “application/Java-archive”},
{“.java”, “text/plain”},
{“.jpeg”, “image/jpeg”},
{“.jpg”, “image/jpeg”},
{“.js”, “application/x-JavaScript”},
{“.log”, “text/plain”},
{“.m3u”, “audio/x-mpegurl”},
{“.m4a”, “audio/mp4a-latm”},
{“.m4b”, “audio/mp4a-latm”},
{“.m4p”, “audio/mp4a-latm”},
{“.m4u”, “video/vnd.mpegurl”},
{“.m4v”, “video/x-m4v”},
{“.mov”, “video/quicktime”},
{“.mp2”, “audio/x-mpeg”},
{“.mp3”, “audio/x-mpeg”},
{“.mp4”, “video/mp4”},
{“.mpc”, “application/vnd.mpohun.certificate”},
{“.mpe”, “video/mpeg”},
{“.mpeg”, “video/mpeg”},
{“.mpg”, “video/mpeg”},
{“.mpg4”, “video/mp4”},
{“.mpga”, “audio/mpeg”},
{“.msg”, “application/vnd.ms-outlook”},
{“.ogg”, “audio/ogg”},
{“.pdf”, “application/pdf”},
{“.png”, “image/png”},
{“.pps”, “application/vnd.ms-powerpoint”},
{“.ppt”, “application/vnd.ms-powerpoint”},
{“.pptx”, “application/vnd.openxmlformats-officedocument.presentationml.presentation”},
{“.prop”, “text/plain”},
{“.rc”, “text/plain”},
{“.rmvb”, “audio/x-pn-realaudio”},
{“.rtf”, “application/rtf”},
{“.sh”, “text/plain”},
{“.tar”, “application/x-tar”},
{“.tgz”, “application/x-compressed”},
{“.txt”, “text/plain”},
{“.wav”, “audio/x-wav”},
{“.wma”, “audio/x-ms-wma”},
{“.wmv”, “audio/x-ms-wmv”},
{“.wps”, “application/vnd.ms-works”},
{“.xml”, “text/plain”},
{“.z”, “application/x-compress”},
{“.zip”, “application/x-zip-compressed”},

7.3部分的内容为转载:CSDN博主「峥嵘life」的原创文章,原文链接:https://blog.csdn.net/wenzhi20102321/article/details/52876648

7.4 Activity之间的跳转

7.4.1 Activity之间的数据传递

Android提供的Intent可以在界面跳转时传递参数。使用Intent传递参数有两种方式,具体如下:

  1. 使用Intent的putExtra()方法传递数据
    putExtra()方法的参数为<key,value>的形式,并且Android系统提供了多个重载的putExtra()方法供不同类型的参数传递
    获取参数:首先通过getIntent()方法获取到Intent对象,然后通过Intent对象的getXxxExtra()方法获取到参数
  2. 使用Bundle类传递数据
    此方法是先将参数通过Bundle对象的putXxx()方法保存在Bundle对象中,同样是<key,value>的形式,然后再通过Intent的putExtras()方法保存到Intent对象中
    获取参数:首先通过getIntent()方法获取到Intent对象,然后通过Intent对象的getExtras()方法获取到Bundle对象,然后通过Intent对象的getXxx()方法获取到参数
  3. 如何传递对象(让对象具有被传递的功能)
    第一种方法:让对象类实现 Serializable 接口,同样通过以上两种方式传递,在另一个Activity中使用 getSerializableExtra("key")获取到对象,须强转为目标对象,此方法基于JVM虚拟机,不兼容Android虚拟机
    第二种方法:让对象类实现 Parcelable 接口,同样通过以上两种方式传递,Android推荐使用,在另一个Activity中使用 getParcelableExtra("key")获取到对象,不用强转为目标对象(使用了泛型),此方法兼容Android虚拟机

7.4.2 Activity之间的数据回传

当我们从MainActivity界面跳转到SecondActivity界面时,在SecondActivity界面进行操作后,当关闭SecondActivity界面时,想要从该界面返回一些数据到MainActivity界面。此时Android系统为我们提供了一些方法用于Activity之间数据的回传。具体如下:

  1. startActivityForResult(intent, REQUEST_CODE)方法:
    该方法跟startActivity()方法一样,也是用于启动活动的,但这个方法期望在活动销毁的时候能够返回一个结果给上一个活动。
    REQUEST_CODE为请求码,用于在之后的回调中判断数据的来源。
  2. setResult(RESULT_CODE, intent)方法:
    该方法是专门用于向上一个活动返回数据的。
    RESULT_CODE为返回码,用于向上一个活动返回处理结果,该结果可用于在之后的回调中判断数据的来源,一般只使用RESULT_OK或RESULT_CANCELED这两个值;
  3. onActivityResult()方法: 
    由于我们是使用startActivityForResult()方法来启动的Activity,在Activity被销毁之后会回调上一个活动的onActivityResult()方法,因此我们需要在MainActivity中重写这个方法来得到返回数据。

注意1:若使用此方法进行Activity之间数据的传递与回传,则必须同时使用以上3个方法,其中第3个方法是一个用于重写的方法

注意2:新手在使用Intent启动其他的Activity时,需要在AndroidManifest.xml清单文件中进行声明,其中android:exported 属性表示是否支持其它应用调用/启动该组件。

activity_main.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/et_name"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:hint="姓名"/>

    <EditText
        android:id="@+id/et_score"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:hint="分数"/>

    <Button
        android:id="@+id/btn_submit"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="提交数据"/>

    <TextView
        android:id="@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="150dp"
        android:hint="返回的结果"/>

</LinearLayout>

MainActivity.java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private EditText et_name,et_score;
    private Button btn_submit;
    private TextView tv_result;
    public static final int REQUEST_CODE = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        et_name = findViewById(R.id.et_name);
        et_score = findViewById(R.id.et_score);
        btn_submit  = findViewById(R.id.btn_submit);
        tv_result = findViewById(R.id.tv_result);

        btn_submit.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        String name = et_name.getText().toString();
        Double score = Double.valueOf(et_score.getText().toString());
        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra("name",name);
        intent.putExtra("score", score);
        startActivityForResult(intent, REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE) {
            if (resultCode == SecondActivity.RESULT_CODE) {
                tv_result.setText("返回的结果为:"+data.getStringExtra("result"));
            }
        }
    }
}

activity_second.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".SecondActivity">

    <TextView
        android:id="@+id/tv_data"
        android:layout_width="match_parent"
        android:layout_height="150dp"/>

    <EditText
        android:id="@+id/et_result"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:hint="返回的数据"/>

    <Button
        android:id="@+id/btn_return"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="返回数据"/>

</LinearLayout>

SecondActivity.java代码:

public class SecondActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView tv_data;
    private EditText et_result;
    private Button btn_return;
    public static final int RESULT_CODE = 2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        tv_data = findViewById(R.id.tv_data);
        et_result = findViewById(R.id.et_result);
        btn_return = findViewById(R.id.btn_return);

        Intent intent = getIntent();
        StringBuffer data = new StringBuffer();
        data.append("姓名:" + intent.getStringExtra("name"));
        data.append("\n分数:" + intent.getDoubleExtra("score", 0));
        tv_data.setText(data);

        btn_return.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Intent intent = new Intent(this, SecondActivity.class);
        intent.putExtra("result", et_result.getText().toString());
        setResult(RESULT_CODE, intent);
        finish(); //用于结束一个Activity的生命周期,finish会调用到onDestory()方法。
    }
}

运行效果:

第8章 Service(服务)

8.1 线程的相关概念

8.1.1 相关概念

  • 程序:为了完成特定任务,用某种语言编写的一组指令集合(一组静态代码)
  • 进程:进程是系统调度与资源分配(CPU、内存)的基本单位,它是程序执行时的一个实例。操作系统会为每个进程分配一段内存空间!程序的依次动态执行,经历代码的加载,执行, 执行完毕的完整过程!
  • 线程:线程是程序执行时的最小单位,是进程的一个执行流,CPU调度和分派的基本单位。每个进程可能有多个线程,线程需要放在一个进程中才能执行,线程由程序负责管理,而进程则由系统进行调度!
  • 多线程的理解:并行执行多条指令,将CPU时间片按照调度算法分配给各个 线程,实际上是分时执行的,只是这个切换的时间很短,用户感觉到"同时"而已!

8.1.2 线程的生命周期

8.1.3  创建线程的三种方式

  • 继承Thread类,并重写run方法
  • 实现Callable接口,并重写call()方法
  • 实现Callable接口,并重写call()方法

详见1:https://www.jianshu.com/p/0977349d20db

详见2:https://blog.csdn.net/chaochao2113/article/details/118861041

8.2 Service与Thread线程的区别

其实他们两者并没有太大的关系,不过有很多朋友经常把这两个混淆了! Thread是线程,程序执行的最小单元,分配CPU的基本单位! 而Service则是Android提供一个允许长时间留驻后台的一个组件,最常见的用法就是做轮询操作!或者想在后台做一些事情,比如后台下载更新、音乐播放等! 记得别把这两个概念混淆! 

8.3 Service的生命周期

8.3.1 Service的生命周期图

8.3.2 Service的相关方法详解

  • onCreate():当Service第一次被创建后立即回调该方法,该方法在整个生命周期 中只会调用一次!
  • onDestory():当Service被关闭时会回调该方法,该方法只会回调一次!
  • onStartCommand(intent,flag,startId):早期版本是onStart(intent,startId), 当客户端调用startService(Intent)方法时会回调,可多次调用StartService方法, 但不会再创建新的Service对象,而是继续复用前面产生的Service对象,但会继续回调 onStartCommand()方法!
  • IBinder onOnbind(intent):该方法是Service都必须实现的方法,该方法会返回一个 IBinder对象,app通过该对象与Service组件进行通信!
  • onUnbind(intent):当该Service上绑定的所有客户端都断开时会回调该方法!

8.3.3 Service的两种使用方式

  • StartService启动Service
    首次启动会创建一个Service实例,依次调用onCreate()和onStartCommand()方法,此时Service 进入运行状态,如果再次调用StartService启动Service,将不会再创建新的Service对象, 系统会直接复用前面创建的Service对象,调用它的onStartCommand()方法!
    但这样的Service与它的调用者无必然的联系,就是说当调用者结束了自己的生命周期, 但是只要不调用stopService,那么Service还是会继续运行的!
    无论启动了多少次Service,只需调用一次StopService即可停掉Service

  • BindService启动Service
    当首次使用bindService绑定一个Service时,系统会实例化一个Service实例,并调用其onCreate()和onBind()方法,然后调用者就可以通过IBinder和Service进行交互了,此后如果再次使用bindService绑定Service,系统不会创建新的Sevice实例,也不会再调用onBind()方法,只会直接把IBinder对象传递给其他后来增加的客户端!
    如果我们解除与服务的绑定,只需调用unbindService(),此时onUnbind和onDestory方法将会被调用!这是一个客户端的情况,假如是多个客户端绑定同一个Service的话,情况如下 当一个客户完成和service之间的互动后,它调用 unbindService() 方法来解除绑定。当所有的客户端都和service解除绑定后,系统会销毁service。(除非service也被startService()方法开启)
    另外,和上面那张情况不同,bindService模式下的Service是与调用者相互关联的,可以理解为 "一条绳子上的蚂蚱",要死一起死,在bindService后,一旦调用者销毁,那么Service也立即终止!
    通过BindService调用Service时调用的Context的bindService的解析 bindService(Intent Service,ServiceConnection conn,int flags)
    service:通过该intent指定要启动的Service
    conn:ServiceConnection对象,用户监听访问者与Service间的连接情况, 连接成功回调该对象中的onServiceConnected(ComponentName,IBinder)方法; 如果Service所在的宿主由于异常终止或者其他原因终止,导致Service与访问者间断开 连接时调用onServiceDisconnected(CompanentName)方法,主动通过unBindService() 方法断开并不会调用上述方法!
    flags:指定绑定时是否自动创建Service(如果Service还未创建), 参数可以是0(不自动创建),BIND_AUTO_CREATE(自动创建)

  • StartService启动Service后bindService绑定
    如果Service已经由某个客户端通过StartService()启动,接下来由其他客户端 再调用bindService()绑定到该Service后调用unbindService()解除绑定最后在 调用bindService()绑定到Service的话,此时所触发的生命周期方法如下:
    onCreate( )->onStartCommand( )->onBind( )->onUnbind( )->onRebind( )
    PS:前提是:onUnbind()方法返回true!!! 这里或许部分读者有疑惑了,调用了unbindService后Service不是应该调用 onDistory()方法么!其实这是因为这个Service是由我们的StartService来启动的 ,所以你调用onUnbind()方法取消绑定,Service也是不会终止的!
    得出的结论: 假如我们使用bindService来绑定一个启动的Service,注意是已经启动的Service!!! 系统只是将Service的内部IBinder对象传递给Activity,并不会将Service的生命周期 与Activity绑定,因此调用unBindService( )方法取消绑定时,Service也不会被销毁!

StartService启动与BindService启动的区别:
①启动方式:前者startService、后者bindService;
②和Activity联系:前者Acitivty销毁,service仍然继续运行,后者跟随Acitivity一起销毁;
③方法调用和数据交换:前者不可以,后者可以;
④回调的周期函数:前者是onStartCommand,后者是onBind
⑤结束方式:前者是stopService或者stopSelf,后者是unbindService。

生命周期的不同:
startService方式:调用startService方法->onCreate->onStartCommand->Servcie运行->调用stopService->Service停止->onDestroy->Service销毁;
bindService方式:调用bindServcie方法->onCreate->onBind->绑定Service->调用unbindService方法->解绑Service->onUnBind->onDestroy->Service销毁。

第9章 BroadcastReceiver(广播接收器) 

9.1 广播与广播接收器

9.1.1 什么是广播

广播(Broadcast)是 Android 系统中应用程序间通信的手段。
当有特定事件发生时,例如有来电、有短信、电池电量变化等事件发生时,Android 系统都会产生特定的 Intent 对象并且自动进行广播,而针对特定事件注册的 BroadcastReceiver 会接收到这些广播,并获取 Intent 对象中的数据进行处理。
在广播 Intent 对象时可以指定用户权限,以此限制仅有获得了相应权限的 BroadcastReceiver 才能接收并处理对应的广播。

9.1.2 什么是广播接收器 

        BroadcastReceiver翻译为广播接收者,它是一个系统全局的监听器,用于监听系统全局的Broadcast消息,所以它可以很方便的进行系统组件之间的通信。

  BroadcastReceiver虽然是一个监听器,但是它和之前用到的OnXxxListener不同,那些只是程序级别的监听器,运行在指定程序的所在进程中,当程序退出的时候,OnXxxListener监听器也就随之关闭了,但是BroadcastReceiver属于系统级的监听器,它拥有自己的进程,只要存在与之匹配的Broadcast被以Intent的形式发送出来,BroadcastReceiver就会被激活。

  虽然同属Android的四大组件,BroadcastReceiver也有自己独立的声明周期,但是和Activity、Service又不同。当在系统注册一个BroadcastReceiver之后,每次系统以一个Intent的形式发布Broadcast的时候,系统都会创建与之对应的BroadcastReceiver广播接收者实例,并自动触发它的onReceive()方法,当onReceive()方法被执行完成之后,BroadcastReceiver的实例就会被销毁。虽然它独自享用一个单独的进程,但也不是没有限制的,如果BroadcastReceiver.onReceive()方法不能在10秒内执行完成,Android系统就会认为该BroadcastReceiver对象无响应,然后弹出ANR(Application No Response)对话框,所以不要在BroadcastReceiver.onReceive()方法内执行一些耗时的操作。

  如果需要根据广播内容完成一些耗时的操作,一般考虑通过Intent启动一个Service来完成该操作,而不应该在BroadcastReceiver中开启一个新线程完成耗时的操作,因为BroadcastReceiver本身的生命周期很短,可能出现的情况是子线程还没有结束,BroadcastReceiver就已经退出的情况,而如果BroadcastReceiver所在的进程结束了,该线程就会被标记为一个空线程,根据Android的内存管理策略,在系统内存紧张的时候,会按照优先级,结束优先级低的线程,而空线程无异是优先级最低的,这样就可能导致BroadcastReceiver启动的子线程不能执行完成。

9.2 发送广播的类型

9.2.1 标准广播

它是完全异步的,也就是说,在逻辑上,当一个Broadcast被发出之后,所有的与之匹配的BroadcastReceiver都同时接收到Broadcast。优点是传递效率比较高,但是也有缺点,就是一个BroadcastReceiver不能影响其他响应这条Broadcast的BroadcastReceiver。

  • sendBroadcast():发送普通广播。

9.2.2 有序广播

它是同步执行的,就是说有序广播的接收器将会按照预先声明的优先级(-1000~1000)依次接受Broadcast,优先级越高,越先被执行。因为是顺序执行,所有优先级高的接收器,可以把执行结果传入下一个接收器中,也可以终止Broadcast的传播(通过abortBroadcast()方法),一旦Broadcast的传播被终止,优先级低于它的接收器就不会再接收到这条Broadcast了。

  • sendOrderedBroadcast():发送有序广播。

注:有序广播能被设置了高优先级的广播接收者的 abortBroadcast() 截断。

9.3 如何使用BroadcastReceiver

        BroadcastReceiver本质上还是一个监听器,所以使用BroadcastReceiver的方法也是非常简单,只需要继承BroadcastReceiver,在其中重写onReceive(Context context,Intent intent)即可。一旦实现了BroadcastReceiver,并部署到系统中后,就可以在系统的任何位置,通过sendBroadcast、sendOrderedBroadcast方法发送Broadcast给这个BroadcastReceiver。

        但是仅仅继承BroadcastReceiver和实现onReceive()方法是不够的,同为Android系统组件,它也必须在Android系统中注册,注册一个BroadcastReceiver有两种方式:

9.3.1 静态注册(不建议使用)

使用清单文件AndroidManifest.xml注册,在<application/>节点中,使用<receiver/>节点注册,并用android:name属性中指定注册的BroadcastReceiver对象,一般还会通过<Intent-filter/>指定<action/>和<category/>,并在<Intent-filter/>节点中通过android:priority属性设置BroadcastReceiver的优先级,在-1000~1000范围内,数值越到优先级越高。

        <receiver android:name=".MyBroadcastReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="com.example.mybroadcastreceiver.staticReceiver"/>
            </intent-filter>
        </receiver>

广播接收者接收Android 8.0及以上的静态注册的广播:

intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);

intent.setComponent(new ComponentName("com.example.mybroadcastreceiver","com.example.mybroadcastreceiver.MyBroadcastReceiver"));
//第一个参数为广播接收者的包名,第二个参数为广播接收者的类名

intent.setPackage("com.example.mybroadcastreceiver");
//参数为广播接收者的报名

9.3.2 动态注册

就是在Java代码中指定 IntentFilter,然后添加不同的 Action 即可,想监听什么广播就写什么 Action,最后通过registerReceiver(BroadcastReceiver receiver, IntentFilter filter) 方法进行注册,另外动态注册的广播,一定要调用unregisterReceiver() 方法撤销广 播的注册。

        MyBroadcastReceiver myBroadcastReceiver = new MyBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.mybroadcastreceiver.dynamicReceiver");
//参数为标识名,用于发送广播时识的别时
        registerReceiver(myBroadcastReceiver, intentFilter);

9.3.3 广播接收总结

  • 广播接收的特点:
    ①广播接收器接收到相应广播后,会自动回调 onReceive() 方法
    ②一般情况下,onReceive方法会涉及 与 其他组件之间的交互,如发送Notification、启动Service
    ③默认情况下,广播接收器运行在 UI 线程,因此,onReceive()方法不能执行耗时操作(10s的时间),否则将导致ANR
  • 实现自定义的广播接收:
    ①继承BroadcastReceiver类,实现自定义的广播接收者,实现onReceive(Context context, Intent intent)方法用于接受广播
    ②注册BroadcastReceiver,静态注册 / 动态注册
    sendBroadcast(Intent intent); 发送广播
    ④在onReceive(Context context, Intent intent)中处理逻辑业务; 接收广播

activity_main.xml代码: 

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_static"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="静态注册广播"
        android:textSize="30sp"
        app:layout_constraintBottom_toTopOf="@+id/btn_dynamic"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_dynamic"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="动态注册广播"
        android:textSize="30sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_static" />

</androidx.constraintlayout.widget.ConstraintLayout>

MyBroadcastReceiver.java代码:

public class MyBroadcastReceiver extends BroadcastReceiver {
    private static final String TAG = "zhumeng";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e(TAG, "接收到广播消息‘"+intent.getStringExtra("msg")+"’");
    }
}

MainActivity.java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private MyBroadcastReceiver myBroadcastReceiver;

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

        Button btn_static = findViewById(R.id.btn_static);
        Button btn_dynamic = findViewById(R.id.btn_dynamic);

        btn_static.setOnClickListener(this);
        btn_dynamic.setOnClickListener(this);
        
        initReceiver();
    }

    private void initReceiver() { //动态注册广播接收者
        myBroadcastReceiver = new MyBroadcastReceiver();
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.mybroadcastreceiver.dynamicReceiver");
        registerReceiver(myBroadcastReceiver, intentFilter);
    }

    @Override
    public void onClick(View view) {
        Log.e("zhumeng", "单击事件发生");
        Intent intent;
        switch (view.getId()){
            case R.id.btn_static:
                intent = new Intent();
                intent.setAction("com.example.mybroadcastreceiver.staticReceiver");
//                intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
//                intent.setComponent(new ComponentName("com.example.mybroadcastreceiver","com.example.mybroadcastreceiver.MyBroadcastReceiver"));
                intent.setPackage("com.example.mybroadcastreceiver");
                intent.putExtra("msg","静态注册的广播");
                sendBroadcast(intent);
                break;
            case R.id.btn_dynamic:
                intent = new Intent();
                intent.setAction("com.example.mybroadcastreceiver.dynamicReceiver");
                intent.putExtra("msg","动态注册的广播");
                sendBroadcast(intent);
                break;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(myBroadcastReceiver);
    }
}

运行效果图:

第10章 Fragment(碎片)

10.1 Fragment是什么

10.1.1 Fragment的历史

 Fragment是Android3.0后引入的一个新的API,他出现的初衷是为了适应大屏幕的平板电脑, 当然现在他仍然是平板APP UI设计的宠儿。

10.1.2 Fragment是什么

Fragment(碎片)是一种嵌入在Activity中的UI片段,它可以用来描述Activity中的一部分布局。如果Activity界面布局中的控件比较多比较复杂,那么Activity管理起来就很麻烦,我们可以使用Fragment把屏幕划分成几个片段,进行模块化的管理,从而使程序更加合理和充分地利用屏幕的空间。
一个Activity中可以包含多个Fragment,一个Fragment也可以在多个Activity中使用,如果在Activity中有多个相同的业务模块,则可以复用Fragment。
想想,如果一个很大的界面,我们就一个布局,写起界面来会有多麻烦,而且如果组件多的话是管理起来也很麻烦!而使用Fragment我们可以把屏幕划分成几块,然后进行分组,进行一个模块化的管理!从而可以更加方便的在运行过程中动态地更新Activity的用户界面!另外Fragment并不能单独使用,他需要嵌套在Activity中使用,尽管他拥有自己的生命周期,但是还是会受到宿主Activity的生命周期的影响。比如,当Activity暂停时,其中的所有片段也会暂停;当Activity被销毁时,所有片段也会被销毁。 不过,当Activity 正在运行(处于已恢复生命周期状态)时,您可以独立操纵每个片段,如添加或移除它们。 当您执行此类片段事务时,您也可以将其添加到由Activity管理的返回栈,Activity中的每个返回栈条目都是一条已发生片段事务的记录。返回栈让用户可以通过按返回按钮撤消片段事务(后退)。
下面是一个典型的示例演示如何让两个由碎片定义的UI模块,在为平板设计的活动中组合,在为手持设备设计的活动中分离。

当运行在在平板尺寸的设备上,这个应用程序可以在活动A中嵌入两个碎片。在手机设备屏幕上,由于没有足够的空间,活动A仅包含有文章列表的碎片,当用户点击文章时,启动包含第二个碎片的活动B来阅读文章。 

10.2 Fragment的生命周期

10.2.1 Fragment的生命周期简介

Fragment必须是依存与Activity而存在的,因此Activity的生命周期会直接影响到Fragment的生命周期。Fragment状态与Activity类似,也存在如下4种状态:

  • 运行:当前Fmgment位于前台,用户可见,可以获得焦点。
  • 暂停:其他Activity位于前台,该Fragment依然可见,只是不能获得焦点。
  • 停止:该Fragment不可见,失去焦点。
  • 销毁:该Fragment被完全删除,或该Fragment所在的Activity被结束。

10.2.2 Fragment的生命周期状态 

Fragment的生命周期看起来和Activity的生命周期类似,但是多了一些,因为Fragment的生命周期是和其关联的Activity有关。

(1)onAttach(Context context):在Fragment和Activity关联上的时候调用,且仅调用一次。在该回调中我们可以将context转化为Activity保存下来,从而避免后期频繁调用getAtivity()获取Activity的局面,避免了在某些情况下getAtivity()为空的异常(Activity和Fragment分离的情况下)。同时也可以在该回调中将传入的Arguments提取并解析,在这里强烈推荐通过setArguments给Fragment传参数,因为在应用被系统回收时Fragment不会保存相关属性,具体之后会讲解。

(2)onCreate(Bundle savedInstanceState):在最初创建Fragment的时候会调用,和Activity的onCreate类似。

(3)View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState):在准备绘制Fragment界面时调用,返回值为Fragment要绘制布局的根视图,当然也可以返回null。注意使用inflater构建View时一定要将attachToRoot指明false,因为Fragment会自动将视图添加到container中,attachToRoot为true会重复添加报错。onCreateView并不是一定会被调用,当添加的是没有界面的Fragment就不会调用,比如调用FragmentTransaction的add(Fragment fragment, String tag)方法。

(4)onActivityCreated(Bundle savedInstanceState):在Activity的onCreated执行完时会调用。

(5)onStart():Fragment对用户可见的时候调用,前提是Activity已经started。

(6)onResume():Fragment和用户之前可交互时会调用,前提是Activity已经resumed。

(7)onPause():Fragment和用户之前不可交互时会调用。

(8)onStop():Fragment不可见时会调用。

(9)onDestroyView():在移除Fragment相关视图层级时调用。

(10)onDestroy():最终清楚Fragment状态时会调用。

(11)onDetach():Fragment和Activity解除关联时调用。

10.2.3 Fragment的生命周期调用

1)创建Fragment

onAttach() —> onCreate() —> onCreateView() —> onActivityCreated() —> onStart() —> onResume()

2)按下Home键回到桌面 / 锁屏

onPause() —> onStop()

3)从桌面回到Fragment / 解锁

onStart() —> onResume()

4)切换到其他Fragment

onPause() —> onStop() —> onDestroyView()

5)切换回本身的Fragment

onCreateView() —> onActivityCreated() —> onStart() —> onResume()

6) 按下Back键退出

onPause() —> onStop() —> onDestroyView() —> onDestroy() —> onDetach()

注意:Fragment必须依赖于Activity,理解为Activity的寄生虫

详见1:Fragment生命周期 - 第壹时间 - 博客园 (cnblogs.com) 

详见2:Fragment的生命周期_杭州小白的博客-CSDN博客_fragment生命周期

10.3 Fragment用法

10.3.1 Fragment的静态加载

实现步骤:

  • Step 1:自定义一个Fragment类,需要继承Fragment或者他的子类,重写onCreateView()方法,在该方法中调用inflater.inflate()方法加载Fragment的布局文件并实例化view对象,接着返回加载的view对象
    public class BlankFragment1 extends Fragment {
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.fragment_blank1, container, false);
            Button btn = view.findViewById(R.id.btn);
            TextView tv = view.findViewById(R.id.tv);
            btn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    tv.setText("Yes,I am,and you!");
                }
            });
            return view;
        }
    }
  • Step 2:若第1步在Java目录下New Fragment类的话就会自动生成Fragment的布局(否则需要自己定义),就是fragment显示内容的
    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".BlankFragment1"
        android:orientation="vertical">
    
        <TextView
            android:id="@+id/tv"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/hello_blank_fragment"
            android:textSize="30sp"/>
    
        <Button
            android:id="@+id/btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="获取回答" />
    
    </LinearLayout>
  • Step 3:在需要加载Fragment的Activity对应的布局文件中添加fragment的标签,必需写name属性且是全限定类名哦,就是要包含Fragment的包名,还需要有一个id属性
    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <fragment
            android:id="@+id/fragment1"
            android:name="com.example.fragmentbase.BlankFragment1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    
    </LinearLayout>
  • Step 4:Activity在onCreate( )方法中调用setContentView()加载布局文件即可

运行效果图: 

注:若要添加多个Fragment就直接创建多个继承Fragment的类,然后在activity_main.xml中通过name属性引用即可

10.3.2 Fragment的动态加载

实现步骤:

  • Step 1:自定义一个Fragment类,需要继承Fragment或者他的子类,重写onCreateView()方法,在该方法中调用inflater.inflate()方法加载Fragment的布局文件并实例化view对象,接着返回加载的view对象
  • Step 2:若第1步在Java目录下New Fragment类的话就会自动生成Fragment的布局(否则需要自己定义),就是fragment显示内容的
  • Step 3:在需要加载Fragment的Activity对应的布局文件中添加fragment的标签,必需写name属性且是全限定类名哦,就是要包含Fragment的包名,还需要有一个id属性
  • Step 4:Activity在onCreate( )方法中调用setContentView()加载布局文件即可
  • Step 5:通过getFragmentManager()获得FragmentManager对象fragmentManager;
  • Step 6:通过fragmentManager.beginTransaction()获得FragmentTransaction对象fragmentTransaction;
  • Step 7:调用fragmentTransaction的add()方法或者replace()方法加载Fragment;
    第一个参数:要传入的布局容器
    第二个参数:要加载的fragment对象
  • Step 8:在前面的基础上还需调用commit()方法提交事务。当然还有其他的方法如remove()等

注意:若点击back键按照添加过程退出时需要在commit()方法之前执行transation.addToBackStack(null);

MainActivity.java代码:其中BlankFragment1类和ItemFragment1类由AS自动生成

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn1 = findViewById(R.id.btn1);
        Button btn2 = findViewById(R.id.btn2);
        btn1.setOnClickListener(this);
        btn2.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.btn1:
                replaceFragment(new BlankFragment1());
                break;
            case R.id.btn2:
                replaceFragment(new ItemFragment1());
                break;
        }
    }

    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fragment1, fragment);

        transaction.addToBackStack(null);

        transaction.commit();
    }
}

运行效果图: 

10.4 Fragment的通信

10.4.1 Fragment与activity通信——原生Bundle

MainActivity.java代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn1 = findViewById(R.id.btn1);
        Button btn2 = findViewById(R.id.btn2);
        btn1.setOnClickListener(this);
        btn2.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.btn1:
                BlankFragment blankFragment = new BlankFragment();
                Bundle bunlsle = new Bundle();
                bunlsle.putString("name", "张三");
                bunlsle.putInt("age", 18);
                blankFragment.setArguments(bunlsle);
                replaceFragment(blankFragment);
                break;
            case R.id.btn2:
                replaceFragment(new ItemFragment());
                break;
        }
    }

    private void replaceFragment(Fragment fragment) {
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fragment1, fragment);

        transaction.addToBackStack(null);

        transaction.commit();
    }
}

获取参数:在BlankFragment类中任何位置都可以通过Bundle bundle = getArguments()的bundle获取到通过blankFragment.setArguments(bunlsle)传递到该类的参数

运行效果图: 

10.4.2 Fragment与activity通信——Java类的接口

实现步骤:

  • 新建用于传递消息的接口IFragmentCallback
    public interface IFragmentCallback {
        void sendMsgToActivity(String msg);
        String getMsgFromActivity(String msg);
    }
  • 在需要进行触发消息传递的类BlankFragment中创建接口的实例并为其编写set方法public void setiFragmentCallback(IFragmentCallback iFragmentCallback),然后在onCreateView方法中编写监听器用于触发发送、获取消息
    public class BlankFragment extends Fragment {
    
        private static final String TAG = "TAG";
        private IFragmentCallback iFragmentCallback;
        private View view;
    
        public void setiFragmentCallback(IFragmentCallback iFragmentCallback) {
            this.iFragmentCallback = iFragmentCallback;
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            if (view == null) {
                view = inflater.inflate(R.layout.fragment_blank, container, false);
            }
            Button btn3 = view.findViewById(R.id.btn3);
            Button btn4 = view.findViewById(R.id.btn4);
            btn3.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    iFragmentCallback.sendMsgToActivity("hello, I'm from Fragment.");
                }
            });
            btn4.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    String msg = iFragmentCallback.getMsgFromActivity("hello, I'm Fragment.");
                    Toast.makeText(BlankFragment.this.getContext(), msg, Toast.LENGTH_SHORT).show();
                }
            });
            return view;
        }
    }
  • 在目标布局的方法中用BlankFragment对象调用setiFragmentCallback()方法进行对接口实例化
    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Button btn1 = findViewById(R.id.btn1);
            Button btn2 = findViewById(R.id.btn2);
            btn1.setOnClickListener(this);
            btn2.setOnClickListener(this);
        }
    
        @Override
        public void onClick(View view) {
            switch (view.getId()){
                case R.id.btn1:
                    BlankFragment blankFragment = new BlankFragment();
                    Bundle bunlsle = new Bundle();
                    bunlsle.putString("name", "张三");
                    bunlsle.putInt("age", 18);
                    blankFragment.setArguments(bunlsle);
                    blankFragment.setiFragmentCallback(new IFragmentCallback() {
                        @Override
                        public void sendMsgToActivity(String msg) {
                            Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
                        }
    
                        @Override
                        public String getMsgFromActivity(String msg) {
                            return "I'm from Activity.";
                        }
                    });
                    replaceFragment(blankFragment);
                    break;
                case R.id.btn2:
                    replaceFragment(new ItemFragment());
                    break;
            }
        }
    
        private void replaceFragment(Fragment fragment) {
            FragmentManager fragmentManager = getSupportFragmentManager();
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            transaction.replace(R.id.fragment1, fragment);
    
            transaction.addToBackStack(null);
    
            transaction.commit();
        }
    }

fragment_blank.xml代码:

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

    <!-- TODO: Update blank fragment layout -->
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/hello_blank_fragment" />

    <Button
        android:id="@+id/btn3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="发送数据"
        android:layout_marginTop="50dp"/>

    <Button
        android:id="@+id/btn4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取数据"
        android:layout_marginTop="100dp"/>

</FrameLayout>

运行效果图:

10.5 Fragment与ViewPager2的联合应用

10.5.1 ViewPage2的使用

使用步骤:

  1. 添加依赖
    在build.gradle文件中添加implementation 'androidx.viewpager2:viewpager2:1.0.0'
  2. 新建item_page.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">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="hello"
            android:layout_centerInParent="true"
            android:textSize="30sp"/>
    
    </RelativeLayout>
  3. 在activity_main.xml中使用ViewPager2
    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/vp"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </LinearLayout>
  4. 自定义ViewPagerAdapter类继承RecyclerView.Adapter<VH>。其中VH是自定义继承自RecyclerView.ViewHolder的ViewPagerHolder类(可以写成匿名内部类)
    public class ViewPagerAdapter extends RecyclerView.Adapter<ViewPagerAdapter.ViewPagerHolder> {
    
        private ArrayList<String> data = new ArrayList<>();
    
        public ViewPagerAdapter(ArrayList<String> data) {
            this.data = data;
        }
    
        @NonNull
        @Override
        public ViewPagerHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            ViewPagerHolder viewPagerHolder = new ViewPagerHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pager, parent, false));
            return viewPagerHolder;
        }
    
        @Override
        public void onBindViewHolder(@NonNull ViewPagerHolder holder, int position) {
            holder.textView.setText(data.get(position).toString());
        }
    
        @Override
        public int getItemCount() {
            return data.size();
        }
    
        class ViewPagerHolder extends RecyclerView.ViewHolder{
    
            TextView textView;
    
            public ViewPagerHolder(@NonNull View itemView) {
                super(itemView);
                textView = itemView.findViewById(R.id.tv);
            }
        }
    }
  5. 在Java中装填数据和使用ViewPager2

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ArrayList<String> data = new ArrayList<>();
            for (int i = 0; i < 5; i++) {
                data.add(new String("你好,我是第"+i+"个Pager"));
            }
    
            ViewPager2 viewPager = findViewById(R.id.vp);
            ViewPagerAdapter pagerAdapter = new ViewPagerAdapter(data);
            viewPager.setAdapter(pagerAdapter);
        }
    }

运行效果图:

10.5.2 Fragment与ViewPager2实例WeChat首页)

FragmentPagerAdapter与FragmentStatePagerAdapter介绍

  • FragmentPagerAdapter
    FragmentPagerAdapter继承PagerAdapter。使用FragmentPagerAdapter 时,Fragment对象会一直存留在内存中,所以当有大量的显示页时,就不适合用FragmentPagerAdapter了,FragmentPagerAdapter 适用于只有少数的page情况,像选项卡。
  • FragmentStatePagerAdapter
    FragmentStateAdapter继承RecyclerView.Adapter。如果page比较多,这个时候你可以考虑使用FragmentStatePagerAdapter ,当使用FragmentStatePagerAdapter 时,如果Fragment不显示,那么Fragment对象会被销毁,(滑过后会保存当前界面,以及下一个界面和上一个界面(如果有),最多保存3个,其他会被销毁掉)但在回调onDestroy()方法之前会回调onSaveInstanceState(Bundle outState)方法来保存Fragment的状态,下次Fragment显示时通过onCreate(Bundle savedInstanceState)把存储的状态值取出来,FragmentStatePagerAdapter 比较适合页面比较多的情况,像一个页面的ListView 。
  • 如上所述,使用FragmentStatePagerAdapter更省内存,但是销毁和新建也是需要时间的。一般情况下,如果你是制作主页面,用3、4个Tab,那么可以考虑使用FragmentPagerAdapter,如果你是用于ViewPager展示数量较多的条目时,那么建议使用FragmentStatePagerAdapter。

WeChat首页的滑动窗口实现:

  1. 在activity_main.xml中使用ViewPager2
    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <androidx.viewpager2.widget.ViewPager2
            android:id="@+id/viewpager"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"/>
    
    </LinearLayout>
  2. 新建BlankFragment继承于Fragment并生成fragment_blank.xml文件
  3. 编写fragment_blank.xml文件
    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".BlankFragment">
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:text="@string/hello_blank_fragment"
            android:textSize="30sp"
            android:gravity="center"/>
    
    </FrameLayout>
  4. 编写BlankFragment类
    public class BlankFragment extends Fragment {
    
        private static final String ARG_PARAM1 = "param1";
    
        private String mParam1;
    
        private View fragmentView;
    
        public BlankFragment() {
            // Required empty public constructor
        }
    
        public static BlankFragment newInstance(String param1) {
            BlankFragment fragment = new BlankFragment();
            Bundle args = new Bundle();
            args.putString(ARG_PARAM1, param1);
            fragment.setArguments(args);
            return fragment;
        }
    
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            if (getArguments() != null) {
                mParam1 = getArguments().getString(ARG_PARAM1);
            }
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            if (fragmentView == null) {
                fragmentView = inflater.inflate(R.layout.fragment_blank, container, false);
            }
            TextView textView = fragmentView.findViewById(R.id.textView);
            textView.setText(mParam1);
            return fragmentView;
        }
    }
  5. 新建BlankFragment类的适配器MyFragmentPagerAdapter继承自FragmentStatePagerAdapter(LifeCycle是GoogleApp架构中推荐的一个组件,作用就是用来监听Activity与Fragment的生命周期变化。)
    public class MyFragmentPagerAdapter extends FragmentStateAdapter {
    
        ArrayList<Fragment> fragmentList = new ArrayList<>();
    
        public MyFragmentPagerAdapter(@NonNull FragmentManager fragmentManager, @NonNull Lifecycle lifecycle, ArrayList<Fragment> fragmentList) {
            super(fragmentManager, lifecycle);
            this.fragmentList = fragmentList;
        }
    
        @NonNull
        @Override
        public Fragment createFragment(int position) {
            return fragmentList.get(position);
        }
    
        @Override
        public int getItemCount() {
            return fragmentList.size();
        }
    }
  6. 编写MainActivity类
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            ViewPager2 viewPager = findViewById(R.id.viewpager);
    
            ArrayList<Fragment> fragmentList = new ArrayList<>();
            fragmentList.add(BlankFragment.newInstance("微信聊天"));
            fragmentList.add(BlankFragment.newInstance("通讯录"));
            fragmentList.add(BlankFragment.newInstance("发现"));
            fragmentList.add(BlankFragment.newInstance("我的主页"));
            MyFragmentPagerAdapter myFragmentPagerAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager(),getLifecycle(),fragmentList);
            viewPager.setAdapter(myFragmentPagerAdapter);
        }
    }
  7. 运行效果图

 WeChat首页的雏形实现:

  1. 准备图片并在src/main/res/drawable/新建资源,资源名为tab_wexin.xml(同理新建4个用于选择的资源并编写)
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:drawable="@drawable/weixin_pressed" android:state_pressed="true"/>
        <item android:drawable="@drawable/weinxin_normal"/>
    </selector>
  2. 在src/main/res/color/新建tab_color.xml资源,作为颜色选择器
    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:color="#09C15F" android:state_selected="true"/>
        <item android:color="@color/black"/>
    </selector>
  3. 在src/main/res/layout/新建bottom_layout.xml并编写内容
    <?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="55dp"
        android:background="@color/gray">
    
        <LinearLayout
            android:id="@+id/tab_wechat"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/tab_wechat_iv"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:background="@drawable/tab_wechat"/>
            <TextView
                android:id="@+id/tab_wechat_tv"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:text="微信"
                android:gravity="center"
                android:textColor="@color/tab_color"/>
        </LinearLayout>
    
        <LinearLayout
            android:id="@+id/tab_contact"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/tab_contact_iv"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:background="@drawable/tab_contact"/>
            <TextView
                android:id="@+id/tab_contact_tv"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:text="通讯录"
                android:gravity="center"
                android:textColor="@color/tab_color"/>
        </LinearLayout>
    
        <LinearLayout
            android:id="@+id/tab_find"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/tab_find_iv"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:background="@drawable/tab_find"/>
            <TextView
                android:id="@+id/tab_find_tv"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:text="发现"
                android:gravity="center"
                android:textColor="@color/tab_color"/>
        </LinearLayout>
    
        <LinearLayout
            android:id="@+id/tab_profile"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:orientation="vertical">
            <ImageView
                android:id="@+id/tab_profile_iv"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:background="@drawable/tab_profile"/>
            <TextView
                android:id="@+id/tab_profile_tv"
                android:layout_width="32dp"
                android:layout_height="wrap_content"
                android:text="我"
                android:gravity="center"
                android:textColor="@color/tab_color"/>
        </LinearLayout>
    
    </LinearLayout>
  4. 在activity_main.xml中用include标签引入bottom布局
    <include layout="@layout/bottom_layout"/>
  5. 编写MainActivity.java类
    public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    
        private ViewPager2 viewPager;
        private LinearLayout llWechat, llContact, llFind, llProfile;
        private ImageView ivWechat, ivContact, ivFind, ivProfile, ivCurrent;
        private TextView tvWechat, tvContact, tvFind, tvProfile, tvCurrent;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            initFragmentPage();
    
            initTabView();
    
        }
    
        private void initTabView() {
            llWechat = findViewById(R.id.tab_wechat);
            ivWechat = findViewById(R.id.tab_wechat_iv);
            tvWechat = findViewById(R.id.tab_wechat_tv);
    
            llContact = findViewById(R.id.tab_contact);
            ivContact = findViewById(R.id.tab_contact_iv);
            tvContact = findViewById(R.id.tab_contact_tv);
    
            llFind = findViewById(R.id.tab_find);
            ivFind = findViewById(R.id.tab_find_iv);
            tvFind = findViewById(R.id.tab_find_tv);
    
            llProfile = findViewById(R.id.tab_profile);
            ivProfile = findViewById(R.id.tab_profile_iv);
            tvProfile = findViewById(R.id.tab_profile_tv);
    
            ivWechat.setSelected(true);
            tvWechat.setSelected(true);
            ivCurrent = ivWechat;
            tvCurrent = tvWechat;
    
            llWechat.setOnClickListener(this);
            llContact.setOnClickListener(this);
            llFind.setOnClickListener(this);
            llProfile.setOnClickListener(this);
        }
    
        private void initFragmentPage() {
            viewPager = findViewById(R.id.viewpager);
    
            ArrayList<Fragment> fragmentList = new ArrayList<>();
            fragmentList.add(BlankFragment.newInstance("微信聊天"));
            fragmentList.add(BlankFragment.newInstance("通讯录"));
            fragmentList.add(BlankFragment.newInstance("发现"));
            fragmentList.add(BlankFragment.newInstance("我的主页"));
            MyFragmentPagerAdapter myFragmentPagerAdapter = new MyFragmentPagerAdapter(getSupportFragmentManager(),getLifecycle(),fragmentList);
            viewPager.setAdapter(myFragmentPagerAdapter);
    
            viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
                @Override
                public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                    super.onPageScrolled(position, positionOffset, positionOffsetPixels);
                }
    
                @Override
                public void onPageSelected(int position) {
                    super.onPageSelected(position);
                    switch (position){
                        case 0:
                            changeTab(ivWechat, tvWechat);
                            break;
                        case 1:
                            changeTab(ivContact, tvContact);
                            break;
                        case 2:
                            changeTab(ivFind, tvFind);
                            break;
                        case 3:
                            changeTab(ivProfile, tvProfile);
                            break;
                    }
                }
    
                @Override
                public void onPageScrollStateChanged(int state) {
                    super.onPageScrollStateChanged(state);
                }
            });
        }
    
        private void changeTab(ImageView imageView, TextView textView) {
            ivCurrent.setSelected(false);
            tvCurrent.setSelected(false);
            imageView.setSelected(true);
            textView.setSelected(true);
            ivCurrent = imageView;
            tvCurrent = textView;
        }
    
        @Override
        public void onClick(View view) {
            switch (view.getId()){
                case R.id.tab_wechat:
                    changeTab(ivWechat, tvWechat);
                    viewPager.setCurrentItem(0);
                    break;
                case R.id.tab_contact:
                    changeTab(ivContact, tvContact);
                    viewPager.setCurrentItem(1);
                    break;
                case R.id.tab_find:
                    changeTab(ivFind, tvFind);
                    viewPager.setCurrentItem(2);
                    break;
                case R.id.tab_profile:
                    changeTab(ivProfile, tvProfile);
                    viewPager.setCurrentItem(3);
                    break;
            }
        }
    }
  6. 运行

运行效果图: 

第11章 数据存储

11.1 数据存储的选择

11.1.1 数据存储介绍

SP:保存配置信息或记忆信息(如记住密码)

SQLite:数据库

Room(新):数据库,更简洁

11.1.2 SP特点介绍

SP为SharedPreference(首选项)的缩写

首选项用于存储软件的配置信息:
window        .ini
android        .xml

自动登录,记住密码,主题记录等等
首选项不能存在太多的信息。特点:当程序运行首选项里面的数据会全部加载进内容(map的格式,key--value)

注意:很小、很简单的数据可以保存到首选项SP里面去

11.2 SP的简单使用

11.2.1 保存数据

保存目录:/data/data/包名/shared_prefs/文件名.xml

@override
public SharedPreferences getSharedPreferences(String name,int mode){
    return mBase.getSharedpreferences(name, mode);
}

参数1:SP文件的名字
参数2:SP保存的模式
        常规(每次保存都会更新/覆盖):Context.MODE_PRIVATE
        追加(每次保存都会追加到后面):Context.MODE_APPEND

11.2.2 获取数据

总代码:

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
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);
    }

    //保存数据到SP中
    public void saveToSP(View view) {
        SharedPreferences sp = getSharedPreferences("SPName", Context.MODE_PRIVATE);
        sp.edit().putString("book1","《九阳神功》").apply();
    }

    //获取SP中的数据
    public void getSPData(View view) {
        SharedPreferences sp = getSharedPreferences("SPName", Context.MODE_PRIVATE);
        String book1 = sp.getString("book1", "数据值不存在");
        Toast.makeText(this, book1, Toast.LENGTH_SHORT).show();
    }
}

运行效果:

11.2.3 SP实战(记住密码和自动登录)

注意:由于时间关系,没有考虑程序的健壮性,该项目有Bug,在点击自动登录时需要连勾保存密码,在取消勾选保存密码时需要取消勾选自动登录,该功能只需要实现复选框改变的监听函数即可。

activity_main代码:

<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="50dp"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="用户名:" />

        <EditText
            android:id="@+id/et_username"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入用户名"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="密    码:" />

        <EditText
            android:id="@+id/et_password"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="请输入密码"
            android:inputType="textPassword"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <CheckBox
            android:id="@+id/cb_rememberPassword"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="记住密码"/>

        <CheckBox
            android:id="@+id/cb_autoLogin"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自动登录"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <Button
            android:id="@+id/btn_register"
            android:layout_width="wrap_content"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="注册"/>

        <Button
            android:id="@+id/btn_login"
            android:layout_width="wrap_content"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:text="登录"/>
    </LinearLayout>
</LinearLayout>

MainActivity代码:

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {

    private SharedPreferences configSP;
    private EditText et_username, et_password;
    private CheckBox cb_rememberPassword, cb_autoLogin;
    private Button btn_register, btn_login;

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

        init();

        //若是打勾了,则下一次登录是回显数据
        boolean rememberPassword = configSP.getBoolean("rememberPassword", false);
        boolean autoLogin = configSP.getBoolean("autoLogin", false);
        if (rememberPassword){
            et_username.setText(configSP.getString("username", ""));
            et_password.setText(configSP.getString("password", ""));
            cb_rememberPassword.setChecked(true);
        }
        if (autoLogin){
            cb_autoLogin.setChecked(true);
            Toast.makeText(this, "自动登录成功", Toast.LENGTH_SHORT).show();
        }
    }

    private void init() {
        configSP = getSharedPreferences("config", Context.MODE_PRIVATE);

        et_username = findViewById(R.id.et_username);
        et_password = findViewById(R.id.et_password);
        cb_rememberPassword = findViewById(R.id.cb_rememberPassword);
        cb_autoLogin = findViewById(R.id.cb_autoLogin);
        btn_register = findViewById(R.id.btn_register);
        btn_login = findViewById(R.id.btn_login);

        MyOnClickListener myOnClickListener = new MyOnClickListener();
        btn_register.setOnClickListener(myOnClickListener);
        btn_login.setOnClickListener(myOnClickListener);
    }
    public class MyOnClickListener implements View.OnClickListener{

        @Override
        public void onClick(View view) {
            switch (view.getId()){
                case R.id.btn_register:
                    break;
                case R.id.btn_login:
                    login(view);
                    break;
            }
        }
    }

    private void login(View view) {
        String username = et_username.getText().toString().trim();
        String password = et_password.getText().toString().trim();
        if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)){
            Toast.makeText(this, "用户名或密码不能为空", Toast.LENGTH_SHORT).show();
        } else {
            if (TextUtils.equals("admin", username) && TextUtils.equals("123456", password)){
                SharedPreferences.Editor edit = configSP.edit(); //获取到编辑
                if (cb_rememberPassword.isChecked()){ //是否需要保存密码
                    edit.putString("username", username);
                    edit.putString("password", password);
                    edit.putBoolean("rememberPassword", true);
                    edit.apply();
                }
                if (cb_autoLogin.isChecked()){ //是否需要自动登录
                    edit.putBoolean("autoLogin", true);
                    edit.apply();
                }Toast.makeText(this, "登录成功", Toast.LENGTH_SHORT).show();

            } else {
                Toast.makeText(this, "用户名或密码错误", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

运行效果图:

11.3 SQLite的简单使用

11.3.1 SQLite介绍

SQLite为关系型数据库
嵌入式的数据库,体积小功能强大,几十kb,嵌入式设备上:计算器、手表等。
在Android平台上,集成了一个嵌入式关系型数据库——SQLite,SQLite3支持NULL、INTEGER、REAL(浮点数字)、TEXT(字符串文本)和BLOB(二进制对象)数据类型,虽然它支持的类型只有五种,但实际上sqlite3也接受varchar(n)、char(n)、decimal(p,s)等数据类型,只不过在运算或保存时会转成对应的五种数据类型。

SQLite最大的特点是你可以把各种类型的数据保存到字段中,但是主键只能是Integer类型的。Sqlite数据库一般要求主键是_id,当然也可以是id。
Android里面的数据库是由底层的sqilte.c的代码来动态生成的。

11.3.2 SQLite的可视化工具

安装SQLite的可视化工具,将SQLite数据库导出,然后将导出的SQLite数据库拖入可视化工具中即可

SQLiteExpert官网链接:SQLite administration | SQLite Expert

更多SQLite的可视化工具:(4条消息) sqlite 免费的可视化工具_Lucas__liu的博客-CSDN博客_sqlite可视化工具

11.3.3 SQLiteOpenHelper类的方法介绍

方法名

作用

onCreate()

创建数据库,一个Helper类只会创建一次表

onUpgrade()

升级数据库

close()

关闭所有打开的数据库对象

execSQL()

执行增删改操作的SQL语句

rawQuery()执行查询操作的SQL语句

insert()

插入数据

delete()

删除数据

query()查询数据

update()

修改数据

getWritableDatabase()

创建或打开可写的数据库

getReadableDatabase()

创建或打开可读的数据

11.3.4 SQLite数据库的操作

1、创建SQLite数据库的辅助类(包括创库和创表)

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import androidx.annotation.Nullable;

//MySqliteOpenHelper是工具类 --> 单例模式(1.构造函数私有化  2.对外提供函数)

public class MySQLiteOpenHelper extends SQLiteOpenHelper {

    private static MySQLiteOpenHelper mySQLiteHelper = null;

    //2.对外提供函数
    public static synchronized MySQLiteOpenHelper getInstance(Context context){
        if (mySQLiteHelper == null){
            mySQLiteHelper = new MySQLiteOpenHelper(context,"user.db",null,1);
        }
        return mySQLiteHelper;
    }

    //1.构造函数私有化
    private MySQLiteOpenHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL("create table persons(_id integer primary key autoincrement, name varchar(20), sex varchar(2), age integer)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

2、创建JavaBean类

public class Person {
    private String name;
    private String sex;
    private Integer age;

    public Person() {
    }

    public Person(String name, String sex, Integer age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

3、编写activity_main.xml代码 

4、操作SQLite数据库

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private MySQLiteOpenHelper myHelper;
    private String TAG = "MainActivity";

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

        initi();
    }

    private void initi() {
        myHelper = MySQLiteOpenHelper.getInstance(this);

        Button btn_insert = findViewById(R.id.btn_insert);
        Button btn_delete = findViewById(R.id.btn_delete);
        Button btn_query = findViewById(R.id.btn_query);
        Button btn_update = findViewById(R.id.btn_update);

        btn_insert.setOnClickListener(this);
        btn_delete.setOnClickListener(this);
        btn_query.setOnClickListener(this);
        btn_update.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_insert:
                Log.i(TAG, "插入数据");
                insert("张三", "女", 18);
                break;
            case R.id.btn_delete:
                Log.i(TAG, "删除数据");
                delete("李四");
                break;
            case R.id.btn_query:
                Log.i(TAG, "查询数据");
                query();
                break;
            case R.id.btn_update:
                Log.i(TAG, "更新数据");
                update("张三", "李四");
                break;
        }
    }

    private void insert(String name, String sex, Integer age) {
        SQLiteDatabase writableDatabase = myHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("name", name);
        values.put("sex", sex);
        values.put("age", age);
        long insertCount = writableDatabase.insert("persons", null, values);
        /**
         * 第一个参数:操作的表名
         * 第二个参数:插入的列没有数据匹配时的默认值
         * 第三个参数:插入的数据列表
         */
        if (insertCount > 0){
            Toast.makeText(this, "插入成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "插入失败", Toast.LENGTH_SHORT).show();
        }
    }

    private void delete(String name) {
        SQLiteDatabase writableDatabase = myHelper.getWritableDatabase();
        String[] whereArgs = {name};
        int deleteCount = writableDatabase.delete("persons", "name=?", whereArgs);
        /**
         * 第一个参数:操作的表名
         * 第二个参数:条件语句, 如果为null, 就删除所有行
         * 第三个参数:条件字符串中"?"占位符的匹配列表
         */
        if (deleteCount > 0){
            Toast.makeText(this, "删除成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "删除失败", Toast.LENGTH_SHORT).show();
        }
        writableDatabase.close();
    }

    private void query() {
        SQLiteDatabase readableDatabase = myHelper.getReadableDatabase();
        Cursor cursor = readableDatabase.query("persons", null, null, null, null, null, null);
        while (cursor.moveToNext()) {
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String sex = cursor.getString(cursor.getColumnIndex("sex"));
            int age = cursor.getInt(cursor.getColumnIndex("age"));
            Person person = new Person(name, sex, age);
            Log.i(TAG, person.toString());
        }
        cursor.close();
        readableDatabase.close();
    }

    private void update(String name, String name1) {
        SQLiteDatabase writableDatabase = myHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put("name", name1);
        String[] whereArgs = {name};
        int updateCount = writableDatabase.update("persons", values, "name=?", whereArgs);
        /**
         * 第一个参数:操作的表名
         * 第二个参数:更新的数据列表
         * 第三个参数:条件语句, 如果为null, 就更新所有行
         * 第四个参数:条件字符串中"?"占位符的匹配列表
         */
        if (updateCount > 0){
            Toast.makeText(this, "更新成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "更新失败", Toast.LENGTH_SHORT).show();
        }
        writableDatabase.close();
    }
}

5、运行效果图

注意:在数据库查询操作的cursor.getString(cursor.getColumnIndex("name"));处报错,但不影响编译(可分为两步写解决报错问题)
        操作顺序为插入-->查询-->更新--查询-->删除-->查询

11.4 Room的简单使用

11.4.1 Room的介绍

Room是一个对象关系映射(ORM)库。Room抽象了SQLite的使用,可以在充分利用SQLite的同时访问流畅的数据库

Room官方文档介绍 https://developer.android.com/training/data-storage/room/

Room由三个重要的组件组成(三角色):Entity、Dao、Database。

  • Entity:数据库实体,系统根据Entity类创建数据库,里面规定了PrimaryKey,列名、表名等数据库必备设定
  • Dao:Database access object:定义了一些操作数据库的操作,比如说增删改查
  • Database:可以认为是完整的数据库,完整的数据库包括数据库信息和数据库操作,也就是EntityDao

11.4.2 Room三角色的实现

注意:Room操作数据库时都是基于主键操作的

1、Student(Entity)

import androidx.room.Entity;
import androidx.room.Index;
import androidx.room.PrimaryKey;

@Entity(indices = {@Index(value = {"studentNumber"}, unique = true)})
public class Student {
    @PrimaryKey(autoGenerate = true)
    private Integer id;
    private String studentNumber;
    private String name;
    private String classes;

    public Student(Integer id, String studentNumber, String name, String classes) {
        this.id = id;
        this.studentNumber = studentNumber;
        this.name = name;
        this.classes = classes;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getStudentNumber() {
        return studentNumber;
    }

    public void setStudentNumber(String studentNumber) {
        this.studentNumber = studentNumber;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getClasses() {
        return classes;
    }

    public void setClasses(String classes) {
        this.classes = classes;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", studentNumber='" + studentNumber + '\'' +
                ", name='" + name + '\'' +
                ", classes='" + classes + '\'' +
                '}';
    }
}

2、StudentDao(Dao)

import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;

import java.util.List;

@Dao
public interface StudentDao {
    @Insert
    void insertStudent(Student student);

    @Insert
    void insertStudents(List<Student> students);

    @Query("DELETE FROM student WHERE studentNumber=:studentNumber")
    int deleteStudent(String studentNumber);

    @Query("DELETE FROM student")
    int deleteAllStudent();

    @Query("SELECT * FROM Student")
    List<Student> queryAllStudent();

    @Query("SELECT * FROM Student WHERE studentNumber=:studentNumber")
    Student queryStudentByStudentNumber(String studentNumber);

    @Query("SELECT * FROM Student WHERE name=:name")
    List<Student> queryStudentByName(String name);

    @Update
    int updateStudent(Student student);
}

3、StudentDatabase(Database)

import android.content.Context;

import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;

/**
 * entities:传入所有Entity的class对象;
 * version:数据库版本号。
 * exportSchema:设置是否导出数据库schema,默认为true,需要在build.gradle中设置
 */
@Database(entities = {Student.class}, version = 1, exportSchema = false)
public abstract class StudentDatabase extends RoomDatabase {

    private static StudentDatabase studentDatabase = null;

    public static synchronized StudentDatabase getInstance(Context context){
        if (studentDatabase == null){
            studentDatabase = Room.databaseBuilder(context.getApplicationContext(), StudentDatabase.class, "student")
                    .allowMainThreadQueries() //强制在主线程执行
                    .build();
        }
        return studentDatabase;
    }

    public abstract StudentDao getStudentDao();
}

11.4.3 使用Room三角色

1、编写activity_main.xml

2、编写ActivityMain.java

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.util.List;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "MainActivity";
    private StudentDao studentDao;

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

    private void init() {
        studentDao = StudentDatabase.getInstance(this).getStudentDao();

        Button btn_insert = findViewById(R.id.btn_insert);
        Button btn_delete = findViewById(R.id.btn_delete);
        Button btn_query = findViewById(R.id.btn_query);
        Button btn_update = findViewById(R.id.btn_update);

        btn_insert.setOnClickListener(this);
        btn_delete.setOnClickListener(this);
        btn_query.setOnClickListener(this);
        btn_update.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_insert:
                Log.i(TAG, "插入数据");
                insert("38241319601", "张三", "2019级计算机科学与技术六班");
                insert("38241319602", "李si", "2019级计算机科学与技术六班");
                insert("38241319603", "王五", "2019级计算机科学与技术六班");
                insert("38241319604", "赵六", "2019级计算机科学与技术六班");
                break;
            case R.id.btn_delete:
                Log.i(TAG, "删除数据");
                delete("38241319602");
                break;
            case R.id.btn_query:
                Log.i(TAG, "查询数据");
                queryAll();
                break;
            case R.id.btn_update:
                Log.i(TAG, "更新数据");
                update(2, "李四");
                break;
        }
    }

    private void insert(String studentNumber, String name, String classes) {
        Student student = new Student(null, studentNumber, name, classes);
        student.setStudentNumber(studentNumber);
        studentDao.insertStudent(student);
    }

    private void delete(String studentNumber) {
        int deleteCount = studentDao.deleteStudent(studentNumber);
        if (deleteCount > 0){
            Toast.makeText(this, "删除成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "删除失败", Toast.LENGTH_SHORT).show();
        }
    }

    private void queryAll() {
        List<Student> students = studentDao.queryAllStudent();
        for (Student student : students) {
            Log.i(TAG, student.toString());
        }
    }

    private void update(Integer id, String name) {
        Student student = new Student(id, null, name, null);
        int updateCount = studentDao.updateStudent(student);
        if (updateCount > 0){
            Toast.makeText(this, "更新成功", Toast.LENGTH_SHORT).show();
        } else {
            Toast.makeText(this, "更新失败", Toast.LENGTH_SHORT).show();
        }
    }
}

6、运行效果

第12章 多媒体应用

Android 多媒体框架支持捕获和编码各种常见的音频和视频格式。 

12.1 MediaRecorder概述

用于录制音频和视频的一个类。

12.1.1 状态转换图

说明:

下面是关于MediaRecorder状态图的各个状态的介绍:

Initial:初始状态,当使用new()方法创建一个MediaRecorder对象或者调用了reset()方法时,该MediaRecorder对象处于Initial状态。在设定视频源或者音频源之后将转换为Initialized状态。另外,在除Released状态外的其它状态通过调用reset()方法都可以使MediaRecorder进入该状态。

Initialized:已初始化状态,可以通过在Initial状态调用setAudioSource()或setVideoSource()方法进入该状态。在这个状态可以通过setOutputFormat()方法设置输出格式,此时MediaRecorder转换为DataSourceConfigured状态。另外,通过reset()方法进入Initial状态。

DataSourceConfigured:数据源配置状态,这期间可以设定编码方式、输出文件、屏幕旋转、预览显示等等。可以在Initialized状态通过setOutputFormat()方法进入该状态。另外,可以通过reset()方法回到Initial状态,或者通过prepare()方法到达Prepared状态。

Prepared:就绪状态,在DataSourceConfigured状态通过prepare()方法进入该状态。在这个状态可以通过start()进入录制状态。另外,可以通过reset()方法回到Initialized状态。

Recording:录制状态,可以在Prepared状态通过调用start()方法进入该状态。另外,它可以通过stop()方法或reset()方法回到Initial状态。

Released:释放状态(官方文档给出的词叫做Idle state 空闲状态),可以通过在Initial状态调用release()方法来进入这个状态,这时将会释放所有和MediaRecorder对象绑定的资源。

Error:错误状态,当错误发生的时候进入这个状态,它可以通过reset()方法进入Initial状态。

提示:使用MediaRecorder录音录像时需要严格遵守状态图说明中的函数调用先后顺序,在不同的状态调用不同的函数,否则会出现异常。

12.1.2 使用MediaRecorder的使用

1、添加权限

    <!-- 授予该程序录制声音的权限 -->
	<uses-permission android:name="android.permission.RECORD_AUDIO"/>
	<!-- 授予该程序使用摄像头的权限 -->	
	<uses-permission android:name="android.permission.CAMERA"/>

2、MainActivity.java(含申请权限) 

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

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

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

        init();
    }

    public void init(){
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA},
                1000);

        Button btn_record = findViewById(R.id.btn_record);
        Button btn_playVideo = findViewById(R.id.btn_playVideo);
        Button btn_playAudio = findViewById(R.id.btn_playAudio);
        Button btn_playVideoVV = findViewById(R.id.btn_playVideoVV);

        btn_record.setOnClickListener(this);
        btn_playVideo.setOnClickListener(this);
        btn_playAudio.setOnClickListener(this);
        btn_playVideoVV.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.btn_record:
                startActivity(new Intent(this, RecordActivity.class));
                break;
            case R.id.btn_playVideo:
                startActivity(new Intent(this, VideoActivity.class));
                break;
            case R.id.btn_playVideoVV:
                startActivity(new Intent(this, VideoViewActivity.class));
                break;
            case R.id.btn_playAudio:
                startActivity(new Intent(this, RecordActivity.class));
                break;
        }
    }
}

3、RecordActivity.java(问题:声音和图像分开播放,未解决)

import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.IOException;

public class RecordActivity extends AppCompatActivity implements View.OnClickListener {
//    private SurfaceView surfaceView;
    private TextureView textureView;
    private Button btn_opt;

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

//        surfaceView = findViewById(R.id.surfaceView);
        textureView = findViewById(R.id.textureView);

        btn_opt = findViewById(R.id.btn_opt);
        btn_opt.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        CharSequence btn_optText = btn_opt.getText();
        MediaRecorder mediaRecorder = null;
        Camera camera = Camera.open();
        if (TextUtils.equals("开始", btn_optText)){
            btn_opt.setText("结束");
            camera.setDisplayOrientation(90); //竖屏
            camera.unlock();
            mediaRecorder = new MediaRecorder();
            mediaRecorder.setCamera(camera);
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); //设置音频源 麦克风
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); //设置视频源 麦克风
            mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //设置视频输出格式
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); //设置音频编码格式
            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP); //设置视频格式
            mediaRecorder.setOrientationHint(90); //设置文件正向
            mediaRecorder.setOutputFile(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath()); //设置文件输出路径
            mediaRecorder.setVideoSize(640, 480); //设置视频显示大小
//            mediaRecorder.setPreviewDisplay(surfaceView.getHolder().getSurface()); //低版本不行
            mediaRecorder.setPreviewDisplay(new Surface(textureView.getSurfaceTexture()));
            try {
                mediaRecorder.prepare(); //录制准备
            } catch (IOException e) {
                e.printStackTrace();
            }
            mediaRecorder.start(); //开始录制
        } else {
            btn_opt.setText("开始");
            mediaRecorder.stop();
            mediaRecorder.release(); //释放mediaRecorder
            camera.stopPreview();
            camera.release();
        }
    }
}

4、activity_record.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RecordActivity">

    <SurfaceView
        android:id="@+id/surfaceView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/btn_opt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="开始"
        app:layout_constraintBottom_toBottomOf="@+id/textureView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

12.2 MediaPlayer简介

MediaPlayer类是媒体框架最重要的组成部分之一。此类的对象能够获取、解码以及播放音频和视频,而且只需极少量设置。它支持多种不同的媒体源,例如:

  • 本地资源
  • 内部URI,例如您可能从内容解析器那获取的URI·外部网址(流式传输)

媒体格式列表:https://developer.android.google.cn/guide/topics/media/media-formats?hl=zh_cn

12.2.1 状态转换图

12.2.2 自定义

1、VideoActivity.java

import android.media.MediaPlayer;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;
import java.io.IOException;

public class VideoActivity extends AppCompatActivity implements View.OnClickListener, MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener {
    private TextureView textureView;
    private Button btn_opt;

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

        textureView = findViewById(R.id.textureView);

        btn_opt = findViewById(R.id.btn_opt);
        btn_opt.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        CharSequence btn_optText = btn_opt.getText();
        MediaPlayer mediaPlayer = null;
        if (TextUtils.equals("开始", btn_optText)) {
            btn_opt.setText("结束");
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setOnPreparedListener(this); //加载完成监听
            mediaPlayer.setOnCompletionListener(this); //播放完成监听
            try {
                mediaPlayer.setDataSource(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath());
            } catch (IOException e) {
                e.printStackTrace();
            }
            mediaPlayer.setSurface(new Surface(textureView.getSurfaceTexture())); //播放画布
            mediaPlayer.prepareAsync(); //异步加载
        } else {
            btn_opt.setText("开始");
            mediaPlayer.stop();
            mediaPlayer.release();
        }
    }

    @Override
    public void onPrepared(MediaPlayer mediaPlayer) {
        mediaPlayer.start(); //播放
    }

    @Override
    public void onCompletion(MediaPlayer mediaPlayer) {
        btn_opt.setText("开始");
        mediaPlayer.stop();
        mediaPlayer.release();
    }
}

2、activity_video.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".RecordActivity">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

12.2.3 Android自带的VideoView

1、VideoViewActivity.java

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.MediaController;
import android.widget.VideoView;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;

public class VideoViewActivity extends AppCompatActivity implements View.OnClickListener {

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

        VideoView videoView = findViewById(R.id.videoView);

        MediaController mediaController = new MediaController(this);
        mediaController.setPrevNextListeners(this, this); //上一曲、下一曲
        videoView.setMediaController(mediaController);
        videoView.setVideoPath(new File(getExternalFilesDir(""), "a.mp4").getAbsolutePath());
        videoView.start();
    }

    @Override
    public void onClick(View view) {
        Log.i("VideoView", "======");
    }
}

2、activity_videoview.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".VideoViewActivity">

    <VideoView
        android:id="@+id/videoView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

12.3 SoundPool音效播放

12.3.1 SoundPool简介

MediaPlayer虽然也能播放音频,但是它有资源占用量较高、延迟时间较长、不支持多个音频同时播放等缺点。这些缺点决定了MediaPlayer在某些场合的使用情况不会很理想,例如在对时间精准度要求相对较高的场景。而SoundPool一般用来播放密集、急促而又短暂的音效,比如:"滴滴一下,马上出发”。

12.3.2 使用SounPool

1、Music.java

public class Music {
    private String name;
    private int musicId;

    public Music() {
    }

    public Music(String name, int musicId) {
        this.name = name;
        this.musicId = musicId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getMusicId() {
        return musicId;
    }

    public void setMusicId(int musicId) {
        this.musicId = musicId;
    }

    @Override
    public String toString() {
        return "Music{" +
                "name='" + name + '\'' +
                ", musicId=" + musicId +
                '}';
    }
}

2、创建SoundActivity

activity_sound.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SoundActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@color/teal_200"/>

</androidx.constraintlayout.widget.ConstraintLayout>

3、music_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:orientation="vertical"
    android:background="@color/red">

    <TextView
        android:id="@+id/tv_musicName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="30sp"/>
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="@color/black"/>

</LinearLayout>

4、MusicAdamter.java

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import java.util.List;

public class MusicAdapter extends RecyclerView.Adapter<MusicAdapter.ViewHolder> {
    private List<Music> musicList;
    private Context context;

    public MusicAdapter(List<Music> musicList, Context context){
        this.musicList = musicList;
        this.context = context;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        //用来创建ViewHolder实例,再将加载好的布局传入构造函数,最后返回ViewHolder实例
//        View view = View.inflate(context, R.layout.music_item, null);
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.music_item, parent, false); //解决宽度不能铺满
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int position = view.getVerticalScrollbarPosition();
                Toast.makeText(context, musicList.get(position).getName(), Toast.LENGTH_SHORT).show();
            }
        });
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.textView.setText(musicList.get(position).getName());
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                if(onItemClickListener != null) {
                    int pos = holder.getLayoutPosition();
                    onItemClickListener.onItemClick(holder.itemView, pos);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return musicList.size();
    }

    public interface OnItemClickListener{
        void onItemClick(View view, int position);
    }
    private OnItemClickListener onItemClickListener;
    public void setOnItemClickListener(MusicAdapter.OnItemClickListener listener) {
        this.onItemClickListener = listener;
    }


    public class ViewHolder extends RecyclerView.ViewHolder{
        protected TextView textView;

        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            textView = itemView.findViewById(R.id.tv_musicName);
        }
    }
}

5、SoundActivity.java

import android.content.Context;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class SoundActivity extends AppCompatActivity {

    private List<Music> musicList = new ArrayList<>();
    private SoundPool soundPool;

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

        initMusics();

        RecyclerView recyclerView = findViewById(R.id.recyclerView);

        LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);  //设置方向
        recyclerView.setLayoutManager(layoutManager);
        MusicAdapter musicAdapter = new MusicAdapter(musicList, this);
        musicAdapter.setOnItemClickListener(new MusicAdapter.OnItemClickListener() {
            @Override
            public void onItemClick(View view, int position) {
                Toast.makeText(SoundActivity.this, "点击了"+musicList.get(position).getName(), Toast.LENGTH_SHORT).show();
                Music music = musicList.get(position);
                AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
                int volumem = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
                int volume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
                soundPool.play(music.getMusicId(), volumem, volume, 1, 1, 1);
            }
        });
        recyclerView.setAdapter(musicAdapter);
    }

    private void initMusics() {
        soundPool = new SoundPool.Builder().setMaxStreams(6).build();
        for (int i = 0; i < 6; i++) {
            musicList.add(new Music("a"+(i+1), soundPool.load(this, R.raw.a1+i, 1))); //不建议
        }

//        musicList.add(new Music("a2", soundPool.load(this, R.raw.a2, 1)));
//        musicList.add(new Music("a3", soundPool.load(this, R.raw.a3, 1)));
//        musicList.add(new Music("a4", soundPool.load(this, R.raw.a4, 1)));
//        musicList.add(new Music("a5", soundPool.load(this, R.raw.a5, 1)));
//        musicList.add(new Music("a6", soundPool.load(this, R.raw.a6, 1)));
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        for (Music music : musicList) {
            soundPool.unload(music.getMusicId());
        }
        soundPool.release();
    }

音效资源链接:https://pan.baidu.com/s/1yIY5l7xQf0kG7lO_U6A4uQ 
提取码:neyt

第13章 项目发布

13.1 项目安全

13.1.1 加固

为什么应用需要加固:防止应用被逆向分析、反编译、二次打包,防止嵌入各类病毒、广告等恶意代码,从源头保护数据安全和开发者利益。

13.1.2 设置多渠道

1、统计各个渠道包的情况,例如哪个渠道的下载量更大,哪个渠道下载的客户活跃度或者粘性更高等信息。
2、针对不同的渠道做一些不同的操作。
目前常用的多渠道打包工具有三种:

  • 友盟
  • 美团
  • 360

11.1.3 生成release apk

 

常用:【Build】-->【Generate Signed Bundle / APK...】-->【APK】-->【Next】--> 【Key story path】-->【Next】-->【使用加固工具进行加固】 -->【应用发布】

更多知识点详见:Android基础入门教程 | 菜鸟教程 (runoob.com)

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐