Day1 Android简介

快捷键

运行

  1. 运行:Shift+F10
  2. 终止:Ctrl+F2

日志

  1. 生成TAG常量:在方法外输入logt + TAB
  2. 打印不同级别的日志:在方法内输入logd/logi/logw/loge + TAB

第一章 开始启程--你的第一行Android代码

1.1 Android的四大组件

  1. 活动(Activity) :
    1. 活动是所有Android应用程序的门面,凡是在应用中你看得到的东西 , 都是放在活动中的
  2. 服务(Service):
    1. 你无法看到它,但它会一自在后台默默地运行
    2. 即使用户退出了应用,服务仍然是可以继续运行的。
  3. 广播接收器(BroadcastReceiver) :
    1. 广播接收器允许你的应用接收来自 各处的广播消息, 比如电话、短信等
    2. 当然你的应用同样也可以向外发出广播消息
  4. 内容提供器(ContentProvider):
    1. 内容提供器则为应用程序之间共享数据提供了可能
    2. 比如你想要读取系统电话部中的联系人,就需要通过内容提供器来实现。

1.2 项目目录

image-20220830143913960

1.2.1 .gradle和idea

  1. 这两个目录下放置的都是Android Studio自动生成的一些文件
  2. 我们无须关心、 也不要去手动编辑

1.2.2 app

  1. 项目中的代码、资源等内容几乎都是放置在这个目录下的、 我们后面的开发工作也基本都是在这个目录下进行的, 待会儿还会对这个目录单独展开进行讲解

1.2.3 build

  1. 这个目录你也不需要过多关心, 它主要包含了一些在编译时自动生成的文件

1.2.4 gradle

  1. 这个目录下包含了gradle wrapper的配置文件
  2. 使用gradle wrapper的方式不需要提前将gradle下载好,而是会自动根据本地的缓存情况决定是否需要联网下载gradle
  3. Android Studio默认没有启用gradle wrapper的方式,如果需要打开,可以点击Android Studio导航栏|File|Settings|Build, Execution, Deployment | Gradle,进行配置更改。

1.2.5 . gitignore

  1. 这个文件是用来将指定的目录或文件排除在版本控制之外的

1.2.6 build.gradle

  1. 这是项目全局的gradle构建脚本 , 通常这个义件中的内容是不需要修改的

1.2.7 gradle.properties

  1. 这个文件是全局的gradle配置文件 ,存这里配置的屈性将会影响到项目中所有的gradle编译脚本

1.2.8 gradlew和gradlew.bat

  1. 这两个文件是用来在命令行界面中执行gradle命令的
  2. 其中gradlew是在Linux或Mac系统中使用时,gradJew.bat是在Windows系统中使用的

1.2.9 HelloWorld.iml

  1. iml文件是所有lntelliJ IDEA项目都会自动生成的一个文件(Android Studio是基T lntelliJ IDEA开发的)
  2. 用于标识这是一个lntelliJ IDEA顶目
  3. 我们不需要修改这个文件中的任何内容

1.2.10 local.properties

  1. 这个文件用于指定本机中的Android SDK路径
  2. 通常内容都是自动生成的, 我们并不需要修改,除非你本机中的Android SOK位置发生了变化 ,那么就将这个文件中的路径改成新的位置即可

1.2.11 settings.gradle

  1. 这个文件用于指定项目中所有引入的模块
  2. 由于HelloWorld项目中就只有一个app模块,因此该文件中也就只引入了app这一个模块
  3. 通常情况下模块的引入都是自动完成的,需要我们手动去修改这个文件的场景可能比较少

1.3 app目录

image-20220830144532751

1.3.1 build

  1. 这个目录和外层的build目录类似,主要也是包含了一些在编译时自动生成的文件,不过它里面的内容会更多更杂,我们不需要过多关心

1.3.2 libs

  1. 如果你的项目中使用到了第三方jar包,就需要把这些jar包都放在libs目录下
  2. 放在这个目录下的jar包都会被自动添加到构建路径里去。

1.3.3 androidTest

  1. 此处是用来编写Android Test测试用例的, 可以对项目进行一些自动化测试

1.3.4 java

  1. 毫无疑问,java目录是放置我们所有Java代码的地方
  2. 展开该目录,你将看到我们刚才创建的HelloWorldActivity文件就在里面。

1.3.5 res

Android布局下的res文件夹:image-20220830150114123

项目目录下的res文件夹:image-20220830150302717

  1. 项目中使用到的所有图片、布局 、字符串等资源都要存放在这个目录下。
  2. 当然这个目录下还有很多子目录,所以你不用担心会把整个res目录弄得乱糟糟的
    1. drawable目录:图片,多个文件夹用于适用不同设备的分辨率
    2. layout目录:布局
    3. mipmap目录:应用图标,多个文件夹用于适用不同设备的分辨率
    4. values目录:字符串、样式、颜色等配置

1.3.6 AndroidManifest.xml

  1. 这是你整个Android项目的配置文件
  2. 你在程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用程序添加权限声明。
  3. 由于这个文件以后会经常用到,我们用到的时候再做详细说明

1.3.7 test

  1. 此处是用来编写Unit Test测试用例的,是对项目进行自动化测试的另一种方式

1.3.8 .gitignore

  1. 这个文件用于将app 模块内的指定的目录或文件排除在版本控制之外,作用和外层的.gitignore文件类似

1.3.9 app.iml

  1. IntelliJ IDEA项目自动生成的文件, 我们不需要关心或修改这个文件中的内容。

1.3.10 build.gradle

  1. 这是app模块的gradle构建脚本,这个文件中会指定很多项目构建相关的配置

1.3.11 proguard-rules.pro

  1. 这个文件用于指定项目代码的混淆规则
  2. 当代码开发完成后打成安装包文件,如果不希望代码被别入破解, 通常会将代码进行混淆, 从而让破解者难以阅读

1.4 项目的运行

1.4.1 AndroidManifest.xml:注册活动

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.hw2_activity">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Hw2_Activity"
tools:targetApi="31">
<!--对资源文件的引用-->

<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!--这两句话表示, MainActivity是本项目的主活动, 在手机上点击应用图标, 首先应该启动这个活动-->
</intent-filter>
</activity>
</application>

</manifest>


1.4.2 MainActivity.java

public class MainActivity extends AppCompatActivity {
//AppCompatActivity是一种向下兼容的Activity
//可以将Activity在各个系统版本中增加的特性和功能最低兼容到Android 2.1版本
@Override
protected void onCreate(Bundle savedInstanceState) {
//活动被创建时必须执行的方法
super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);
//给当前活动引入了一个activity_main布局
}
}

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

<!--用于显示文字的一个控件-->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

1.4.4 res/values/strings.xml

<resources>
<string name="app_name">hw2_Activity</string>
</resources>
  1. 定义了一个应用程序名的字符串
  2. 引用方式:
    1. 在代码中:R.string.app_name
    2. 在XML中:@string/app_name
  3. string可以换成drawable、mipmap、layout等,用于引用其它文件夹内的资源

1.4.5 build.gradle

image-20220830210700816

外层的build.gradle文件

plugins {
id 'com.android.application' version '7.2.2' apply false
id 'com.android.library' version '7.2.2' apply false
//构件中可能会使用到的插件的声明
}

task clean(type: Delete) {
delete rootProject.buildDir
}

app/build.gradle文件

plugins {
id 'com.android.application'
//使用插件
//.application表示是应用程序模块
//.library表示是库模块
}

android {
compileSdk 32 //指定项目的编译版本

defaultConfig { //项目的更多细节配置
applicationId "com.example.hw2_activity" //指定项目的包名
minSdk 21 //最低兼容的Android系统版本
targetSdk 32 //在该目标版本上已经做过充分的测试,系统会为程序启用该版本的最新功能和特性
versionCode 1 //指定项目的版本号
versionName "1.0" //指定项目的版本名
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release { //生成正式版安装文件的配置
minifyEnabled false
// 是否对项目的代码进行混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
//混淆时使用的规则文件
//proguard-android-optimize.txt是所有项目通用的混淆规则, 在Android SDK目录下
//proguard-rules.pro当前有项目特有的混淆规则, 在当前项目的根目录下
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies { //项目的所有依赖关系
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

三种依赖关系:

  1. 本地依赖:可以对本地的Jar包或目录添加依赖关系
  2. 库依赖:可以对项目中的库模块添加依赖关系
  3. 远程依赖:可以对jcenter库上的开源项目添加依赖关系
    1. com.google.android.material:域名
    2. :material:组名
    3. :1.4.0:版本号

1.5 日志工具Log

1.5.1 日志工具类的使用

Android中的日志工具类是Log (android.util.Log). 这个类中提供如下5个方法来供我们打印日志

  1. Log.v():用于打印那些最为琐碎的意义最小的日志信息。对应级别verbose,是Android日志里面级别最低的一种
  2. Log.d():用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别debug,比verbose高一级
  3. Log.i():用于打印一些比较重要的数据,这些数据应该是你非常想看到的,可以帮你分析用户行为数据。 对应级别info, 比debug高一级
  4. Log.w():用于打印一些警告信息,提示程序在这个地力可能会有潜在的风险,最好去修复一下这些出现瞥告的地方。对应级别warn,比info高一级
  5. Log.e():用于打印程序中的错误信息,一般都代表程序出现严重问题了,必须尽快修复。对应级别error,比warn高一级
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d("MainActivity", "onCreate execute");
//第一个参数为tag, 一般传入当前类名
//第二个参数为msg, 为想要打印的具体内容
}

1.5.2 Logcat的使用

image-20220830154354724

  1. 过滤器:image-20220830154410752

  2. 自定义过滤器:在上述图标下拉栏中,选择Edit Filter Configuration

    1. Log Tag:按照日志的TAG过滤

    image-20220830154509417

  3. 按照日志的级别过滤:只会显示高于选中级别的日志

    image-20220830154655780

Day2 Android基础UI开发

第二章 先从看得到的入手--探究活动

2.1 活动的基本用法

2.1.1 手动创建活动

  1. 在下图目录下,右击|新建|Activity|Empty Activity

    image-20220830162047076

  2. 不要勾选

    1. Generate a Layout File:自动创建一个对应的布局文件
    2. Lancher Activity:自动将创建的活动MainActivity设置为当前项目的主活动

    image-20220830162335459

2.1.2 创建和加载布局

  1. 在下图目录下,右击|新建|Layout Resource File

    image-20220830162420500

  2. 布局文件的首字母要小写

    image-20220830162517505

  3. first_layout.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">
    <!--match_parent表示让当前元素和父元素一样宽-->
    <!--wrap_content表示当前元素的高度只要能刚好包含里面的内容就可以-->

    <Button
    android:id="@+id/button"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Button" />
    <!--@+表示在xml中定义一个id-->
    </androidx.constraintlayout.widget.ConstraintLayout>
  4. FirstActivity.java

    public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.first_layout);
    //给当前活动加载一个布局
    }
    }

2.1.3 在AndroidMainfest文件中注册

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.hw2_activity">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Hw2_Activity"
tools:targetApi="31">

<activity
android:name=".FirstActivity"
android:exported="true"
android:label="This is the first activity">
<!--注意android:exported必须为true-->
<!--android:label是当前活动的标题-->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

</manifest>

2.1.4 在活动中使用Toast

  1. Toast是Android系统提供的一种非常好的提醒方式

  2. 在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间,

  3. 我们现在就尝试一下如何在活动中使用Toast:通过Button触发Toast,修改FirstActivity.java

    public class FirstActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.first_layout);

    Button button1 = (Button) findViewById(R.id.button);
    // 通过button的id,获取其实例
    // 这个值是在first_layout.xml中通过android:id属性指定的
    // findViewById返回的是View对象, 需要将其向下转化为Button对象
    button1.setOnClickListener(new View.OnClickListener() {
    // 获得按钮实例后, 通过调用setOnClickListener()方法为按钮注册一个监听器
    // 点击按钮时, 就会执行监听器中的onClick()方法
    @Override
    public void onClick(View view) {
    Toast.makeText(FirstActivity.this, "You clicked Button ",Toast.LENGTH_SHORT).show();
    // 通过静态方法makeText()创建一个Toast对象, 然后调用show()方法将其显示出来即可
    // makeText()的三个参数:
    // context: Toast要求的上下文, 由于活动本身就是一个Context对象, 因此这里直接传入this
    // text: Toast显示的文本内容
    // Toast显示的时长, 有两个选择: Toast.LENGTH_SHORT/Toast.LENGTH_LONG
    }
    });
    }
    }

2.1.5 在活动中使用Menu

  1. res目录下,新建一个目录menu

  2. 右击menu文件夹|新建|Menu Resource File,文件名输入main

  3. 修改main.xml文件

    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
    android:id="@+id/add_item"
    android:title="Add"/>

    <item
    android:id="@+id/remove_item"
    android:title="Remove"/>
    <!--<item>标签就是用来创建具体的某一个菜单项-->
    <!--android:id表示唯一标识符-->
    <!--android:title给这个菜单项指定一个名称-->
    </menu>
  4. 修改FirstActivity.java文件,重写onCreateOptionMenu()方法

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.main, menu);
    // 通过getMenuInflater()方法能够得到MenuInflater对象
    // 再调用它的inflate()方法就可以给当前活动创建菜单了
    // inflate的两个参数:
    // 第一个: 指定通过哪一个资源文件来创建菜单
    // 第二个: 指定我们的菜单项将添加到哪一个Menu对象中
    return true;
    // 返回true: 表示允许创建的菜单显示出来
    }
  5. 定义菜单响应事件:重写onOptionsItemSelected函数

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
    switch (item.getItemId()){ // 判断点击的是哪一个菜单项
    case R.id.add_item:
    Toast.makeText(this, "You clicked Add", Toast.LENGTH_SHORT).show();
    break;
    case R.id.remove_item:
    Toast.makeText(this, "You clicked Remove", Toast.LENGTH_SHORT).show();
    break;
    default:
    break;
    }
    return true;
    }

2.1.6 销毁一个活动

  1. 调用Activity类的finish()方法即可,修改Button监听器中的代码

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Toast.makeText(FirstActivity.this, "You clicked Button ",Toast.LENGTH_SHORT).show();
    finish();
    }
    });

2.2 使用Intent在活动之间穿梭

2.2.1 使用显式Intent

  1. 右击com.example.hw2_Activity包|新建|Activity|Empty Activity,创建一个新活动

  2. 这次勾选Generate Layout File,并将布局文件起名为second_layout,但是不要勾选Launcher Activity

  3. 进入布局文件,新建一个Button控件

  4. 注意:所有Activity都需要在AndroidManifest.xml中注册,这里系统已经帮我们注册好了

    <!--注册SecondActivity-->
    <activity
    android:name=".SecondActivity"
    android:exported="false" />

    <!--注册FirstActivity-->
    <activity
    android:name=".FirstActivity"
    android:exported="true"
    android:label="This is the first activity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    </activity>
  5. 使用显式Intent启动活动:修改FirstActivity.java中的按钮点击事件

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    // 第一个参数: 启动活动的上下文
    // 第二个参数: 目标活动
    startActivity(intent);
    }
    });

2.2.2 使用隐式Intent

  1. 相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。

  2. 什么叫作合适的活动呢?简单来说就是可以响应我们这个隐式Intent的活动。

  3. 修改AndroidManifest.xml,让SecondActivity能够响应隐式Intent

    <!--注册SecondActivity-->
    <activity
    android:name=".SecondActivity"
    android:exported="false">
    <intent-filter>
    <action android:name="com.example.hw2_activity.ACTION_START"/>
    <!--指明当前活动可以响应com.example.hw2_activity.ACTION_START这个action-->
    <category android:name="android.intent.category.DEFAULT"/>
    <!--指明当前活动能够响应的Intent中还可能带有的category-->
    <!--只有<action>和<category>同时匹配上Intent中指定的action和category时,这个活动才能响应该Intent-->
    </intent-filter>
    </activity>
  4. 修改FirstActivity.java中的按钮点击事件

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent("com.example.hw2_activity.ACTION_START");
    intent.addCategory("com.example.hw2_activity.MY_CATEGORY");
    // 每个Intent只能有一个Action, 但是可以有多个Category
    // 只有当Activity同时匹配上Action和所有的Category时, 才会响应Intent
    startActivity(intent);
    // android.intent.category.DEFAULT是默认category
    // 在调用startActivity()方法时会自动将这个category添加到Intent中
    }
    });

2.2.3 更多隐式Intent的用法

  1. 打开网页:修改FirstActivity.java中的按钮点击事件

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent(Intent.ACTION_VIEW);
    // Intent.ACTION_VIEW是Android系统内置的动作
    // 其常量值为android.intent.action.VIEW
    intent.setData(Uri.parse("http://www.baidu.com"));
    // 使用Uri.parse()方法,将一个网址字符串解析成一个Uri对象
    // 调用Intent的setData()方法, 将这个Uri对象传递进去
    startActivity(intent);
    }
    });
  2. setData()方法:指定当前Intent正在操作的数据

    1. 与此对应,我们还可以在< intent-filter >标签中再配置一个< data >标签,用于更精确地指定当前活动能够响应什么类型的数据。< data >标签中主要可以配置以下内容。
      1. android:scheme:用于指定数据的协议部分,如上例中的http部分
      2. android: host:用于指定数据的主机名部分,如上例中的www.baidu.com部分
      3. android:port:用于指定数据的端口部分,一般紧随在主机名之后
      4. android:path:用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容
      5. android:mimeType:用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
    2. 只有< data >标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent
    3. 不过一般在< data >标签中都不会指定过多的内容
      1. 如上面浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent了
  3. 写一个能够响应打开网页的活动

    1. 新建一个活动ThirdActivity
    2. 修改AndroidManifest.xml
    <!-- 注册ThirdActivity -->
    <activity
    android:name=".ThirdActivity"
    android:exported="true">
    <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:scheme="http"/>
    </intent-filter>
    </activity>
  4. 拨打电话

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent(Intent.ACTION_DIAL);
    intent.setData(Uri.parse("tel:10086"));
    startActivity(intent);
    }
    });

2.2.4 向下一个活动传递数据

  1. 给出数据:putExtra()方法

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    String data = "Hello SecondActivity";
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    intent.putExtra("extra_data", data);
    // 第一个参数是键, 第二个参数是传递的数据
    startActivity(intent);
    }
    });
  2. 接收数据:getStringExtra()方法

    public class SecondActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.second_layout);
    Intent intent = getIntent();
    String data = intent.getStringExtra("extra_data");
    // 字符串型数据就是getStringExtra, 整型数据就是getIntExtra
    Log.d("SecondActivity", data);
    }
    }

2.2.5 返回数据给上一个活动

  1. 需要返回数据的启动新活动:startActivityForResult()方法

    button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
    startActivityForResult(intent, 1);
    }
    });
  2. 返回数据:setResult()方法

    button2.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    // 第一个参数: 向上一个活动返回处理结果, 一般只使用RESULT_OK/RESULT_CANCELED
    // 第二个参数: 带有数据的Intent
    finish();
    }
    });
  3. 获取返回数据:onActivityResult()方法

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    // requestCode: 启动新活动是传入的请求码
    // resultCode: 返回数据时传入的处理结果
    // data: 携带着返回数据的Intent
    switch (requestCode){
    case 1:
    if(resultCode == RESULT_OK){
    String returnedData = data.getStringExtra("data_return");
    Log.d("FirstActivity", returnedData);
    }
    break;
    default:
    break;
    }
    }
  4. 按返回键返回:onBackPressed()方法

    @Override
    public void onBackPressed() { // 按返回键返回
    Intent intent = new Intent();
    intent.putExtra("data_return", "Hello FirstActivity");
    setResult(RESULT_OK, intent);
    finish();
    }

2.3 活动的生命周期

2.3.1 返回栈 Back Stack

  1. 其实Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)
  2. 栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置
  3. 系统总是会显示处于栈顶的活动给用户

2.3.2 活动状态

  1. 运行状态:活动位于返回栈栈顶
  2. 暂停状态:活动不在栈顶,但是仍然可见
  3. 停止状态:活动不在栈顶,且不可见
  4. 销毁状态:活动不在返回栈中

2.3.3 活动的生命周期

2.3.3.1 对应的7个回调方法

image-20220830192712259

  1. onCreate():它会在活动第一次被创建的时候调用。
    1. 你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等
  2. onStart():这个方法在活动由不可见变为可见的时候调用
  3. onResume():这个方法在活动准备好和用户进行交互的时候调用。
    1. 此时的活动一定位于返回栈的栈顶,并且处于运行状态
  4. onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用。
    1. 我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据
    2. 但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用
  5. onStop():这个方法在活动完全不可见的时候调用。
    1. 它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行
  6. onDestroy():这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态
  7. onRestart():这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了
2.3.3.2 将活动注册为对话框
<activity
android:name=".DialogActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Dialog"/>

2.3.4 活动被回收了怎么办

  1. 存储临时数据:onSaveInstanceState()

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
    super.onSaveInstanceState(outState, outPersistentState);
    String temoData = "Something you just Typed";
    outState.putString("data_key", temoData);
    }
  2. 恢复临时数据:onCreate()中的savedInstanceState参数

    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d(TAG,"onCreate");
    setContentView(R.layout.activity_main);

    if(savedInstanceState != null){
    String tempData = savedInstanceState.getString("data_key");
    Log.d(TAG, tempData);
    }
    }

2.4 活动的启动模式

  1. 可以在AndroidManifest.xml中,通过给< activity >标签指定android:launchMode属性,来指定启动模式

2.4.1 standard

  1. 默认启动模式
  2. 不检查栈中有是否已有这个Activity,总会创建⼀个新Activity,并push到栈顶

image-20220830195908952

2.4.2 singleTop

  1. 检查栈顶判断是否需要新建Activity
  2. ⾮栈顶的元素不会检查,所以当FirstActivity不位于栈顶时,再次startActivity(FirstActivity)还会再创建⼀个FirstActivity

image-20220830200014909

2.4.3 singleTask

  1. 每次启动该活动时系统⾸先会在返回栈中检查是否存在该Activity的实例
  2. 如果发现已经存在则直接使⽤该实例,并把在这个Activity之上的所有Activity统统出栈

image-20220830200117581

2.4.4 singleInstance

  1. 启用一个新的返回栈来管理指定为singleInstanceActivity

  2. 使⽤场景:跨进程(app)间的Activity实例共享,不管是哪个应⽤程序来访问这个Activity,都共⽤同⼀个返回栈

    image-20220830200531526

2.5 活动的最佳实践

2.5.1 知晓当前是在哪一个活动

  1. 首先,新建一个BaseActivity类,继承自AppCompatActivity

  2. 重写OnCreate()方法

    public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("BaseActivity", getClass().getSimpleName());
    }
    }
  3. BaseActivity称为hw2_activity项目中所有活动的父类

    1. 修改FirstActivity、SecondActivity、ThirdActivity,让他们不再继承于AppCompatActivity,而是继承自BaseActivity

2.5.2 随时随地退出程序

  1. 新建一个类ActivityCollector作为活动管理器

    public class ActivityCollector {
    public static List<Activity> activities = new ArrayList<>();

    public static void addActivity(Activity activity){
    activities.add(activity);
    }

    public static void removeActivity(Activity activity){
    activities.remove(activity);
    }

    public static void finishAll(){
    Log.d("ActivityCollector", "finishAll"+activities.size());
    for(Activity activity : activities){
    if(!activity.isFinishing())
    activity.finish();
    }
    activities.clear();

    android.os.Process.killProcess(android.os.Process.myPid());
    // 只能杀掉当前进程
    }
    }
  2. 修改BaseActivity中的代码

    public class BaseActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("BaseActivity", getClass().getSimpleName());

    ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    ActivityCollector.removeActivity(this);
    }
    }
  3. ThirdActivity界面中点击按钮直接退出程序

    public class ThirdActivity extends BaseActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.third_layout);

    Button button3 = (Button) findViewById(R.id.button3);
    button3.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
    Log.d("ThirdActivity", "onClick: ");
    ActivityCollector.finishAll();
    }
    });
    }
    }

2.5.3 启动进程的最佳写法

  1. 启动Secondary进程时,我们可能并不知道它需要哪些参数,要么去问负责这个活动的同学,要么自己去看代码

  2. 我们只需要在Secondary中封装一个方法actionStart,即可方便的告诉大家,Secondary启动时需要哪些参数

    public class SecondActivity extends BaseActivity {
    public static void actionStart(Context context, String data1, String data2){
    Intent intent = new Intent(context, SecondActivity.class);
    intent.putExtra("param1", data1);
    intent.putExtra("param2", data2);
    context.startActivity(intent);
    }
    }

第三章 软件也要拼脸蛋--UI开发的点点滴滴

3.1 控件与布局

  1. UI控件:TextView, ImageView, Button, ProgressBar
  2. UI布局:LinearLayout, RelativeLayout, FrameLayout,

image-20220830204018378

image-20220830204024868

3.2 常用控件

3.2.1 TextView

image-20220830204858307

image-20220830204911012

  1. layout_width:控件的宽
  2. layout_height:控件的⾼
  3. wrap_content:表示和⾃身内容⼀样的⻓度
  4. match_parent:表示和⽗组件⼀样的⻓度

image-20220830204243973

3.2.2 px、dp、dpi、density 与 sp

  1. px:pixel,1px代表屏幕上的⼀个物理像素点
  2. dpi:dots per inch,对角线每英寸的像素点的个数;该值越大表示屏幕越清,\(dpi=\frac{\sqrt{ {height}^2+{width}^2} }{size}\)
  3. density\(density=\frac{dpi}{60}\)
  4. dp/dip:density-independent pixel,设备无关像素,\(dp=\frac{px}{density}\)
  5. sp:scale-independent pixel,与缩放⽆关的抽象像素
    1. 与dp近似,但除了受屏幕密度影响外,还受到⽤户字体大小影响(正相关)

3.2.3 EditText

image-20220830204922564

image-20220830204836168

  1. 监听输⼊内容变化:TextWatcher

image-20220830205018365

3.2.4 ImageView

image-20220830205158604

  1. 静态设置

    1. android:src:指定drawable(本地图片)或bitmap资源(网络图片)
    2. android:background:指定ImageView背景(如color)
    3. android:scaleType:设置图片如何缩放以适应ImageView大小;
      1. 参数如center,centerCrop等
  2. 动态设置

    1. setImageResource:添加资源

      mImageView.setImageResource(R.drawable.icon_search);
    2. 解析成bitmap后,setRotate设置旋转等

  3. svgpng相比有何优势

    1. 抗拉伸
    2. 适配分辨率友好
    3. 占⽤空间小

3.2.5 Dialogs(自学内容)

image-20220830205419562

image-20220830205454275

  1. https://developer.android.com/guide/topics/ui/dialogs
  2. 如何生成,展示,隐藏⼀个Dialog?
  3. 如何⾃定义Dialog样式,添加Button和Listener?

3.3 基本布局

3.3.1 LinearLayout

  1. android:orientation:表示线性布局排列⽅向

    1. 可选verticalhorizontal
  2. android:layout_gravity:表示指定控件在layout中的对⻬⽅式

    1. center_vertical只在orientation=“horizontal”时⽣效
    2. center_horizontal只在orientation=“vertical”时⽣效

    image-20220830205626934

  3. android:layout_weight:使用比例的方式指定控件的大小

    1. 每个控件在排列⽅向上尺寸占比为:self weight / total weight
    2. 如下图两个View的weight都为1,则两个View的宽度与屏幕宽度⽐均为\(\frac{1}{1+1}=\frac{1}{2}\)
    3. 两个View的layout_width的规范写法为0dp
    4. 如果⼀些控件未指定weight,则这些控件按指定width或height展示。其余指定weight的控件对剩余屏幕宽度或高度进行分割

    image-20220830210035726

3.3.2 RelativeLayout

image-20220830210142687

image-20220830210118803

3.3.3 padding 与 margin

image-20220830210223617

3.3.4 FrameLayout(自学内容)

image-20220830210319103

3.3.5 ConstraintLayout

  1. 对View A在水平和垂直两个方向上指定限制,每⼀方向上⾄少指定⼀个 限制,限制标的可以是其他View或父View

    image-20220830210417516

  2. 如果对ViewB同时添加了app:layout_constraintRight_toRightOf=“@id/viewA”和 app:layout_constraintLeft_toLeftOf=“@id/viewA”表示什么含义?

  3. ConstraintLayout的使⽤场景(拓展)

    1. N等分布局
    2. 角度布局
    3. 超长限制优化
    dependencies {
    implementation "androidx.constraintlayout:constraintlayout:2.1.4"
    }

3.3.6 自定义控件

  1. 可继承任意Android控件,在此基础上添加或重写功能

  2. 更好的封装

    1. 举例:六个可输入方格、选中框、光标等

    2. ⼀种实现:作为⼀个EditText,⾃定义View(SixWordEditText)继承EditText

      1. Override TextViewonDraw⽅法,绘制每个方格样式和⽂字

      2. TextWatcher监听afterTextChanged

      3. SixWorkEdtiTextXML中引⽤

        <com.ss.meetx.roomui.widget.SixWordEditText
        android:id="@+id/accessCodeEditText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
  3. 提高复用性

    1. 如何在XML中实现下面的UI?使⽤什么布局和控件?层级是怎样的?

      image-20220830211137204

      1. input_view.xml

      2. 构造SearchTextLayout

        // constructor
        public SearchTextLayout(Context context, AttributeSet attrs) {
        LayoutInflater.from(context).inflate(R.layout.input_view, this)
        }
      3. 引用SearchTextLayout

        <com.ss.meetx.roomui.widget.input.SearchTextLayout
        android:id=“@+id/inputViewSearch"
        android:layout_width=“0dp"
        android:layout_height=“52dp"/>

3.4 RecyclerView

3.4.1 基本布局:ScrollView

  1. 滚动布局,默认垂直方向滑动,也⽀持水平方向滑动HorizontalScrollView

  2. 直接子View只能有⼀个

  3. 如果⽤ScrollView实现右侧的滑动列表应该怎么做?

  4. 开发上有什么不便?性能上有什么弊端?

    1. 重复写n个View,动态添加View比较复杂
    2. 初始化时会将所有数据项全部加载出来,没有回收和复同;导致内存占用大和OOM
    3. 用户体验是加载速度慢,卡顿

3.4.2 RecyclerView

  1. 核心:View HolderAdapterRecycler View

  2. Item Decorator:Item之间Divider(分割线)

  3. Item Animator:添加删除Item的动画

3.4.3 LayoutManager

  1. LinearLayoutManager(左图)

    1. 线性造型,类似ListView的功能
    2. 支持上下或左右滑动,每⼀行或⼀列上仅有⼀个item
  2. GridLayoutManager(中图)

    1. 网格造型,每个item在滑动方向上的尺寸相同
    2. 可以通过setSpanSizeLookup和getSpanSize,指定条件(如item中text宽度,item的position等),来控制该item占几个位置(即每⼀行有几个item)
  3. StaggeredGridLayoutManager(右图)

    1. 瀑布流造型,每个item的尺寸可不相同,错落式布局
    2. 在其constructor中可指定滑动方向和行数(或列数)

    image-20220830211927154

  4. 自定义LayoutManager(拓展)

    1. 继承LayoutManager
    2. 重写generateDefaultLayoutParams⽅法,直接返回⼀个⻓宽都为WRAP_CONTENTLayoutParams即可;
    3. 重写onLayoutChildren⽅法,在这⾥⾯布局Items(显示出来);具体包括分离和回收有效items(detachAndScrapAttachedViews),获取需要布局的items(可见的),再通过addView将这些item添加回去。然后对其测量 (measureChild)确定View的宽高,
    4. 使⽤layoutDecorated确定View摆放的位置,并设置跟随滑动放缩比例
    5. 重写canScrollHorizontallycanScrollVertically方法,使它⽀持⽔平或垂直滚动;
    6. 重写scrollHorizontallyByscrollVerticallyBy,并在这里处理滚动工作;

    image-20220830212145783

3.4.4 RecyclerView使用示例

3.4.4.1 添加依赖
  1. app/build.gradle中,添加RecyclerView的依赖
implementation 'androidx.recyclerview:recyclerview:1.1.0'
3.4.4.2 添加相关内容,表示RecyclerView中每一个独立的item
  1. 创建JavaItemData,表示每一个item存储的数据
public class ItemData {
private String title;
private String link;

public ItemData(String title, String link) {
this.title = " " + title;
this.link = link;
}

public String getTitle() {
return title;
}

public String getLink() {
return link;
}
}
  1. 创建layout布局文件recyclerview_item.xml,表示每一个item的布局
<?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="wrap_content"
android:background="@drawable/text_view_shape">

<TextView
android:id="@+id/RecyclerView_Item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:drawableBottom="@drawable/text_view_shape"
android:fontFamily="sans-serif-thin"
android:maxLines="1"
android:text="文章的标题"
android:textAlignment="viewStart"
android:textColor="#000000"
android:textSize="24sp"
android:textStyle="bold"
android:typeface="sans"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
  1. 创建JavaMyViewHolder,用于存储每个item的控件,控件都有哪些,在recyclerview_item.xml中定义
public class MyViewHolder extends RecyclerView.ViewHolder{
public TextView textView;

public MyViewHolder(View itemView){
super(itemView);
this.textView = itemView.findViewById(R.id.recyclerview_item);
}
}
3.4.4.3 添加RecyclerView的Adapter
  1. 创建JavaMyRecyclerViewAdapter,为RecyclerView控件的Adapter
    1. 这个类继承自RecyclerView.Adapter< MyViewHolder >
    2. 这个类会重写View.OnClickListener
  2. 类中包含了Adapter所在的Context,每个item包含的数据
    1. 这些是在构造函数中需要传入的
  3. 创建一个方法setItem(int position, ItemData item),表示修改position地方的数据
    1. 先修改itemList中的数据
    2. 然后调用this.notifyItemChanged(position)方法,修改item的控件
  4. 重写多个方法,实现Adapter的功能
    1. onCreateViewHolder()item框创立时, 调用该方法
      1. 根据item对应的layout文件recyclerview_item.xml,创建每个item对应的View视图
      2. View视图设置Listener
      3. itemView中获取MyViewHolder并返回
    2. onAttachedToRecyclerView():将RecycleView附加到Adapter上时, 调用该方法
      1. 设置当前Adapter负责的RecyclerView
    3. onDetachedFromRecyclerView():将RecycleViewAdapter解除时, 调用该方法
      1. 设置当前Adapter负责的RecyclerView
    4. onBindViewHolder()item显示时, 调用该方法
      1. 根据item的位置,设置当前item的每个组件的值
    5. getItemCount()item的数量
  5. 重写onClick()方法,处理RecyclerView的点击事件
    1. 添加自定义接口OnItemClickListener,处理item的点击事件
      1. 需要实现的方法:void onItemClick(RecyclerView parent, View view, int position, ItemData data);
    2. 通过recyclerView.getChildAdapterPosition()方法,获取当前点击的item的位置
    3. 执行具体实现的onItemClick()方法
public class MyRecyclerViewAdapter
extends RecyclerView.Adapter<MyViewHolder>
implements View.OnClickListener{

// 当前Activity/Fragment
private Context context;
// 每个item包含的数据
private List<ItemData> itemList;

// 每个item的控件
private View itemView;
// 被附加到Adapter上的RecyclerView控件
private RecyclerView recyclerView;

public MyRecyclerViewAdapter(Context context, List<ItemData> itemList) {
this.context = context;
this.itemList = itemList;
}
public void setItem(int position, ItemData item){
itemList.set(position, item);
this.notifyItemChanged(position);
}

// item框创立时, 调用该方法
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// 根据layout文件, 创建View视图
itemView = LayoutInflater.from(context).inflate(R.layout.recyclerview_item, parent, false);

// 给View视图设置Listener
itemView.setOnClickListener(this);

// 从itemView中获取MyViewHolder并返回
MyViewHolder myViewHolder = new MyViewHolder(itemView);
return myViewHolder;
}

// 将RecycleView附加到Adapter上时, 调用该方法
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
this.recyclerView = recyclerView;
}

// 将RecycleView从Adapter解除时, 调用该方法
@Override
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
super.onDetachedFromRecyclerView(recyclerView);
this.recyclerView = null;
}

// item显示时, 调用该方法
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
// 根据item位置的数据, 设置当前item的每个组件的值
ItemData data = itemList.get(position);
holder.textView.setText(data.getTitle());
}

// item的数量
@Override
public int getItemCount() {
return itemList.size();
}


// 在 RecyclerView 的 Adapter 中定义单击事件的回调接口
public interface OnItemClickListener{
//参数: 父组件, 当前单击的View, 单击的View的位置, 数据
void onItemClick(RecyclerView parent, View view, int position, ItemData data);
}
private OnItemClickListener onItemClickListener;

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}

// RecyclerView被点击时, 调用该方法
@Override
public void onClick(View view) {
//根据RecyclerView获得当前View的位置
int position = recyclerView.getChildAdapterPosition(view);

//程序执行到此,会去执行具体实现的onItemClick()方法
if(onItemClickListener != null){
onItemClickListener.onItemClick(recyclerView, view, position, itemList.get(position));
}
}
}

3.4.4.4 在Activity中对RecyclerView控件进行相关的设置
  1. onCreate()中调用自定义方法setRecyclerView()
  2. setRecyclerView()中进行RecyclerView控件的初始化操作
    1. 设置所有item的默认数据
    2. 设置RecyclerView控件的Adapter
    3. 设置RecyclerView控件的LayoutManager
    4. 设置RecyclerView控件的Adapter的点击事件响应方法OnItemClickListener()
public class MainActivity extends AppCompatActivity {
private static final int CONTENT_ACTIVITY_RequestCode = 1;

private RecyclerView recyclerView;
private MyRecyclerViewAdapter adapter;
private LinearLayoutManager layoutManager;
private List<ItemData> itemList = new ArrayList<>();

@Override
protected void onCreate(Bundle savedInstanceState) {
// 初始化界面的相关操作
super.onCreate(savedInstanceState);
setContentView(R.layout.lab5_main_layout);

// 初始化RecyclerView
networkResult = findViewById(R.id.recyclerView);
setRecyclerView();
}

// 初始化RecyclerView
private void setRecyclerView(){
// 设置recyclerView中所有item的数据
for(int i = 0; i < 20; i++){
ItemData data = new ItemData("第 " + i +" 篇文章标题为:", "");
itemList.add(data);
}

// 设置Adapter
adapter = new MyRecyclerViewAdapter(this, itemList);
recyclerView.setAdapter(adapter);

// 设置LayoutManager
layoutManager = new LinearLayoutManager(this);
layoutManager.setOrientation(RecyclerView.VERTICAL);
recyclerView.setLayoutManager(layoutManager);

// 设置click事件响应
adapter.setOnItemClickListener(new MyRecyclerViewAdapter.OnItemClickListener() {
@Override
public void onItemClick(RecyclerView parent, View view, int position, ItemData data) {
// 使用WebView Activity打开网页
String urlString = data.getLink();
Intent intent = new Intent(MainActivity.this, WebViewActivity.class);
intent.putExtra("url", urlString);
startActivity(intent);

// 修改Adapter中的itemID处的文本
adapter = (MyRecyclerViewAdapter) recyclerView.getAdapter();
ItemData item = new ItemData("item " + itemID + " 已完成");
adapter.setItem(itemID, item);
}
});
}
}

3.4.5 回收复用机制

image-20220830212638148

Day3 UI进阶

一、Fragment

1.1 Fragment的基本用法和生命周期

1.1.1 Fragment的优点

  1. 将Activity模块化,将功能分散到小的Fragment中
  2. 一个Activity可以有多个Fragment,一个Fragment也可以有多个Fragment
  3. 可以重用、灵活
  4. 相比View,带有声明周期的概念

1.1.2 静态添加Fragment

  1. 定义fragment布局文件fragment_hello_layout.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"
    android:background="@color/black">

    <!--显示一行文字: Hello-->
    <TextView
    android:id="@+id/tv_hello"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="Hello"
    android:textSize="32sp"
    android:textColor="@color/white"/>
    </FrameLayout>
  2. 定义fragment类HelloFragment.java

    public class HelloFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_hello_layout, container, false);
    // inflate方法的主要作用: 将xml转换成一个View对象, 用于动态的创建布局
    // 参数说明
    // 1.int resource: 布局的资源id
    // 2.ViewGroup root: 填充的根视图
    // 3.boolean attachToRoot: 是否将载入的视图绑定到根视图中
    }
    }
  3. activity布局文件中嵌入 fragment

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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">

    <androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragment_hello"
    android:name="com.example.demo.HelloFragment"
    android:layout_width="300dp"
    android:layout_height="400dp"
    android:layout_gravity="center"/>
    </FrameLayout>

    MainActivity.java

    public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    }
    }

1.1.3 动态添加/删除Fragment

  1. Activity布局文件中定义Fragment容器activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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">

    <androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragment_hello"
    android:name="com.example.demo.HelloFragment"
    android:layout_width="300dp"
    android:layout_height="400dp"
    android:layout_gravity="center"/>

    <!--Fragment容器-->
    <FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    <!--后续实现跳转逻辑的一个Button-->
    <Button
    android:id="@+id/btn_replace"
    android:layout_width="120dp"
    android:layout_height="60dp"
    android:layout_marginBottom="40dp"
    android:layout_gravity="bottom|center_horizontal"
    android:text="Replace"/>
    </FrameLayout>
  2. 定义⼀个新的MainFragment

    fragment布局文件fragment_main_layout.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"
    android:background="#66CCFF">
    <!--设置背景色: android:background="#66CCFF"-->

    <!--显示一行文字: Replace success-->
    <TextView
    android:id="@+id/tv_replace"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="Replace success"
    android:textSize="32sp"/>

    </FrameLayout>

    fragment类MainFragment.java

    public class MainFragment extends Fragment {
    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_main_layout, container, false);
    }
    }
  3. 使用FragmentManager添加FragmentMainActivity.java

    public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private Button mReplaceButton;

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

    mReplaceButton = findViewById(R.id.btn_replace);
    mReplaceButton.setOnClickListener(v->{
    FragmentManager fragmentManager = getSupportFragmentManager();
    // 获取一个系统提供的FragmentManager

    fragmentManager.beginTransaction().commit()

    fragmentManager.beginTransaction()
    .remove(fragmentManager.findFragmentById(R.id.fragment_hello))
    .add(R.id.fragment_container, new MainFragment())
    .commit();
    // FragmentManager使用事务机制管理所有的Fragment
    // .beginTransaction(): 开始事务, 返回类型为FragmentTransaction
    // .remove(Fragment): 移除一个Fragment, 返回类型为FragmentTransaction
    // .add(id, Fragment): 添加一个Fragment, 返回类型为FragmentTransaction
    // .commit(): 将当前执行的操作提交, 返回类型为int

    mReplaceButton.setVisibility(View.GONE);
    // setVisibility(): 设置组件的是否可见
    // 有三个参数可供选择
    // VISIBLE:0 意思是可见的
    // INVISIBILITY:4 意思是不可见的,但还占着原来的空间
    // GONE:8 意思是不可见的,不占用原来的布局空间
    });
    }
    }

1.1.4 Fragment 生命周期

image-20220831095802196
  1. onAttach/onDetach:Fragment与Activity绑定/解除绑定
  2. onCreate/onDestroy:进行与View无关的初始化才做
  3. onCreateView/onDestroyView:渲染出视图布局,进行与View有关的初始化才做
  4. onActivityCreated:宿主Activity执行onCreate后调用该方法
  5. onStart/onStop:可见/不可见
  6. onResume/onPause:可交互/不可交互

image-20220831100204861

1.1.5 Fragment添加到返回栈

image-20220831100403525

  1. addToBackStack:将新的Fragment添加至返回栈,MainActivity.java

    public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private Button mReplaceButton;

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

    mReplaceButton = findViewById(R.id.btn_replace);
    mReplaceButton.setOnClickListener(v->{
    FragmentManager fragmentManager = getSupportFragmentManager();

    fragmentManager.beginTransaction()
    .add(R.id.fragment_container, new MainFragment())
    .addToBackStack(null)
    .commit();
    // .addToBackStack(String name): 将Fragment加入到回退栈
    // 是否使用 取决于 是否要在回退的时候显示上一个Fragment

    mReplaceButton.setVisibility(View.GONE);
    });
    }
    }

1.2 结合 ViewPager 创建多 Tab 界⾯

1.2.1 ViewPager的作用

  1. 常用于实现可滑动的多个视图
  2. 容器,类似于RecyclerView
  3. 需要通过 Adapter 配置内容
  4. 内容⼀般通过 Fragment 实现
  5. 可配置 TabLayout 或三⽅库添加 Title

1.2.2 ViewPager + Fragment

  1. 布局xml 中添加ViewPagerfragment_main_layout.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">
    <!--线性布局的对齐方式: android:orientation-->

    <!--添加一个ViewPager2-->
    <androidx.viewpager2.widget.ViewPager2
    android:id="@+id/view_pager_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    </LinearLayout>
  2. 定义Fragment

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

    <!--显示文本: values/string.xml中名为first_text的变量的值-->
    <TextView
    android:id="@+id/tv_content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="@string/first_text"
    android:textSize="28sp"/>

    </FrameLayout>

    fragment_object_animation.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"
    android:background="#34C724">

    <!--显示文本: values/string.xml中名为second_text的变量的值-->
    <TextView
    android:id="@+id/tv_content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="@string/second_text"
    android:textSize="28sp"/>
    </FrameLayout>

    fragment_lottie_animation.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"
    android:background="#ff0000">

    <!--显示文本: values/string.xml中名为third_text的变量的值-->
    <TextView
    android:id="@+id/tv_content"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:text="@string/third_text"
    android:textSize="28sp"/>

    </FrameLayout>
  3. 定义配置页面FragmentAdapterHelloFragmentViewPagerAdapter.java

    public class HelloFragmentViewPagerAdapter extends FragmentStateAdapter {
    private static final int FRAGMENTS_Count = 3;
    public static final int FRAGMENT_View_Animation = 0;
    public static final int FRAGMENT_Object_Animation = 1;
    public static final int FRAGMENT_Lottie_Animation = 2;

    public HelloFragmentViewPagerAdapter(@NonNull Fragment fragment) {
    super(fragment);
    }

    // 根据position的值, 判断创建哪一个Fragment
    @NonNull
    @Override
    public Fragment createFragment(int position) {
    switch (position){
    case FRAGMENT_View_Animation:
    return new ViewAnimationFragment();
    case FRAGMENT_Object_Animation:
    return new ObjectAnimationFragment();
    case FRAGMENT_Lottie_Animation:
    return new LottieAnimationFragment();
    default:
    return new Fragment();
    }
    }

    // 返回当前ViewPager有多少个Fragment
    @Override
    public int getItemCount() {
    return FRAGMENTS_Count;
    }
    }
  4. ViewPager设置AdapterMainFragment.java

    public class MainFragment extends Fragment {
    ViewPager2 mViewPager;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_main_layout, container, false);

    mViewPager = view.findViewById(R.id.view_pager_main);
    mViewPager.setAdapter(new HelloFragmentViewPagerAdapter(this));
    return view;
    }
    }

1.2.3 ViewPager + TabLayout

  1. 布局 xml 中继续添加 TabLayoutfragment_main_layout.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:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <!--线性布局的对齐方式: android:orientation-->

    <com.google.android.material.tabs.TabLayout
    android:id="@+id/tab_layout"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    app:tabIndicatorColor="@color/black"
    app:tabIndicatorHeight="2dp"
    app:tabIndicatorFullWidth="false"
    app:tabIndicatorGravity="bottom"
    app:tabGravity="center"
    app:layout_constraintTop_toTopOf="parent"/>

    <View
    android:id="@+id/divider"
    android:layout_width="match_parent"
    android:layout_height="2dp"
    android:background="#1A000000"/>


    <!--添加一个ViewPager2-->
    <androidx.viewpager2.widget.ViewPager2
    android:id="@+id/view_pager_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    </LinearLayout>
  2. 在代码中对 ViewPager 和 TabLayout 建立关联MainFragment.java

    public class MainFragment extends Fragment {
    private static final String TITLE_View_Animation = "视图动画";
    private static final String TITLE_Object_Animation = "属性动画";
    private static final String TITLE_Lottie_Animation = "Lottie动画";

    private final String[] tabTitles = new String[3];
    private ViewPager2 mViewPager;
    private TabLayout mTabLayout;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_main_layout, container, false);

    // 设置ViewPager的Adapter
    mViewPager = view.findViewById(R.id.view_pager_main);
    mViewPager.setAdapter(new HelloFragmentViewPagerAdapter(this));

    // 设置标题
    tabTitles[HelloFragmentViewPagerAdapter.FRAGMENT_View_Animation] = TITLE_View_Animation;
    tabTitles[HelloFragmentViewPagerAdapter.FRAGMENT_Object_Animation] = TITLE_Object_Animation;
    tabTitles[HelloFragmentViewPagerAdapter.FRAGMENT_Lottie_Animation] = TITLE_Lottie_Animation;

    // 设置TabLayout的监听器
    mTabLayout = view.findViewById(R.id.tab_layout);
    TabLayoutMediator tabLayoutMediator = new TabLayoutMediator(
    mTabLayout,
    mViewPager,
    true,
    false,
    (tab, position) -> tab.setText(tabTitles[position]));
    tabLayoutMediator.attach();
    return view;
    }
    }

1.3 Fragment/Activity 之间的通信

  1. 构造 Fragment 时传递参数(setArguments/getArguments)
  2. 通过接口和回调

1.3.1 Fragment与Activity之间的通信

1.3.1.1 传参
  1. activity_main.xml中添加一个文本框,用于测试

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout 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">

    <androidx.fragment.app.FragmentContainerView
    android:id="@+id/fragment_hello"
    android:name="com.example.demo.HelloFragment"
    android:layout_width="300dp"
    android:layout_height="400dp"
    android:layout_gravity="center"/>

    <!--Fragment容器-->
    <FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    <!--后续实现跳转逻辑的一个Button-->
    <Button
    android:id="@+id/btn_replace"
    android:layout_width="120dp"
    android:layout_height="60dp"
    android:layout_marginBottom="40dp"
    android:layout_gravity="bottom|center_horizontal"
    android:text="Replace"/>

    <!--用于测试传参的文本框-->
    <TextView
    android:id="@+id/tv_tabs_count"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="60dp"
    android:layout_gravity="center_horizontal"
    android:visibility="gone"
    android:textSize="32sp" />
    </FrameLayout>
  2. 通过传参为Fragment指定⼀个背景色:ViewAnimationFragment.java

    1. Fragment中提供实例化自身对象的静态方法
    2. onCreate中处理传递的参数
    public class ViewAnimationFragment extends Fragment {
    private static final String PARAM_Color = "param_color";
    private int mColor = Color.WHITE;

    public ViewAnimationFragment(){
    }

    public static ViewAnimationFragment newInstance(int color){
    ViewAnimationFragment fragment = new ViewAnimationFragment();
    Bundle args = new Bundle();
    args.putInt(PARAM_Color, color);
    fragment.setArguments(args);
    return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
    mColor = getArguments().getInt(PARAM_Color);
    }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_view_animation, container, false);
    view.setBackgroundColor(mColor);
    return view;
    }
    }
1.3.1.2 Listener
  1. 宿主Activity通过Listener回调获取当前已创建的tab数量MainFragment.java(省略部分见之前的MainFragment.java)

    public class MainFragment extends Fragment {
    // ...

    // Fragment 中定义⼀个接⼝以及接⼝类型的成员变量
    private MainFragmentListener mListener = null;
    public interface MainFragmentListener{
    void onMultiTabsViewCreated(int tabsCount);
    }

    // onAttach 中获取接⼝实例
    @Override
    public void onAttach(@NonNull Context context) {
    super.onAttach(context);
    mListener = (MainFragmentListener) context;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    // 创建view, 见之前的MainFragment...

    // 按需调⽤改接⼝⽅法进⾏通信
    if(mListener != null){
    mListener.onMultiTabsViewCreated(adapter.getItemCount());
    }
    return view;
    }
    }
  2. 宿主Activity中实现接口方法,执行相关处理:MainActivity.java

    public class MainActivity extends AppCompatActivity
    implements MainFragment.MainFragmentListener {

    @Override
    public void onMultiTabsViewCreated(int tabsCount) {
    TextView tv = findViewById(R.id.tv_tabs_count);
    tv.setText(tabsCount + " tabs created");
    tv.setVisibility(View.VISIBLE);
    }
    }

1.3.2 Master Detail(自学)

image-20220831104220441
image-20220831104235895
image-20220831104243620

1.4 总结

  1. Fragment: 灵活,可重⽤,迷你 Activity
  2. 生命周期、静态/动态添加⽤法
  3. ViewPager & Fragment
  4. 和 Activity 通信:Argument、Listener

二、Animation

2.1 视图动画

image-20220831104443434

2.1.1 示例

  1. Java方式设置

    private static final long ROTATE_Duration = 2000;
    private static final float ROTATE_Start_Degree = 0f;
    private static final float ROTATE_End_Degree = 360f;
    private static final float ROTATE_Pivot = 0.5f;

    private void initAnimation(){
    mRotateAnimation = new RotateAnimation(
    ROTATE_Start_Degree, ROTATE_End_Degree,
    Animation.RELATIVE_TO_SELF, ROTATE_Pivot, //x轴旋转中心
    Animation.RELATIVE_TO_SELF, ROTATE_Pivot //y轴旋转中心
    );

    // 设置动画的持续时间, 重复次数, 重复模式
    mRotateAnimation.setDuration(ROTATE_Duration);
    mRotateAnimation.setRepeatCount(Animation.INFINITE);
    mRotateAnimation.setRepeatMode(Animation.REVERSE);

    // 设置监听器
    mRotateAnimation.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    Log.d(TAG, "onAnimationStart");
    }

    @Override
    public void onAnimationEnd(Animation animation) {
    Log.d(TAG, "onAnimationEnd");
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
    Log.d(TAG, "onAnimationRepeat");
    }
    });
    }
  2. 配合XML方式设置

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

    <!--显示一张图片-->
    <ImageView
    android:id="@+id/iv_robot"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    android:src="@mipmap/ic_launcher_round" />

    </FrameLayout>

    ViewAnimationFragment.java

    public class ViewAnimationFragment extends Fragment {
    private static final String TAG = "ViewAnimationFragment";

    private static final String PARAM_Color = "param_color";
    private static final long ROTATE_Duration = 2000;
    private static final float ROTATE_Start_Degree = 0f;
    private static final float ROTATE_End_Degree = 360f;
    private static final float ROTATE_Pivot = 0.5f;

    private int mColor = Color.WHITE;
    private ImageView mRobot;

    // 动画部分: 旋转动画
    private RotateAnimation mRotateAnimation;

    public ViewAnimationFragment(){
    }

    public static ViewAnimationFragment newInstance(int color){
    ViewAnimationFragment fragment = new ViewAnimationFragment();
    Bundle args = new Bundle();
    args.putInt(PARAM_Color, color);
    fragment.setArguments(args);
    return fragment;
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Bundle args = getArguments();
    if(args != null){
    int givenColor = args.getInt(PARAM_Color);
    mColor = (givenColor != 0) ? givenColor : mColor;
    }
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_view_animation, container, false);
    view.setBackgroundColor(mColor);
    mRobot = view.findViewById(R.id.iv_robot);
    return view;
    }

    @Override
    public void onResume() {
    super.onResume();
    // 启动动画
    initAnimation();
    if(mRobot != null){
    mRobot.startAnimation(mRotateAnimation);
    }
    }

    @Override
    public void onPause() {
    super.onPause();
    // 动画不为空, 且动画已经启动, 则停止动画
    if(mRotateAnimation != null && mRotateAnimation.hasStarted()){
    mRotateAnimation.cancel();
    }
    }

    private void initAnimation(){
    mRotateAnimation = new RotateAnimation(
    ROTATE_Start_Degree, ROTATE_End_Degree,
    Animation.RELATIVE_TO_SELF, ROTATE_Pivot, //x轴旋转中心
    Animation.RELATIVE_TO_SELF, ROTATE_Pivot //y轴旋转中心
    );

    // 设置动画的持续时间, 重复次数, 重复模式
    mRotateAnimation.setDuration(ROTATE_Duration);
    mRotateAnimation.setRepeatCount(Animation.INFINITE);
    mRotateAnimation.setRepeatMode(Animation.REVERSE);

    // 设置监听器
    mRotateAnimation.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    Log.d(TAG, "onAnimationStart");
    }

    @Override
    public void onAnimationEnd(Animation animation) {
    Log.d(TAG, "onAnimationEnd");
    }

    @Override
    public void onAnimationRepeat(Animation animation) {
    Log.d(TAG, "onAnimationRepeat");
    }
    });
    }
    }

2.1.2 视图动画的属性

image-20220831105228934

2.2 属性动画

2.2.1 属性动画 vs 视图动画

  1. 属性动画:android.animation
    1. 基于属性的动画
    2. ⼀切可以连续变化的属性都是动画的元素
    3. 实现⼀种复杂动画,就是将动画拆解成不同属性组合的过程
  2. 视图动画:android.view.animation
    1. 只能对 View 做动画
    2. 只能对 View 的某些绘制属性做动画
    3. 只是视觉效果

2.2.2 属性动画的角色构成

  1. Property:alpha, scaleX, scaleY, rotation, translationX, translationY
  2. 参数:StartValue, EndValue, Duration
  3. RepeatCount:number, infinite
  4. RepeatMode:restart, reverse
  5. TypeEvaluator:IntEvaluator, ArgbEvaluator
  6. Interpolator:linear/accelerate/decelerate

2.2.3 Animator:单个属性动画

2.2.3.1 fragment_object_animation.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"
android:background="#34C724">

<!--显示一张图片-->
<ImageView
android:id="@+id/iv_robot"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:src="@mipmap/ic_launcher_round"/>
</FrameLayout>
2.2.3.2 ObjectAnimationFragment.java
public class ObjectAnimationFragment extends Fragment {
private static final String PARAM_Color = "param_color";
private int mColor = Color.WHITE;

private ImageView mRobot;

// 属性动画类
private ObjectAnimator mAnimator;
// 当前播放的时哪一个动画
private int mAnimationType = ALPHA_Animation_Type;
// 每个动画的重复次数, 持续时间
private static final int ANIMATION_Repeat_Count = 1;
private static final long ANIMATION_Duration = 2000;
// 透明度动画
private static final int ALPHA_Animation_Type = 0;
private static final float ALPHA_Start = 1f;
private static final float ALPHA_End = 0f;

public ObjectAnimationFragment(){
}


// ObjectAnimationFragment加载视图时调用
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_view_animation, container, false);
mRobot = view.findViewById(R.id.iv_robot);
return view;
}

// ObjectAnimationFragment可交互时调用
@Override
public void onResume() {
super.onResume();
startAnimation();
}
// 创建动画类,并开始动画
private void startAnimation(){
// 如果图片不存在, 则直接返回
if(mRobot == null) return;
initAlphaAnimation();
mAnimator.start();
}
// 初始化动画类
private void initAlphaAnimation(){
// 设置属性动画的相关参数
mAnimator = ObjectAnimator.ofFloat(
mRobot, "alpha",
ALPHA_Start, ALPHA_End, ALPHA_Start);

mAnimator.setDuration(ANIMATION_Duration);
mAnimator.setRepeatCount(ANIMATION_Repeat_Count);
mAnimator.setRepeatMode(ValueAnimator.RESTART);
}

// ViewAnimationFragment不可交互时调用
@Override
public void onPause() {
super.onPause();
cancelAnimation();
}
// 将所有动画类删除
private void cancelAnimation(){
// 动画不为空, 且动画已经启动, 则停止动画
if(mAnimator != null && mAnimator.isRunning()){
mAnimator.cancel();
}
}
}

2.2.4 AnimatorSet:多个属性动画之间的切换

public class ObjectAnimationFragment extends Fragment {
private static final String PARAM_Color = "param_color";
private int mColor = Color.WHITE;

private ImageView mRobot;

// 属性动画类
private AnimatorSet mAnimatorSet;
// 当前播放的时哪一个动画
private int mAnimationType = ROTATE_Animation_Type;
// 每个动画的重复次数, 持续时间
private static final int ANIMATION_Repeat_Count = 1;
private static final long ANIMATION_Duration = 2000;
// 透明度动画
private ObjectAnimator mAlphaAnimator;
private static final int ALPHA_Animation_Type = 0;
private static final float ALPHA_Start = 1f;
private static final float ALPHA_End = 0f;
// 旋转动画
private ObjectAnimator mRotateAnimator;
private static final int ROTATE_Animation_Type = 1;
private static final float ROTATE_Start_Degree = 0f;
private static final float ROTATE_End_Degree = 360f;
// 移动动画
private ObjectAnimator mTranslateXAnimator;
private static final int TRANSLATE_Animation_Type = 2;
private static final float TRANSLATE_XDelta_Start = 0f;
private static final float TRANSLATE_XDelta_End = 100f;
// 缩放视图动画
private ObjectAnimator mScaleXAnimator;
private static final int SCALE_Animation_Type = 3;
private static final float SCALE_X_Start = 1f;
private static final float SCALE_X_End = 1.5f;

public ObjectAnimationFragment(){ }

// ObjectAnimationFragment加载视图时调用
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_view_animation, container, false);
mRobot = view.findViewById(R.id.iv_robot);
return view;
}

// ObjectAnimationFragment可交互时调用
@Override
public void onResume() {
super.onResume();
startAnimation();
}
// 创建AnimatorSet,并开始动画
private void startAnimation(){
// 如果图片不存在, 则直接返回
if(mRobot == null) return;
// 创建动画类
initAlphaAnimation();
initRotateAnimation();
initTranslateXAnimation();
initScaleXAnimation();
// 创建AnimatorSet
mAnimatorSet = new AnimatorSet();
mAnimatorSet.playSequentially(mAlphaAnimator, mRotateAnimator, mTranslateXAnimator, mScaleXAnimator);
mAnimatorSet.start();
mAnimatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) { }

@Override
public void onAnimationEnd(Animator animator) {
// 动画序列结束后, 重新开始播放动画序列
mAnimatorSet.start();
}

@Override
public void onAnimationCancel(Animator animator) { }

@Override
public void onAnimationRepeat(Animator animator) { }
});
}
// 初始化动画类
private void initAlphaAnimation(){
// 设置属性动画的相关参数
mAlphaAnimator = ObjectAnimator.ofFloat(
mRobot, "alpha",
ALPHA_Start, ALPHA_End, ALPHA_Start);

mAlphaAnimator.setDuration(ANIMATION_Duration);
mAlphaAnimator.setRepeatCount(ANIMATION_Repeat_Count);
mAlphaAnimator.setRepeatMode(ValueAnimator.RESTART);
mAlphaAnimator.setInterpolator(new LinearInterpolator());
}
private void initRotateAnimation(){
mRotateAnimator = ObjectAnimator.ofFloat(
mRobot, "rotation",
ROTATE_Start_Degree, ROTATE_End_Degree, ROTATE_Start_Degree);

mRotateAnimator.setDuration(ANIMATION_Duration);
mRotateAnimator.setRepeatCount(ANIMATION_Repeat_Count);
mRotateAnimator.setRepeatMode(ValueAnimator.RESTART);
mRotateAnimator.setInterpolator(new LinearInterpolator());
}
private void initTranslateXAnimation(){
mTranslateXAnimator = ObjectAnimator.ofFloat(
mRobot, "translationX",
TRANSLATE_XDelta_Start, TRANSLATE_XDelta_End, TRANSLATE_XDelta_Start);

mTranslateXAnimator.setDuration(ANIMATION_Duration);
mTranslateXAnimator.setRepeatCount(ANIMATION_Repeat_Count);
mTranslateXAnimator.setRepeatMode(ValueAnimator.RESTART);
mTranslateXAnimator.setInterpolator(new LinearInterpolator());
}
private void initScaleXAnimation(){
mScaleXAnimator = ObjectAnimator.ofFloat(
mRobot, "scaleX",
SCALE_X_Start, SCALE_X_End, SCALE_X_Start);

mScaleXAnimator.setDuration(ANIMATION_Duration);
mScaleXAnimator.setRepeatCount(ANIMATION_Repeat_Count);
mScaleXAnimator.setRepeatMode(ValueAnimator.RESTART);
mScaleXAnimator.setInterpolator(new LinearInterpolator());
}

// ViewAnimationFragment不可交互时调用
@Override
public void onPause() {
super.onPause();
cancelAnimation();
}
// 将所有动画类删除
private void cancelAnimation(){
// 动画不为空, 且动画已经启动, 则停止动画
mAnimatorSet.cancel();
if(mAlphaAnimator != null && mAlphaAnimator.isRunning()){
mAlphaAnimator.cancel();
}
if(mRotateAnimator != null && mRotateAnimator.isRunning()){
mRotateAnimator.cancel();
}
if(mTranslateXAnimator != null && mTranslateXAnimator.isRunning()){
mTranslateXAnimator.cancel();
}
if(mScaleXAnimator != null && mScaleXAnimator.isRunning()){
mScaleXAnimator.cancel();
}
}
}
image-20220831105922891

2.2.5 特点,xml语法

image-20220831110102106

2.2.6 属性动画核心 - ValueAnimator

  1. 控制某个数值,在某个时间内,在某个区间内进行规律变化
image-20220831110150134

2.2.7 属性动画原理

  1. 插值器:决定变化的规律

  2. 估值器:决定变化的具体数值

    image-20220831110445532
  3. 常用系统内置插值器

    image-20220831110540996

2.2.8 属性动画 - 注意

  1. 使用ObjectAnimator 时,⽬标属性必须同时具备getter()及setter() ⽅法
  2. ObjectAnimator 操作对象宿主⻚⾯退出前台或销毁时,需保证动画任务得到妥善处理,防⽌内存泄漏

2.2.9 属性动画 - 自定义属性

image-20220831110723890

2.3 Activity 切换动画

image-20220831110751260

2.3.1 示例 FadeInOut

image-20220831110823317

2.4 逐帧动画/Drawable 动画

  1. 逐帧动画可以被当作⼀种特殊的drawable对象
  2. 逐帧动画会按次序播放⼀系列图⽚
  3. 逐帧动画会⼀次性将所有图⽚加载到内存中,会有OOM⻛险

2.4.1 示例 AnimationDrawable

image-20220831110933304

2.5 Lottie

  1. airbnb公司的开源库
  2. 可以直接导入AE制作的动画素材
  3. 本质是将所有动画元素抽象成绘制属性

2.5.1 示例

  1. app/build.gradle中,添加依赖

    dependencies {
    //...
    implementation 'com.airbnb.android:lottie:3.4.2'
    }
  2. 添加资源raw/lottie_raw_rocket.json

    image-20220831173539488

  3. 修改fragment_lottie_animation.xml

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout
    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"
    android:background="#ff0000">

    <com.airbnb.lottie.LottieAnimationView
    android:id="@+id/lottieView"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:layout_gravity="center"
    app:lottie_rawRes="@raw/lottie_raw_rocket"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"/>

    </FrameLayout>
  4. 修改LottieAnimationFragment.java

    public class LottieAnimationFragment extends Fragment {
    private static final String PARAM_Color = "param_color";

    public LottieAnimationFragment(){
    }


    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    return inflater.inflate(R.layout.fragment_lottie_animation, container, false);
    }
    }

Day4 复杂应用组件

一、进程与线程

1.1 进程、线程

  1. 进程:资源分配的最小单位 ==> 一个软件
    1. 如一辆列车
  2. 线程:CPU调度的最小单位 ==> 一个软件的各个功能
    1. 如一辆列车的列车长
  3. 主要区别:
    1. 一个进程可以有多个线程
    2. 同一个进程的多个线程,共享进程的资源

1.2 Android中的主线程

  1. 启动应用时,系统会为该应用创建⼀个称为“main”(主线程)的执行线程。这个线程负责所有和UI界⾯有关的显示、以及响应UI事件监听任务,因此又称座UI线程。
  2. 划重点:所有跟ui相关的操作都必须放在主线程

1.3 ANR 拓展

ANR:Application Not Responding

  1. 程序中所有的组件都会运行在UI线程中,所以必须保证该线程的⼯作效率
  2. UI线程⼀旦出现问题,就会降低⽤户体验
  3. 如在UI线程中进行耗时操作,如下载文件、查询数据库等就会阻塞UI线程,长时间⽆法响应UI交互操作,给用户带来“卡屏”、“死机”的感觉

image-20220901095158122

二、Handler机制

2.1 Handler机制

Handler机制为Android系统解决了以下两个问题:

  1. 任务调度
  2. 线程通信

2.2 Handler的使用场景

image-20220901095534573

2.3 Handler机制简介

本质是消息机制,负责消息的分发以及处理

  1. 通俗点来说,每个线程都有⼀个“流⽔线”,我们可往这条流⽔线上放“消息”,流⽔线的末端有⼯作⼈员会去处理这些消息。因为流⽔线是单线的,所有消息都必须按照先来后到的形式依次处理
  2. 放什么消息以及怎么处理消息,是需要我们去⾃定义的。Handler机制相当于提供了这样的⼀套模式,我们只需要“放消息到流⽔线上”,“编写这些消息的处理逻辑”就可以了,流⽔线会源源不断把消息运送到末端处理。
  3. 最后注意重点:每个线程只有⼀个“流⽔线”,他的基本范围是线程,负责线程内的通信以及线程间的通信。
  4. 每个线程可以看成⼀个厂房,每个厂房只有⼀个生产线。

2.4 Handler原理:UI线程与消息队列机制

image-20220901095712103

image-20220901095732246

  1. Message:消息,由MessageQueue统⼀队列,然后交由Handler处理
  2. MessageQueue:消息队列,⽤来存放Handler发送过来Message,并且按照先⼊先出的规则执⾏
  3. Handler:处理者,负责发送和处理Message每个Message必须有⼀个对应的Handler
  4. Looper:消息轮询器,不断的从MessageQueue中抽取Message并执⾏

2.5 Handler常用方法

// ⽴即发送消息
public final boolean sendMessage(Message msg)
public final boolean post(Runnable r);

// 延时发送消息: 马上发送消息, 但是会延迟处理
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean postDelayed(Runnable r, long delayMillis);

// 定时发送消息
public boolean sendMessageAtTime(Message msg, long uptimeMillis);
public final boolean postAtTime(Runnable r, long uptimeMillis);
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis);

// 移除消息
public final void removeCallbacks(Runnable r);
public final void removeMessages(int what); // what字段:给消息的命名
public final void removeCallbacksAndMessages(Object token); // token=null: 表示移除所有消息

2.6 Handler的使用

  1. 调度Message
    1. 建⼀个Handler,实现handleMessage()⽅法
    2. 在适当的时候给上面的Handler发送消息
  2. 调度Runnable
    1. 建⼀个Handler,然后直接调度Runnable即可
  3. 取消调度
    1. 通过Handler取消已经发送过的Message/Runnable

2.7 Handler的使用举例

2.7.1 发送Runnable对象

  1. 新建一个Handler对象:new Handler()
  2. 新建一个Runnable对象 1. 重写run()方法,表示该Runnable对象要做什么事情
  3. 调用Handler对象的post()方法,将Runnable对象发送出去

启动今日头条app的时候,展示了一个开屏广告,默认播放3秒;在3秒后,需跳转到主界面

image-20220901101255260

如果用户点击了跳过,则应该直接进入主界面

image-20220901101341933

2.7.2 发送Message对象

  1. 新建一个Handler对象:new Handler(Looper.getMainLopper()) 1. 重写handlerMessage()方法,表示收到不同Message时做出的反应
  2. 调用Handler对象的post()方法,将Message对象发送出去 1. 可以直接发送msg.what,来代表一个msg

用户在抖音App中,点击下载视频,下载过程中需要弹出Loading窗,下载结束后提示用户下载成功/失败

image-20220901101208647

image-20220901101419103

2.7.3 辨析Runnable与Message

  1. Runnable会被打包成Message,所以实际上Runnable也是Message
  2. 没有明确的界限,取决于使⽤的⽅便程度

以下两段代码等价

image-20220901101512752

image-20220901101518680

2.8 Handler总结

  1. Handler就是Android中的消息队列机制的⼀个应用,可理解为是⼀种⽣产者消费者的模型,解决了Android中的线程内&线程间的任务调度问题
  2. Handler机制的本质就是⼀个死循环,待处理的Message加到队列⾥⾯,Looper负责轮询执⾏
  3. 掌握Handler的基本⽤法:立即/延时/定时发送消息、取消消息

三、Android中的多线程

3.1 Thread

  1. 新建一个类,继承Thread,重写其run()方法
  2. 调用时,先新建一个实例 1. 可以传入一个String参数,表示线程的名字
  3. 调用tread.start()方法,开启线程

image-20220901103023437

3.2 ThreadPool

3.2.1 为什么要使用线程池

  1. 线程的创建和销毁的开销都比较⼤,降低资源消耗
  2. 线程是可复用的,提⾼响应速度
  3. 对多任务多线程进行管理,提高线程的可管理性

3.2.2 几种常用的线程池

  1. 单个任务处理时间比较短且任务数量很大(多个线程的线程池):
    1. FixedThreadPool 定长线程池
    2. CachedThreadPool 可缓存线程池
  2. 执行定时任务(定时线程池):
    1. ScheduledThreadPool 定时任务线程池
  3. 特定单项任务(单线程线程池):
    1. SingleThreadPool 只有⼀个线程的线程池

3.2.3 使用示例

  1. 接口Java.util.concurrent.ExecutorService表述了异步执行的机制,并且可以让任务在⼀组线程内执行
  2. 重要函数:
    1. execute(Runnable):向线程池提交⼀个任务
    2. submit(Runnbale/Callable):有返回值(Future),可以查询任务的执行状态和执行结果
    3. shutdown() :关闭线程池
  1. 创建一个线程池ExecutorService的示例
  2. 创建一个Runnable对象,并编写其业务逻辑
  3. 通过service.execute()方法,向线程池提交任务

image-20220901103647564

image-20220901103714432

3.3 AsyncTask(已弃用)

image-20220901103805640

image-20220901103822913

3.4 HandlerThread

  1. HandlerThread的本质:继承Thread类 & 封装Handler
    1. 试想⼀款股票交易App:
      1. 由于因为股票的行情数据都是实时变化的
      2. 所以我们软件需要每隔⼀定时间向服务器请求行情数据
    2. 该轮询调度需要放到子线程,由Handler + Looper去处理和调度
  2. HandlerThread是Android API提供的⼀个⽅便、便捷的类,使用它我们可以快速的创建⼀个带有Looper的线程Looper可以用来创建Handler实例
  1. 创建一个HandlerThread对象
  2. 使用handlerThread.start()方法,运行线程
  3. 通过handlerThread.getLooper()方法,获取该线程的Looper
  4. 通过Looper实例创建Handler,将Handler与该线程关联

image-20220901104305510

HandlerThread的源码

  1. onLooperPrepared()
  2. run():运行该线程
  3. getThreadHandler()
  4. quit()quitSafely():停止该线程

image-20220901104612875

3.5 IntentService(不常用, 自学)

image-20220901104842002

image-20220901104852833

3.6 Android多线程总结

image-20220901104924134

四、自定义View

4.1 View绘制的三个重要步骤

  1. Measure:测量宽高
  2. Layout:确定位置
  3. Draw:绘制形状
  4. 举例说明:
    1. 首先画⼀个100 x 100的照片框,需要尺子测量出宽高的长度(measure过程)
    2. 然后确定照片框在屏幕中的位置(layout过程)
    3. 最后借助尺子用手画出我们的照片框(draw过程)

4.2 绘制流程

image-20220901105430782

image-20220901105457172

image-20220901105516634

image-20220901105526514

4.3 自定义View:重写onDraw

  1. Canvas:画布
  2. Paint:画笔
  3. 坐标轴image-20220901105711985

image-20220901105602114

image-20220901105630075

4.3.1 画点

image-20220901105822943

4.3.2 画线

image-20220901105938514

4.3.3 画圆

image-20220901105928257

4.3.4 填充

image-20220901105917656

4.3.5 不规则图形

image-20220901105954866

4.3.6 画文本

image-20220901110009241

image-20220901110022511

4.4 自定义View总结

  1. 重要绘制流程:
    1. Measure:测量
    2. Layout:布局
    3. Draw:绘制
  2. 以及几个重要函数:
    1. invalidate
    2. requestLayout
  3. 理解ViewTreeViewGroupMeasure / Layout / Draw的流程
  4. View自定义绘制:
    1. 绘制图形:点、线、圆形、椭圆、矩形、圆⻆矩形
    2. 绘制文字

Day5 Android中的网络

一、网络基础知识

1.1 网络分层模型

image-20220902093714465

  1. 应用层Application Layer:互联网上的各种应用
    1. 如:看网页、发邮件
    2. 如:HTTP/FTP/SMTP协议
  2. 传输层Transport Layer
  3. 网络层Internet Layer

1.2 HTTP协议

  1. HTTP是一个client-server协议,只能由client主动发起请求,server进行响应

    1. 用户发出请求,服务端进行相应
  2. 一个HTTP请求一定要包含MethodURL

    1. Method:要做什么,如GET、PUT、HEAD、POST、DELETE、TRACE、OPTIONS、CONNETC
    2. URL
  3. Request格式:

    image-20220902094648685

  4. Response格式:

    image-20220902094717700

  5. HTTP状态码:

    image-20220902094756612

    1. 4XX:服务器正常,但客户段出错
    2. 404:服务器正常,但服务不存在
    3. 5XX:服务器不正常,无法处理请求(请求过多)

1.3 RESTful API

1.3.1 API

API:Application Programming Interface 应用程序接口

  1. 用户端使用固定的方式发起请求
  2. 服务端提供服务,响应该请求
  3. 使用一个固定的方式,保证不同模块之间进行通信,提高兼容性
  4. 略去不同模块之间的区别,找到共同的部分

image-20220902095511020

1.3.2 RESTful API

RESTful API:Resource Epresentational State Transfer (资源)表现层状态转换

  1. API 是面向资源的,资源表达的形式可以是json或者xml,它的url中不包含动词,而是通过HTTP动词表达想要的动作
  2. 资源:一段文本、一张图片、一首歌曲
  3. 表现形式:json、xml
  4. 状态变化:通过HTTP动词实现
  5. 目的:看 URL 就知道要什么,看HTTP method 就知道干什么

RESTful 只是一种规范,并不是标准

image-20220902100654244

1.4 数据传输格式

后端与前端/客户端需要约定数据传输的格式,以json为例

  1. 每一组数据都是键值对
  2. {}括起的数据:单个值
  3. []括起的数据:数组值
{
"name" : "中国",
"provinces" : [{
"name" : "黑龙江",
"cities" : {
"city" : ["哈尔滨", "大庆"]
}
},{
"name" : "广东",
"cities" : {
"city" : ["广州", "深圳", "珠海"]
}
}]
}

1.4.1 JSON解析器:gson

image-20220902104328367

常用API

// 将对象转化成Json
public <T> toJson(Object src);
// 从Json中提取对象
public <T> fromJson(String json, Class<T> classOfT);
public <T> fromJson(Reader json, Type typeOfT);

image-20220902104903235

1.4.2 下划线命名 vs 驼峰命名

image-20220902105446240

1.5 实用工具

1.5.1 JSON相关

  1. JSON 在线辅助网站:转 JavaBean;合法性校验;压缩;优化预览
  2. GsonFormatPlus:IDEA 插件,JSON 转 JavaBean
  3. JSON:维基百科,了解 JSON 的来⻰去脉

1.5.2 抓包工具 Charles

image-20220902105219265

image-20220902105234415

image-20220902105347441

1.5.3 抓包工具 Postman–轻松创建请求

image-20220902105519456

二、Android网络通信基础实现

2.1 添加网络权限

AndroidManifest.xml中,添加网络权限uses-permission

<?xml version="1.0" encoding="utf-8"?>
<manifest>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
......
</manifest>

2.2 获取网络中的数据

  1. 新建一个线程HandlerThread,用于执行与网络有关的任务
  2. 新建一个任务Handler,用于从url中获取数据
    1. run()方法中
    2. 调用自定义的getDataFromNetwork()方法,获取数据
    3. 然后根据得到的数据,调用自定义的showDataFromNetwork()方法,刷新界面
@Override
protected void onCreate(Bundle savedInstanceState) {
// 初始化界面的相关操作
super.onCreate(savedInstanceState);
setContentView(R.layout.lab5_main_layout);
networkResult = findViewById(R.id.Lab5_NetworkResult);

// 执行从网络中获取数据的操作
runGetDataFromNetwork("https://www.wanandroid.com/article/list/0/json/");
}

// 新建线程, 执行从网络中获取数据的操作
private void runGetDataFromNetwork(String urlString){
// 新建一个线程, 用于执行与网络有关的任务
HandlerThread handlerThread = new HandlerThread("Networking");
handlerThread.start();

// 新建一个Handler, 用于从url中获取数据
Handler networkingHandler = new Handler(handlerThread.getLooper());
Runnable networkingRunnable = new Runnable() {
@Override
public void run() {
Log.d(TAG, "run networking Runnable: 从url中获取数据");
// 调用getDataFromNetwork()方法, 获取数据
ArrayList<Lab5_NetworkData> result = getDataFromNetwork(urlString);

// 根据得到的数据, 刷新界面
if(result != null && !result.isEmpty()){
Log.d(TAG, "run networking Runnable: 从url中获取数据为:" + "not null");
showDataFromNetwork(result);
}else{
Log.d(TAG, "run networking Runnable: 从url中获取数据为:" + "null");
}
}
};
networkingHandler.post(networkingRunnable);
}
  1. 获取数据:getDataFromNetwork(String urlString)方法
    1. 将传入的urlString转化为URL
    2. 使用HttpURLConnection建立连接
      1. 创建实例connection
    3. connection中获取数据
      1. 先获取为InputStream
      2. 然后转化为BufferedReader
      3. 最后,通过BufferedReader.readLine()方法,将其中的数据转化为JSONObject
    4. 解析JSON:从JSONObject中,读取出所有data.datas[].title/link
      1. 根据目标JSON文件的结构,反复调用getJSONObject()getJSONArray()方法
      2. 如果当前到达了最低层,则调用getString()方法,获取String类型的数据
      3. 将数据存放入一个ArrayList
    5. 将读取到的ArrayList返回
  2. 注意要使用try,防止操作出现异常
// 从网络中获取数据
// 返回一个List, 存放解析出的Json数据
private ArrayList<Lab5_NetworkData> getDataFromNetwork(String urlString){
Log.d(TAG, "getDataFromNetwork: 从网络中获取数据, 返回List");
ArrayList<Lab5_NetworkData> result = new ArrayList<Lab5_NetworkData>();
// 通过try, 防止获取数据出现问题
try{
// 将urlString转化为URL
URL url = new URL(urlString);

// 使用HttpURLConnection建立连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(6000);
connection.connect();

// 获取数据, 并将其转化为JSONObject
InputStream inputStream = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
JSONObject resultJson = new JSONObject(reader.readLine());

// 从JSONObject中, 读取出所有data.datas[].title/link
JSONObject data = resultJson.getJSONObject("data");
JSONArray datas = data.getJSONArray("datas");
for(int index = 0; index < datas.length(); index++){
JSONObject item = datas.getJSONObject(index);
String title = item.getString("title");
String link = item.getString("link");
result.add(new Lab5_NetworkData(title, link));
}

// 关闭BufferedReader, InputStream, 取消HttpURLConnection的连接
reader.close();
inputStream.close();
connection.disconnect();
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

2.3 使用WebView打开网页

  1. 新建一个Activity,其中有WebView控件
<?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=".Lab5_WebViewActivity">

<WebView
android:id="@+id/Lab5_WebView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>
  1. 切换至WebViewActivity时,将urlString作为参数,添加进intent
// 使用单个从网络中获取的数据
private void useDataFromNetwork(Lab5_NetworkData data){
// 使用WebView Activity打开网页
String urlString = data.getLink();
Intent intent = new Intent(this, Lab5_WebViewActivity.class);
intent.putExtra("url", urlString);
startActivity(intent);
}
  1. WebViewActivity初始化时,获取urlString参数,打开网页
    1. 由于现在的网页包含的内容过多,因此要进行一系列设置,才能正常读取页面的数据
public class Lab5_WebViewActivity extends AppCompatActivity {
private WebView webView;
private WebSettings webSettings;
private String urlString;

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

// 获取WebView控件, 传入的url字符串, WebSettings属性
webView = findViewById(R.id.Lab5_WebView);
urlString = getIntent().getStringExtra("url");
webSettings = webView.getSettings();

// 设置WebView的属性
webSettings.setJavaScriptEnabled(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setLoadWithOverviewMode(true);
webSettings.setAppCacheEnabled(true);
webSettings.setDomStorageEnabled(true);

// 使用WebView控件, 打开对应网页
webView.loadUrl(urlString);
}
}

三、进阶实现:Retrofit(自学)

image-20220902112806576

image-20220902112756408

image-20220902112746043

image-20220902112736535

image-20220902112726704

Day6 Android存储

一、存储空间概览

1.1 存储空间的区分

  1. Internal Storage:是系统分配给应用的专属内部存储空间
    1. APP专有的
    2. 用户不可以直接读取(root用户除外)
    3. 应用卸载时自动清空
    4. 有且仅有一个
  2. External Storage:是系统外部存储空间,如 SD卡
    1. 所有用户均可访问
    2. 不保证可用性(可挂载/物理移除)
    3. 可以卸载后仍保留
    4. 可以有多个

1.2 存储目录

  1. Internal Storage:/

    1. APP专用:
      1. data/data/{your.package.name}/ files、cache、db...
  2. External Storage

    1. APP专用:
      1. /storage/emulated/0/Android/data/{your.package.name}/ files、cache
    2. 公共文件夹:./
      1.  --- Standard: DCIM、Download、Movies
      2.  --- Others

    image-20220905093948124

1.3 Internal目录的获取

  1. file目录:context.getFilesDir()
  2. cache目录:context.getCacheDir()
  3. 自定义目录:context.getDir(name, mode_private)

1.4 External目录的获取

1.4.1 获取授权

AndroidManifest.xml中声明权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

请求授权(Android 6.0及以上)

private final static int CODE_REQUEST_PERMISSION = 1;
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALENDAR)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[] {Manifest.permission.READ_EXTERNAL_STORAGE},
CODE_REQUEST_PERMISSION);
}

ActivityonRequestPermissionsResult 方法中获取授权结果

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (requestCode == CODE_REQUEST_PERMISSION) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// ⽤户已授权
} else {
// ⽤户拒绝了授权
}
}
}

1.4.2 Environment APIs

提供了访问环境变量的方法

public class Environment extends Object{};
android.os.Environment;

1.4.3 检查外置存储器的可用性

通过Environment.getExternalStorageState();调⽤获取当前外部存储的状态,并以此判断外部存储是否可⽤

image-20220905100752309

1.4.4 External目录的获取

  1. 应用私有目录:
    1. file目录:context.getExternalFilesDir(String type)
    2. cache目录:context.getExternalCacheDir()
  2. 公共目录:
    1. 标准目录:Environment.getExternalStoragePublicDirectory(String type)
    2. 根目录:Environment.getExternalStorageDirectory()
  3. 标准目录:
    1. DIRECTORY_ALARMS
    2. DIRECTORY_DCIM
    3. DIRECTORY_DOCUMENTS
    4. DIRECTORY_DOWNLOADS
    5. DIRECTORY_MOVIES

1.5 注意事项

  1. 如果用户卸载应⽤,系统会移除保存在应⽤专属存储空间中的⽂件
  2. 由于这⼀行为,不应使用此存储空间保存⽤户希望独例于应用而保留的任何内容
    1. 例如,如果应用允许用户拍摄照片,用户会希望即使卸载应⽤后仍可访问这些照⽚
    2. 因此,应改为使用共享存储空间将此类文件保存到适当的媒体集合中。

更多信息可参考:https://developer.android.com/guide/topics/data?hl=zh-cn

二、键值对

image-20220905094425569

image-20220905094434425

三、SharedPreferences

3.1 介绍

  1. SharedPreference 就是 Android 提供的数据持久化的⼀个方式,适合单进程,小批量的数据存储和访问。基于 XML 进行实现,本质上还是⽂件的读写,API 相较 File 更简单。
  2. 以“键-值”对的方式保存数据的xml⽂件,其文件保存在/data/data/[packageName]/shared_prefs目录下

3.2 获取SharedPreferences

image-20220905094610997

3.3 读取SharedPreferences

通过getxxx()方法获取,需要传入(keydefaultValue)

private static final String SP_NAME = "Lab6_SharedPreference";
private static final String SAVE_KEY = "Lab6_Text";
// 从内存中读取EditText的文本
private String readTextFromStorage(){
SharedPreferences sp = Lab6_SharedPreference.this.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
String text = sp.getString(SAVE_KEY,"记录一些文字");
return text;
}
image-20220905094626044

3.4 写SharedPreferences

通过Editor类来提交修改

private static final String SP_NAME = "Lab6_SharedPreference";
private static final String SAVE_KEY = "Lab6_Text";
// 将当前EditText的文本保存到内存中
private void saveTextToStorage(String text){
SharedPreferences sp = Lab6_SharedPreference.this.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(SAVE_KEY, text);
editor.commit();
// 或者调用apply方法
// editor.apply();
}

3.5 SharedPreferences的原理

image-20220905094927561
image-20220905094941985

3.6 注意事项

  1. SharedPreference 适合场景:小数据
  2. SharedPreference 每次写入均为全量写入
  3. 禁止大数据存储在 SharedPreference 中,导致 ANR

官方推荐的DataStore:https://developer.android.com/topic/libraries/architecture/datastore

四、文件File

4.1 流

都是相对调用者而言的

  1. 按流向分为:
    1. 输⼊流
    2. 输出流
  2. 按传输单位分为:
    1. 字节流:InputStreamOutputStream 基类
    2. 字符流:ReaderWriter 基类

image-20220905095449287

4.2 API

image-20220905095310212

4.3 文件操作

image-20220905095407452

4.4 文件IO读写操作示例

  1. 创建 File 对象,通过构造函数:
    1. new File()
  2. 创建输⼊输出流对象:
    1. new FileReader()
    2. new FileWriter()
  3. 读取 or 写⼊
    1. read 方法
    2. write f昂发
  4. 关闭资源
    1. 有借有还,再借不难
public class Lab6_FileIO extends AppCompatActivity {
private static final String TAG = "Lab6_FileIO";
private static final String SAVE_FILE_NAME = "Lab6_FileIO";
private static final String SAVE_KEY = "Lab6_Text";
private EditText editText;
private Button saveButton;
private String fileName = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.lab6_text_memo);

fileName = getFilesDir().getAbsolutePath() + File.separator + SAVE_FILE_NAME;

editText = findViewById(R.id.Lab6_EditText);
readTextFromStorage();

saveButton = findViewById(R.id.Lab6_Save_Button);
saveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
saveTextToStorage(editText.getText().toString());
}
});
}

// 从内存中读取EditText的文本
private void readTextFromStorage(){
new Thread(new Runnable() {
@Override
public void run() {
File file = new File(fileName);
// 文件不存在, 则新建一个文件
if(!file.exists()){
try{
boolean isSuccess = file.createNewFile();
if(!isSuccess) throw new IOException("create file exception");
}
catch (IOException e) {
Log.e(TAG, "readTextFromStorage: 创建文件失败");
e.printStackTrace();
}
}
// 文件存在
try {
// 创建文件输入流
FileInputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[1024];
final StringBuffer buffer = new StringBuffer();
// 读取数据, 存入buffer中
while (inputStream.read(bytes) != -1){
buffer.append(new String(bytes));
}
// 在UI线程中修改editText的值
runOnUiThread(new Runnable() {
@Override
public void run() {
String text = buffer.toString();
text = text.trim();
if(text.equals("")) text = new String("记录一些文字");
editText.setText(text);
}
});
}
catch (IOException e) {
Log.e(TAG, "readTextFromStorage: 文件读取失败");
e.printStackTrace();
}
}
}).start();
}

// 将当前EditText的文本保存到内存中
private void saveTextToStorage(String text){
new Thread(new Runnable() {
@Override
public void run() {
File file = new File(fileName);
// 文件不存在, 则新建一个文件
if(!file.exists()){
try{
boolean isSuccess = file.createNewFile();
if(!isSuccess) throw new IOException("create file exception");
} catch (IOException e) {
Log.e(TAG, "saveTextToStorage: 创建文件失败");
e.printStackTrace();
}
}
// 文件存在
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
outputStream.write(text.getBytes());
}
catch (IOException e){
Log.e(TAG, "saveTextToStorage: 创建文件失败");
e.printStackTrace();
}
finally { // 将文件输出流关闭
try{
if(outputStream != null)
outputStream.close();
}
catch (IOException e){
Log.e(TAG, "saveTextToStorage: 文件输出流关闭失败");
e.printStackTrace();
}
}
}
}).start();
}
}

4.7 拓展:OkIO

  1. 是在JavaIO基础上再次进行封装的IO框架
  2. https://square.github.io/okio/

image-20220906142429416

五、数据库

5.1 使用场景

  1. 重复的数据
  2. 结构化的数据
  3. 关系型数据

5.2 数据库的设计

image-20220905103427035

5.3 SQL

image-20220905103449215
image-20220905103454598
image-20220905103500886
image-20220905103506146
image-20220905103512495

5.4 使用示例:Todo List App

image-20220906142203738

5.4.1 定义Contract类

定义表结构、SQL语句

image-20220905103926681

5.4.2 继承SQLiteOpenHelper

执行CreateDelete 操作

image-20220905103718927

5.4.3 获取SQLLiteDatabase

image-20220906142245189

5.4.4 Insert

通过ContentValues进行插⼊操作

image-20220905103913631

5.4.5 Query

调用query()方法,返回 Cursor ,对应查询结果集合

moveToNext返回 -1时,遍历结束

image-20220906142305525

5.4.6 Delete

删除数据库中对应 id 的数据

image-20220905104044993

5.4.7 Update

image-20220905104116815

5.4.8 Debug

adb + sqlite3:http://www.sqlite.org/cli.html

image-20220906142351296

image-20220906142357145

5.4.8 注意事项

image-20220905104435654
image-20220905104450504

5.5 Room Library

https://developer.android.com/jetpack/androidx/releases/room

image-20220905104524950

image-20220906142120880

image-20220906142128136

六、Content Provider

6.1 定义

  1. 当我们需要在应⽤间共享数据时,ContentProvider 就是⼀个非常值得使用的组件
  2. 四大组件之⼀,ContentProvider 是⼀种 Android 数据共享机制,无论其内部数据以什么样的方式组织,对外都是提供统⼀的接口
  3. 通过 ContentProvider可以获取系统的媒体、联系⼈、⽇程等数据

https://developer.android.com/reference/android/content/ContentProvider

6.2 Content Provider架构

image-20220905104930570

6.3 优点

  1. 跨应用分享数据
    1. 系统的 providers 有联系人等
  2. 是对数据层的良好抽象
  3. 支持精细的权限控制

七、URI

7.1 URI介绍

URI:Uniform Resource Indentifier,唯一标识ContentProvider的数据

image-20220905105150461
image-20220905105158051
image-20220905105248202

7.2 URI使用示例

7.2.1 查询:获取联系人数据

  1. AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_CONTACTS" />
  1. 在程序中动态请求权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_CONTACTS
}, 1);
}
  1. 通过ContentResolver 查询对应的ContentProvider
// 获取 ContentResolver 对象
ContentResolver contentResolver = this.getContentResolver();
Cursor cursor = contentResolver.query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
  1. 遍历cursor,获取对应字段的值

image-20220906141831356

7.2.2 查询:获取系统相册中的视频文件

  1. AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. 在程序中动态请求权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {
Manifest.permission.READ_EXTERNAL_STORAGE
}, 1);
}
  1. 通过ContentResolver查询
// 获取 ContentResolver 对象
ContentResolver contentResolver = this.getContentResolver();
Cursor cursor = contentResolver.query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, null, null, null,null);

image-20220906141600052

7.2.3 读取URI对应的图片到ImageView中

  1. AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. 在程序中动态请求权限
  2. 重写onRequestPermissionsResult()方法,定义授权后的操作
  3. 重写onActivityResult()方法,定义选择图片之后的操作
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_my_settings, container, false);

userIcon = view.findViewById(R.id.icon_my_setting_user_info);

// 进行一个判断,是否已经有访问权限,如果没有先获取
if(ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED){

ActivityCompat.requestPermissions(getActivity(),
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);

Toast.makeText(getActivity(),"请授予权限", Toast.LENGTH_SHORT);
}else{
// 已经有权限了, 直接执行操作
readImageFromStorage();
}

userIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// 点击后打开本地图库选择照片
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, 1);
}
});
return view;
}

@RequiresApi(api = Build.VERSION_CODES.O)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if(requestCode == 1){
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
readImageFromStorage();
}
}else{
Toast.makeText(getActivity(), "授权失败", Toast.LENGTH_SHORT);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 1 && resultCode == RESULT_OK){
if(data != null){
Uri uri = data.getData();
userIcon.setImageURI(uri);
saveImageToStorage(uri);
Log.e(TAG, "onActivityResult: 设置图片的路径为" + uri.toString());
}
}
}
  1. 自定义readImageFromStorage()方法通过URI读取图片
  2. 自定义saveImageToStorage()方法保存URI
public class Constants {
// 本地用户的图标
public static final String USER_ICON_SAVE_SP = "USER_ICON";
public static final String USER_ICON_SAVE_KEY = "user_icon";
}


@RequiresApi(api = Build.VERSION_CODES.O)
public void readImageFromStorage(){
SharedPreferences sp = getActivity().getSharedPreferences(Constants.USER_ICON_SAVE_SP, Context.MODE_PRIVATE);
String uriString = sp.getString(Constants.USER_ICON_SAVE_KEY, null);
if(uriString == null) return;

Uri uri = Uri.parse(uriString);
userIcon.setImageURI(uri);
}

public void saveImageToStorage(Uri uri){
String uriString = uri.toString();
SharedPreferences sp = getActivity().getSharedPreferences(Constants.USER_ICON_SAVE_SP, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString(Constants.USER_ICON_SAVE_KEY, uriString);
editor.commit();
}

Day7 Android多媒体基础

一、图片基础

1.1 图片基础:颜色空间

RGB色彩空间(R=Red, G=Green, B=Blue, A=Alpha)

  1. ALPHA_8:A = 8位,1字节
  2. RGB_565:R = 5位 G = 6位 B = 5位,2字节
  3. ARGB_4444:A = 4位 R = 4位 G = 4位 B = 4位,2字节
  4. ARGB_8888:A = 8位 R = 8位 G = 8位 B = 8位,4字节

1.2 图片存储:位图

  1. 位图以像素(方格点)构成图像,放大时会失真

    image-20220906195359445

1.3 图片格式:压缩标准方案

JPEG:Joint Photographic Experts Group 有损压缩方案

  1. 去除冗余的图像和彩色数据
  2. 压缩比相对较⾼,⽂件大小相对较小
  3. 不支持透明图和动态图

PNG:全称Portable Network Graphics

  1. 高压缩比的无损压缩,⽂件大小比jpeg⾼些
  2. 最多⽀持48位⾊彩
  3. 支持α通道数据(透明度)
  4. 是Android开发上常用的图片格式,例如图标

WebP是谷歌提供的⼀种⽀持有损压缩和无损压缩的图片文件格式

  1. 比JPEG或PNG更好的压缩
  2. 在Android 4.0(API level 14)中支持有损的WebP图像
  3. 在Android 4.3(API level 18)和更高版本中支持无损和透明的WebP图像

image-20220906195738520

1.4 图片存储:矢量图

  1. 矢量图使用线段和曲线描述图像,由数学向量构成图形图像
  2. 可以被无限放大而不影响质量

二、Android图片加载

2.1 Graphics包:安卓平台2D图形绘制的基础

image-20220906200013440

2.2 Android Bitmap:一种存储像素的数据结构

image-20220906200257970

2.3 图片加载

image-20220906200618941

2.4 加载超大图片

加载的图片过大,可能会出现OOM(Out of Memory)

image-20220906201121241

2.5 大图分块加载

image-20220906201932153

image-20220906201944970

2.6 图片自适应:NinePatchDrawable

image-20220906202051154

2.7 ImageView

  1. ImageView:显示图片的组件

    image-20220906202112375

    https://developer.android.com/reference/android/widget/ImageView.ScaleType

  2. ImageView.ScaleType:主要值设置所显示的图片如何缩放或移动以适应ImageView的大小

    image-20220906202606810

2.8 Drawable

  1. DrawableView 2D 绘画里是⼀个很重要的抽象类,抽象出了怎么画,画什么的⼀个概念

  2. 有很多子类

    image-20220906202902180

    image-20220906202830183

2.9 常见图片库

image-20220906203019084

2.10 Glide

2.10.1 Glide的基础使用

  1. Glide是⼀个快速高效的Android图片加载库,提供了易用的API,高性能、可扩展的图⽚解码管道

build.grade里面添加引用

implementation 'com.github.bumptech.glide:glide:4.9.0'

在代码中使用Glide

Glide.with(context)
.load(uri)
.into(imageView);

2.10.2 Glide流程

image-20220906203447657

2.10.3 Glide RequestBuilder

  1. RequestBuilder是Glide中请求的骨架,负责携带请求的url和你的设置项来开始⼀个新的加载过程。 使用RequestBuilder 可以指定:
    1. 你想加载的资源类型(Bitmap, Drawable, 或其他)
    2. 你要加载的资源地址(url/model)
    3. 你想最终加载到的View
    4. 任何你想应用的(⼀个或多个)RequestOption 对象
    5. 任何你想应用的(⼀个或多个)TransitionOption 对象
    6. 任何你想加载的缩略图 thumbnail()
RequestBuilder<Drawable> requestBuilder = Glide.with(fragment).asDrawable();

RequestBuilder<Drawable> requestBuilder = Glide.with(fragment).load(url);

2.10.4 Glide RequestOptions

Glide中的很多设置项都可以通过 RequestOptions 类和 apply() 方法来应用到程序中

使用Request Options可以实现(包括但不限于):

  1. 转换(Transformations)
  2. 缓存策略(Caching Strategies)
  3. 组件特有的设置项,例如编码质量,或Bitmap的解码配置等
RequestOptions cropOptions = new RequestOptions();
cropOptions.circleCrop();
Glide.with(ImageActivity.this)
.load(uri3)
.apply(cropOptions)
.into(imageView);

2.11 图片缓存策略

为了提升用户体验,图片展示的目标

  1. 每次以最快的速度打开图片
  2. 减少网络流量的消耗
  3. 都缓存在内存,容易出现OOM

如何控制缓存大小:LRU(Least Recently Used)

image-20220906204038719

三、音视频播放

3.1 视频帧

image-20220906204329793

3.2 视频编码

视频编码:通过特定的压缩技术,将某个视频内容数据转换成另⼀种特定格式文件的方式

image-20220906204620020

3.3 视频封装格式

PC和移动设备上都离不开视频播放,涉及的视频格式也比较多,不同扩展名实际上使用了不同的视频和音频编码

image-20220906204714348

3.4 视频播放

  1. 视频解码播放的⼤部分流程,整个视频播放的流程如右图所示
  2. 视频文件都会有特定的封装格式、比特率、时长等信息
  3. 视频解封装之后,对应视频流和⾳频流。
  4. 解复用之后的⾳视频有自己独立的参数,包括
    1. 编码方式、采样率、画面大小等
    2. 音频参数包括采样率、编码方式和声道数等

image-20220906205043235

3.5 Android音频播放

image-20220906205244255

3.6 Media Player

Android音视频播放相关类:MediaPlayer

image-20220906205319512

3.7 MediaPlayer播放视频

MediaPlayer需要配合SurfaceHolder

image-20220906205555970

3.8 VideoView播放视频

image-20220906205647513

image-20220906205709970

3.9 第三方播放器

image-20220906205730172

Day8 Android多媒体进阶

一、视频介绍

1.1 视觉成像的基础原理

1.1.1 视网膜:视锥细胞

image-20220907165815403

1.1.2 RGB:Red, Green, Blue

image-20220907114039455

1.1.3 YUV:Luma & Chroma

  1. Luma:黑白值,人眼对其比较敏感
  2. Chroma:彩度

image-20220907114118012

1.1.4 Chroma下采样:YUV420

image-20220907115145731

1.2 视频是什么

image-20220907115216486

1.3 分辨率、帧率、码率

  1. 分辨率:图像内像素的数量,通常使用宽*高表示

    1. HD:全高清,1920*1080

    image-20220907115255261

  2. 帧率:每秒钟播放的图片数量,单位是FPS

    1. 帧率越高,视频越连续

    image-20220907170259982

  3. 码率:视频文件在单位时间内使用的数据流量,单位是kbps(千位每秒)

    1. 视频大小 = duration时长(s) × kbps 千位每秒 / 8 = xxMB

    image-20220907170420352

1.4 视频编码

1.4.1 原理

  1. 空间冗余:图像相邻像素之间有较强的相关性
  2. 时间冗余:视频序列的相邻图像之间内容相似,可以压缩成⼀个关键帧+变化差值
  3. 感知冗余:在人在观看视频时,人眼无法察觉的信息

image-20220907170520111

1.4.2 I/B/P帧

  1. I帧:关键帧,去除空间冗余信息
  2. P帧:前向预测帧,去除时间冗余信息
  3. B帧:双向预测帧,去除时间冗余信息

image-20220907170655294

1.4.3 编码格式

  1. ⽬前主要使用的编码格式有H264H265
    1. H264可以达到百倍的压缩率
    2. H265H264的压缩率增加⼀倍

1.4.4 封装格式

  1. 把视频码流和音频码流按照⼀定的格式存储在⼀个文件中
  2. 与编码格式无关
  3. 常用的格式有MP4AVIFLVRMVB

二、相机拍照

2.1 调起系统相机

打开系统相机

image-20220907171037772

接收数据,拿到返回的bitmap,并显示在屏幕上

  1. 接收到的bitmap,默认会被进行压缩

image-20220907171112102

2.2 自定义存储路径

申请存储权限

image-20220907171301200

创建文件

image-20220907171320818

获取content:// URI

  1. 7.0以上手机不允许使用file:// URI跳出应⽤
  2. android:authorities:需要是唯一的字符串,后面会用到
  3. android:resource@xml/file_paths打开是下面的代码

image-20220907171847367

@xml/file_paths

  1. 通过external-files-path的方式,保证路径是Android/data/com.bytedance.camera.demo/files/Pictures

image-20220907171905455

设置存储地址

image-20220907172107027

2.3 显示图片

防止内存溢出:

  1. 获取view的宽高
  2. 获取图片的宽高
    1. 使用Options.inJustDecodeBounds = true,不返回实际的bitmap文件,只获取其属性
  3. 计算缩放比例
  4. 获取bitmap
  5. 显示在屏幕上

image-20220907172323198

2.4 显示效果

  1. 读取的图片旋转角度
    1. 通过ExitInterface.getAttributeInt()获取旋转角度
  2. matrix中设置要旋转的角度
  3. 旋转图片

image-20220907172904808

三、简单录制

3.1 调起系统相机

调起相机的录像界面

image-20220907173337047

3.2 显示录制视频

获取拍摄的视频,并显示在界面上,开始播放

image-20220907173403949

3.3 将视频存储到自定义存储路径中

2.2类似

3.4 查看数据

image-20220907173502070

四、自定义录制

4.1 获取Camera实例

申请权限

image-20220907173635705

可用摄像头的数量

Camera.getNumberOfCameras

获取后置摄像头

  1. 使用camera.setDisplayOrientation()方法,旋转预览图片

image-20220907173717133

4.2 摄像头数据实时显示

使用控件:

  1. SurfaceView

关键类:

  1. Camera:使用的摄像头
  2. SurfaceView:预览类
    1. 一个SurfaceView通常有两个Surface:双缓冲区
  3. SurfaceHolderSurface的持有者
    1. 通过SurfaceHolder对Surface进行生命周期的管理
  4. SurfaceHolder.Callback

image-20220907173808801

4.3 SurfaceView

  1. 拥有独立绘图层的特殊View
  2. 双缓冲机制
  3. 通过在Window上面“挖洞”(设置透明区域)进行显示

image-20220907174336816

4.4 处理Activity的声明周期对预览的影响

image-20220907174458829

4.5 拍摄一张实时图片

使用Camera API拍照

mCamera.takePicture(null,null,mPicture);

拍照后继续预览

image-20220907174611358

4.6 MediaRecorder

4.6.1 MediaRecorder的状态机

image-20220907174802680

4.6.2 开始录制(按部就班)

  1. 解锁Camera
  2. 设置音频、视频源
  3. 设置视频的编码格式、输出效果
  4. 设置视频文件的保存位置
  5. 设置Surface
  6. MediaRecorder处于prepare状态
  7. 开启MediaRecorder

image-20220907174909098

4.6.3 结束录制(按部就班)

  1. 停止MediaRecorder
  2. 重置MediaRecorder
  3. 释放MediaRecorder
  4. 锁定Camera

image-20220907174933063