学习笔记:Android

本篇是 Android 开发中需要掌握的基础内容

一、前言

1.开发方向

(1).传统Windows开发

  • SDK、DDK、C++、C#(.Net Framework、.Net Core)

(2).移动开发

  • HTML+CSS、AJAX、JS、XML(XPath)、图形图像

  • 基于 ARM 架构

  • SOC

(3).Web开发

  • CoM

  • 前端:HTML+CSS、AJAX、JS、图形图像

  • 后台:CGI+DB,PHP,ASP.NET、JSP

(4).嵌入式开发

2.学习内容

  • 算法(数据结构)

  • 接口

  • 方法论

二、基础内容

1.零碎

  • Android 讲究逻辑设视图分离 —— 在布局文件中编写界面,然后在活动中引入进来

  • res 文件夹下最后自己再创建 drawable-hdpi、drawable-xhdpi、drawable-xxhdpi 等文件夹,用来存放不同分辨率的图片

2.环境搭建

  • 关于软件下载,可以去这个网站

  • 关于安卓信息,可以去这个网站了解

(1).ADT

  • 关于ADT版本和SDK Tools的对应版本可以查看这里,里面详细说明了不同版本的ADT所对应的JDK版本要求,eclipse版本要求及SDK Tools版本要求。

(2).Android Studio

  • 关于其概述,可以查看这里

  • 安装教程查看这里

3.日志工具

  • Android 中的日志工具类是 Log,该类中提供了以下五个方法:
    Log.v():对应级别verbose,打印所有日志信息
    Log.d():对应级别debug,打印调试信息
    Log.i():对应级别info,打印比较重要的数据
    Log.w():对应级别warn,打印警告信息
    Log.e():对应级别error,打印错误信息

  • 该方法中需要两个字符串参数:
    tag:用于对打印信息进行过滤
    msg:想要打印的具体内容

三、活动(Activity)

1.基础

  • 项目中的任何活动都应该重写 Activity 的 onCreate() 方法

2.简单编写

(1).创建活动

  • 在项目的 app/src/main/java 下的包中右键新建 Empty Activity

(2).创建布局文件

  • 在项目的 app/src/main/res 下右键新建一个Directory,并将其命名为

(3).设计布局文件

(4).加载布局文件

  • 使用 setContentView() 方法,一般需要传入一个布局文件的id

(5).注册活动

  • 将活动在 AndroidManifest.xml 文件中注册

  • 活动的注册声明要放在 <application> 标签内,通过 <activity> 标签来对活动进行注册

  • <activity> 标签中使用 android:name 来指定具体注册哪一个活动,因为前面包名已经指定,所以只需要写 .活动名 即可

(6).配置主活动

  • <activity> 标签内部加入 <intent-filter> 标签,并在标签中写入如下内容:

1
2
3
4
5
6
7
 <activity android:name=".FirstActivity"
android:label="FirstActivity"> <!--指定活动中标题栏内容,并且成为应用名-->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

(7).销毁活动

  • 第一种:按下Back键就可以销毁当前的活动

  • 第二种:调用Activity类中的finish() 方法

1
2
3
4
@Override
public void onClick(View v){
finish();
}

3.Toast

  • 通过静态方法 makeText() 创建出一个 Toast 对象,然后调用 show() 将 Toast 显示出来就可以了

  • 该方法需要传入3个参数:
    Context:Toast要求的上下文
    Text:Toast显示的文本内容
    Time:Toast显示的时长,可选值为:Toast.LENGTH_SHORT与Toast.LENGTH_LONG

  • 如下就是通过点击一个按钮来使其显示 Toast

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
// 获取到在布局文件中定义的元素并返回View对象
Button button1 = (Button) findViewById(R.id.button_1);
// 为按钮注册一个监听器
button1.setOnClickListener(new View.OnClickListener(){
// 为按钮添加单击响应事件
@Override
public void onClick(View v){
Toast.makeText(FirstActivity.this,"You clicked Button 1",Toast.LENGTH_SHORT).show();
}
});
}

4.Menu

5.Intent

6.生命周期

7.启动模式

(1).standard

(2).singleTop

(3).singleTask

(4).singleInstance

8.活动的最佳实践

(1).知晓当前在哪一个活动

(2).随时随地退出程序

(3).启动活动的最佳写法

四、UI 设计

1.视图(View)

  • 任何可视化控件都需要从 android.view.View 类中继承,而任何继承自该类的类都称为视图

  • View 可以分为三种:布局(layout)、视图容器类(View Container)、视图类(Button、TextView、EditText等)

  • 视图的继承关系如下图:
    Android02.png

2.视图之视图类

(1).TextView

  • 主要用于在界面上显示一段文本信息

(2).Button

  • 主要用于在界面上显示一个按钮

  • 在布局文件中设置的文字有小写,但实际会转为大写,可以添加如下文件来禁用这一默认特性:

1
2
3
4
5
6
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="false" //禁用默认特性
android:text="Button" />
  • 绑定点击事件之匿名类注册监听器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//此处写逻辑
}
});
}
}
  • 绑定点击事件之实现接口注册监听器,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
// 实现接口方式注册监听器
button.setOnClickListener(this);
}
@Override
public void onClick(View v){
switch(v.getId()){
case R.id.button:
//此处写逻辑
break;
default:
break;
}
}
}

(3).EditText

  • 主要用于在控件里输入和编辑内容,并可以在程序中对这些内容进行处理

(4).ImageView

  • 主要用于引入图片并进行相关操作

  • 在引入之前最后自己在 res 目录下自己新建一个 drawable-xhdpi 的文件夹并将图片放入其中

(5).ProgressBar

  • 主要用于在界面上显示一个进度条,表示程序正在加载一些数据

  • 如果想要更改进度条的样式,可以如下设置:

1
2
3
4
5
6
7
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
// 更改进度条样式并设置最大值
style="?android:attr/progressBarStyleHorizontal"
android:max="100" />

(6).AlertDialog

  • 可以在当前界面弹出一个对话框,且置顶于所有界面元素之上的

  • 具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 点击按钮弹出一个对话框
@Override
public void onClick(View v){
switch (v.getId()){
case R.id.button:
AlertDialog.Builder dialog = new AlertDialog.Builder(MainActivity.this);
dialog.setTitle("This is Dialog");
dialog.setMessage("这很重要,你确定要删除吗?");
// 设置可以用返回键来关闭对话框
dialog.setCancelable(false);
dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
dialog.setNegativeButton("Cancle", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
// 显示对话框
dialog.show();
break;
default:
break;
}
}

(7).ProgressDialog

  • 可以在当前界面弹出一个对话框,且置顶于所有界面元素之上的,区别于上面的是,该对话框可以显示进度条,一般用于表示当前操作比较耗时

  • 具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 点击按钮弹出对话框且显示进度条
@Override
public void onClick(View v){
switch (v.getId()){
case R.id.button:
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading....");
progressDialog.setCancelable(true);
progressDialog.show();
break;
default:
break;
}
}

(8).相关属性

a).android:id 属性

  • 语法如下:

1
android:id="@+id/user"

b).android:background 属性

  • 语法如下:

1
2
android:background="@mipmap/bg"
android:background="#bfa"

c).android:padding 属性

  • 语法如下:

1
2
android:padding="@dimen/activity_margin"
android:padding="16dp"
  • 还包括如下四个:
    android:paddingLeft
    android:paddingTop
    android:paddingRight
    android:paddingBottom

  • API 17新增加:
    android:paddingStart
    android:paddingEnd

d).android:gravity 属性

  • 用于指定文字的对齐方式,可选值有:top/bottom/left/right/center,也可以用 | 来指定多个值

e).android:textSize 属性

  • 用于设置文字的大小,在Android中字体大小时使用 sp 作为单位的

f).android:hint 属性

  • 常在EditText中使用,可以指定一段提示性文字

g).android:maxLines 属性

  • 常在EditText中使用,可以设置最大行数

h).android:visibility 属性

  • 所有控件都可以使用,用于设置控件的可见属性

  • 可选值如下:
    visible:可见的,默认值
    invisible:不可见的,但仍占据空间
    gone:不可见的,且不占据任何空间

  • 也可以使用代码设置控件的可见性,使用 setVisibility() 方法,可以传入 View.VISIBLE View.INVISIBLE View.GONE 三个值

3.视图之布局

  • 布局是一种可用于放置很多控件的容器,可以按照一定规律调整内部控件的位置

(1).线性布局(LinearLayout)

  • 在线性布局中,vertical下,高度就不可以指定为match;horizontal下,宽度就不可以指定为match

  • 相关属性请看下面的 ab

(2).相对布局(RelativeLayout)

  • 可以通过相对定位的方式让控件出现在布局的任何位置

  • 相关属性请看下面的 c~g

(3).帧布局(FrameLayout)

  • 所有的控件都会默认摆放在布局的左上角

  • 相关属性请看下面的 a

(4).百分比布局

  • 该布局只是作为相对布局和帧布局的拓展,分别为:PercentFrameLayout 和 PercentRelativeLayout

  • 只需要在项目的 build.gradle 中添加百分比布局库的依赖即可在所有系统版本上的兼容性了,打开 app/build.gradle ,在 dependencies 闭包中添加如下内容的最后一行,然后点击 Sync Now 即可:

1
2
3
4
5
6
7
8
9
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
implementation 'com.android.support:percent:24.2.1'
}

(5).相关属性

a).android:layout_gravity 属性

  • 用于指定控件在布局中的对齐方式

b).android:layout_weight 属性

  • 用于使用比例的方式来指定控件的大小

  • 使用该属性时,可以将宽度修改为 0dp ,然后将该属性设置为1,则可以实现控件自动平分宽度

  • 在同一方向上也可以指定部分控件的该属性,使其自动适配

c).android:layout_alignParentXxx 属性

  • 其中XXX包含Left、Right、Top、Bottom

  • 使其相对于父元素定位

d).android:layout_centerInParent 属性

  • 使其相对于父元素居中

e).android:layout_above/below 属性

  • 可以让一个控件位于另一个控件的上方/下方

  • 使用该属性时作为参照物的控件需要写在最上边

f).android:layout_toLeftOf/toRightOf 属性

  • 可以让一个控件位于另一个控件的左侧/右侧

  • 使用该属性时作为参照物的控件需要写在最上边

g).android:layout_alignXXX 属性

  • 其中XXX包含Left、Right、Top、Bottom

  • 可以让一个控件的左/右/上/下边缘与另一个控件的边缘对齐

(6).引入布局或自定义控件

  • 通过引用布局,可以减少重复代码的编写

  • 当引入的布局中有些需要绑定点击事件时,就需要自定义控件来解决该问题了

a).引入布局

  • 新建一个布局文件并编写好,在 activity_main.xml 中使用以下语法来引入:

1
<include layout="@layout/title" />
  • 如果需要去除系统自带的标题栏,则需要在 MainActivity.java 文件中这样编写:

1
2
3
4
ActionBar actionbar = getSupportActionBar();
if(actionbar != null){
actionbar.hide();
}

b).自定义控件

  • 新建一个java文件来继承自 LinearLayout ,并重写其构造函数,如下:

1
2
3
4
5
6
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs){
super(context,attrs);
LayoutInflater.from(context).inflate(R.layout.title,this);
}
}
  • 在布局文件中添加该自定义控件,如下:

1
2
3
4
5
6
7
8
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 引入自定义控件 -->
<com.wrysmile.uilayouttest.TitleLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
  • 在自定义控件中编写适当的点击事件即可

4.视图之视图容器类

(1).ListView

  • 当程序有大量数据时,使用该空间可以将屏幕外的数据滚入,屏幕内的数据滚出

b).点击事件

  • 我们需要使用 setOnItemClickListener() 方法为ListView注册一个监听器,当执行点击事件时,就会回调 onItemClick() 方法

  • 在该方法中通过 position 来判断点击的是哪一个子项,然后执行相应的操作,可看下方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private List<Fruit> fruitList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 初始化水果数据
initFruits();
FruitAdapter adapter = new FruitAdapter(MainActivity.this,R.layout.fruit_item,fruitList);
ListView listView = (ListView) findViewById(R.id.list_view);
listView.setAdapter(adapter);
// 点击事件
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id){
Fruit fruit = fruitList.get(position);
Toast.makeText(MainActivity.this,fruit.getName(),Toast.LENGTH_SHORT).show();
}
});
}
}

(2).RecyclerView

  • 增强版的 ListView,具有同样的效果,还优化了性能问题

  • 和百分比布局类似,它也属于新增的控件,需要在项目的 build.gradle 中的 dependencies 闭包中添加如下内容:

1
implementation 'androidx.recyclerview:recyclerview:1.1.0'
  • 然后在 activity_main.xml 文件中这样编写即可

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

a).点击事件

5.制作 Nine-Patch 图片

  • Nine-Patch 图片可以让某一图片动态的跟随内容变换其长度和高度,而不是单纯的让图片平铺在背景上,主要用于实现类似于聊天框的效果

  • 将图片导入到项目中的 drawable-xhdpi 文件夹中,右击该图片,找到 Create 9-patch file 按钮,点击就进入了制作界面

  • 类似于 PS 的画笔功能,可以在四边用鼠标进行绘画一条直线,上边和左边是控制哪部分拉伸的,下边和右边是控制文本在哪里显示的

  • 修改好后右边可以看到效果图,满意保存替换源文件即可

2.ViewGroup 类

  • 它是一个抽象类,一般使用其子类来实现

  • 该类控制其子组件分布时依赖如下两个内部类:

(1).ViewGroup.LayoutParams 类

  • 该类主要控制布局的位置、高度和宽度的

  • 通常使用以下两个属性:
    android:layout_height:设置布局高度
    android:layout_width:设置布局宽度

  • 这两个属性有如下属性值:
    fill_parent:与父容器形同(API8以下使用)
    match_parent:与父容器相同
    wrap_content:包裹其自身的内容

(2).ViewGroup.MarginLayoutParams 类

  • 主要用于控制子组件的外边距

  • 通常包括四个属性:
    android:layout_marginLeft
    android:layout_marginTop
    android:layout_marginRight
    android:layout_marginBottom

  • API 17新增加:
    android:paddingStart
    android:paddingEnd

3.UI组件的层次结构

  • 如下图:
    Android01.png

4.控制UI界面的方法

(1).使用XML布局文件控制UI界面

  • res/layout 目录下编写XML布局文件

  • 在 Activity 中使用以下Java代码来显示XML文件中布局的内容

1
setContentView(R.layout.activity_main);

(2).在Java代码中控制UI界面

  • 新建的 MainActivity 类应该继承自 AppCompatActivity 类

  • 然后重写 onCreate 方法,且所有代码都得写在 super.onCreate(savedInstanceState); 语句的后面

  • 中间代码设置背景等样式

  • 还可以为按钮绑定单击响应事件

  • 最后需要将写好的内容通过以下代码加入 View 中——frameLayout.addView(text1);

(3).使用XML和Java混合控制UI界面

(4).开发自定义的View

五、碎片(手机平板的兼容)

1.基础

  • 碎片是一种可以嵌入在活动当中的 UI 片段,一般用于平板视图中

2.使用方式

(1).简单用法

  • 写一左一右两个布局文件

  • 新建一左一右两个类并让其继承自 Fragment 类,并在这两个类中通过 LayoutInflater 的 inflate() 方法将刚才定义的布局加载进来

  • 在 activity_main.xml 中使用 <fragment> 标签添加碎片

  • 注意:这里需要通过 android:name 属性来显示指明要添加的碎片类名

(2).动态添加碎片

a).方法

  • 创建待添加的碎片实例

  • 获取 FragmentManager,在活动中可以直接调用 getSupportFragmentManager() 方法得到

  • 开启一个事务,通过调用 beginTransaction() 方法开启

  • 向容器内添加或替换碎片,一般使用 replace() 方法实现,需要传入容器的id和待添加的碎片实例

  • 提交事务,调用 commit() 方法来完成

b).引申

  • 这里在 activity_main.xml 中加载布局可以使用 <FrameLayout></FrameLayout> 布局来写

(3).在碎片中返回栈

  • 作用:使碎片像活动一样有返回栈,按 Back键时会回到上一个页面

  • FragmentTransaction 中提供了一个 addToBackStack() 方法,可以用于将一个事务添加到返回栈中,一般传 null 即可,如下:

1
transaction.addToBackStack(null);

(4).碎片和活动的通信

  • 从布局文件中获取碎片的实例:

1
RightFragment rightFragment = (RightFragment) getSupportFragmentManager().findFragmentById(R.id.right_fragment);
  • 从碎片中调用活动的方法:

1
MainActivity activity = (MainActivity) getActivity();
  • 引申:碎片与碎片之间的通信可以先在一个碎片总得到与它相关联的活动,然后再通过这个活动去获取另外一个碎片的实例

  • 引申:当碎片需要使用 Context 对象时,也可以使用 getActivity() 方法

3.生命周期

  • 生命周期示意图如下:

(1).运行状态

  • 碎片可见,且与之关联的活动正处于运行状态

(2).暂停状态

  • 某一活动进入暂停状态,与之相关联的碎片也会处于暂停状态

(3).停止状态

  • 某一活动进入停止状态,与之相关联的碎片也会处于停止状态

  • 通过调用 FragmentTransaction 的 remove()和replace() 方法将碎片从活动中移除,但在事务提交前调用 addToBackStack() 方法,也会处于停止状态

(4).销毁状态

  • 某一活动被销毁时,与之相关联的碎片就会进入销毁状态

  • 通过调用 FragmentTransaction 的 remove()和replace() 方法将碎片从活动中移除,但在事务提交前没有调用 addToBackStack() 方法,也会处于销毁状态

(5).附加回调方法

  • onAttach():碎片和活动建立关联时调用

  • onCreateView():为碎片创建视图(加载布局)时调用

  • onActivityCreated():确保与碎片相关联的活动一定已经创建完毕时调用

  • onDestroyView():当与碎片相关联的视图被移除时调用

  • onDetach():当碎片和活动解除关联时调用

六、广播接收器

1.基础

  • 发送广播使用 Intent,而接收广播使用 Broadcast Receiver(广播接收器)

  • 广播类型分为:标准广播:一种完全异步执行的广播,广播发出以后,所有的广播接收器几乎在同一时刻接收到这条广播信息有序广播:一种同步执行的广播,广播发出以后,同一时刻只有一个广播接收器能收到这条广播信息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递

2.接收系统广播

(1).动态注册

  • 广播接收器想要监听什么广播,就使用 intentFilter 添加相应的 action 即可

  • 动态注册的广播接收器一定要取消注册才行,可以在 onDestroy() 方法中调用 unregisterReceiver() 方法来实现

  • 注意 Android 系统为了保护用户设备的安全和隐私,做了严格的规定:如果程序需要进行一些对用户来说比较敏感的操作,就必须在配置文件中声明权限才可以,否则程序会直接崩溃

附、问题集合

1.Android Studio启动后gradle下载太慢

  • 在尝试进行第一次编译程序时,发现gradle下载很慢,即使挂梯子也下载很慢,于是在网上找到需要的压缩包进行自行配置,需要的可以点击这里(提取码:m33b)直接进行下载

  • gradlegradle-6.1.1-all.zip(或者其他版本名字) 下载完成之后放着待用

  • 在你的c盘中找到该路径 C:\Users\你的用户名\.gradle\wrapper\dists\gradle-6.1.1-all(或者其他版本名字)\cfmwm155h49vnt3hynmlrsdst

  • 将其中的文件全部删除,再把刚刚下载好的文件粘贴进去

  • 重启 Android Studio,此时会自动跳过下载并开始解压i

2.给 Button 添加背景之后不显示

  • 添加背景后依然显示默认颜色,此时需要给布局处加入以下这一行

1
xmlns:app="http://schemas.android.com/apk/res-auto"
  • 然后在按钮的设置处加入以下这行

1
app:backgroundTint="@null" 

3.如何修改AS的模拟器位置

  • C:\user\用户名\.android\avdC:\Android\.android\avd 中找到模拟器文件夹,一个文件夹对应一个配置文件

  • 将模拟器文件夹移动到你所需要移动到的位置,我这里移动到了这里:

1
D:\Android\avd\Pixel_3a_API_30_x86.avd
  • 修改C盘的配置文件中的path字段即可,如下:

1
2
3
4
avd.ini.encoding=UTF-8
path=D:\Android\avd\Pixel_3a_API_30_x86.avd
path.rel=avd\Pixel_3a_API_30_x86.avd
target=android-30
  • 打开AS测试模拟器是否可以正常运行

4.如何修改Android项目的包名

当你引用别人的项目时,会发现包名是别人的包名,想要修改成为自己的包名按以下操作即可

  • 建议在Project格式下,找到 app/src/main/java 路径,在这之下就是项目的包名,点击右上角的齿轮图标,如下图:
    Android03.png

  • Compact Middle Packages 选项取消勾选,更改后如下图:
    Android04.png

  • 点击你需要更改的包名,按快捷键 shift + F6 进行重命名操作,并选择 Rename package,如下图:
    Android05.png

  • 填好包名后点击 Refactor,此时下方会弹出小框提示哪些文件需要一并修改,这里AS会自动检测并帮我们自动修改,点击 Do Refactor,如下图:
    Android06.png

  • 完成后修改 app 目录下的 build.gradle 文件中的 applicationId 字样,成功后点击 Sync now 修改完成,如下图:
    Android07.png

  • https://www.jianshu.com/p/14d92eecfada

  • 双击返回键退出应用:https://blog.csdn.net/qq_23179075/article/details/60587202

  • 实现沉浸式状态栏:https://blog.csdn.net/weixin_43796132/article/details/102250632