Android开发
Day1 Android简介
快捷键
运行
- 运行:Shift+F10
- 终止:Ctrl+F2
日志
- 生成TAG常量:在方法外输入logt + TAB
- 打印不同级别的日志:在方法内输入logd/logi/logw/loge + TAB
第一章 开始启程--你的第一行Android代码
1.1 Android的四大组件
- 活动(Activity) :
- 活动是所有Android应用程序的门面,凡是在应用中你看得到的东西 , 都是放在活动中的
- 服务(Service):
- 你无法看到它,但它会一自在后台默默地运行
- 即使用户退出了应用,服务仍然是可以继续运行的。
- 广播接收器(BroadcastReceiver) :
- 广播接收器允许你的应用接收来自 各处的广播消息, 比如电话、短信等
- 当然你的应用同样也可以向外发出广播消息
- 内容提供器(ContentProvider):
- 内容提供器则为应用程序之间共享数据提供了可能
- 比如你想要读取系统电话部中的联系人,就需要通过内容提供器来实现。
1.2 项目目录
1.2.1 .gradle和idea
- 这两个目录下放置的都是Android Studio自动生成的一些文件
- 我们无须关心、 也不要去手动编辑
1.2.2 app
- 项目中的代码、资源等内容几乎都是放置在这个目录下的、 我们后面的开发工作也基本都是在这个目录下进行的, 待会儿还会对这个目录单独展开进行讲解
1.2.3 build
- 这个目录你也不需要过多关心, 它主要包含了一些在编译时自动生成的文件
1.2.4 gradle
- 这个目录下包含了gradle wrapper的配置文件
- 使用gradle wrapper的方式不需要提前将gradle下载好,而是会自动根据本地的缓存情况决定是否需要联网下载gradle
- Android Studio默认没有启用gradle wrapper的方式,如果需要打开,可以点击Android Studio导航栏|File|Settings|Build, Execution, Deployment | Gradle,进行配置更改。
1.2.5 . gitignore
- 这个文件是用来将指定的目录或文件排除在版本控制之外的
1.2.6 build.gradle
- 这是项目全局的gradle构建脚本 , 通常这个义件中的内容是不需要修改的
1.2.7 gradle.properties
- 这个文件是全局的gradle配置文件 ,存这里配置的屈性将会影响到项目中所有的gradle编译脚本
1.2.8 gradlew和gradlew.bat
- 这两个文件是用来在命令行界面中执行gradle命令的
- 其中gradlew是在Linux或Mac系统中使用时,gradJew.bat是在Windows系统中使用的
1.2.9 HelloWorld.iml
- iml文件是所有lntelliJ IDEA项目都会自动生成的一个文件(Android Studio是基T lntelliJ IDEA开发的)
- 用于标识这是一个lntelliJ IDEA顶目
- 我们不需要修改这个文件中的任何内容
1.2.10 local.properties
- 这个文件用于指定本机中的Android SDK路径
- 通常内容都是自动生成的, 我们并不需要修改,除非你本机中的Android SOK位置发生了变化 ,那么就将这个文件中的路径改成新的位置即可
1.2.11 settings.gradle
- 这个文件用于指定项目中所有引入的模块
- 由于HelloWorld项目中就只有一个app模块,因此该文件中也就只引入了app这一个模块
- 通常情况下模块的引入都是自动完成的,需要我们手动去修改这个文件的场景可能比较少
1.3 app目录
1.3.1 build
- 这个目录和外层的build目录类似,主要也是包含了一些在编译时自动生成的文件,不过它里面的内容会更多更杂,我们不需要过多关心
1.3.2 libs
- 如果你的项目中使用到了第三方jar包,就需要把这些jar包都放在libs目录下
- 放在这个目录下的jar包都会被自动添加到构建路径里去。
1.3.3 androidTest
- 此处是用来编写Android Test测试用例的, 可以对项目进行一些自动化测试
1.3.4 java
- 毫无疑问,java目录是放置我们所有Java代码的地方
- 展开该目录,你将看到我们刚才创建的HelloWorldActivity文件就在里面。
1.3.5 res
Android布局下的res文件夹:
项目目录下的res文件夹:
- 项目中使用到的所有图片、布局 、字符串等资源都要存放在这个目录下。
- 当然这个目录下还有很多子目录,所以你不用担心会把整个res目录弄得乱糟糟的
- drawable目录:图片,多个文件夹用于适用不同设备的分辨率
- layout目录:布局
- mipmap目录:应用图标,多个文件夹用于适用不同设备的分辨率
- values目录:字符串、样式、颜色等配置
1.3.6 AndroidManifest.xml
- 这是你整个Android项目的配置文件
- 你在程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用程序添加权限声明。
- 由于这个文件以后会经常用到,我们用到的时候再做详细说明
1.3.7 test
- 此处是用来编写Unit Test测试用例的,是对项目进行自动化测试的另一种方式
1.3.8 .gitignore
- 这个文件用于将app 模块内的指定的目录或文件排除在版本控制之外,作用和外层的.gitignore文件类似
1.3.9 app.iml
- IntelliJ IDEA项目自动生成的文件, 我们不需要关心或修改这个文件中的内容。
1.3.10 build.gradle
- 这是app模块的gradle构建脚本,这个文件中会指定很多项目构建相关的配置
1.3.11 proguard-rules.pro
- 这个文件用于指定项目代码的混淆规则
- 当代码开发完成后打成安装包文件,如果不希望代码被别入破解, 通常会将代码进行混淆, 从而让破解者难以阅读
1.4 项目的运行
1.4.1 AndroidManifest.xml:注册活动
|
1.4.2 MainActivity.java
public class MainActivity extends AppCompatActivity { |
1.4.3 activity_main.xml
|
1.4.4 res/values/strings.xml
<resources> |
- 定义了一个应用程序名的字符串
- 引用方式:
- 在代码中:R.string.app_name
- 在XML中:@string/app_name
- string可以换成drawable、mipmap、layout等,用于引用其它文件夹内的资源
1.4.5 build.gradle
外层的build.gradle文件
plugins { |
app/build.gradle文件
plugins { |
三种依赖关系:
- 本地依赖:可以对本地的Jar包或目录添加依赖关系
- 库依赖:可以对项目中的库模块添加依赖关系
- 远程依赖:可以对jcenter库上的开源项目添加依赖关系
- com.google.android.material:域名
- :material:组名
- :1.4.0:版本号
1.5 日志工具Log
1.5.1 日志工具类的使用
Android中的日志工具类是Log (android.util.Log). 这个类中提供如下5个方法来供我们打印日志
- Log.v():用于打印那些最为琐碎的意义最小的日志信息。对应级别verbose,是Android日志里面级别最低的一种
- Log.d():用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别debug,比verbose高一级
- Log.i():用于打印一些比较重要的数据,这些数据应该是你非常想看到的,可以帮你分析用户行为数据。 对应级别info, 比debug高一级
- Log.w():用于打印一些警告信息,提示程序在这个地力可能会有潜在的风险,最好去修复一下这些出现瞥告的地方。对应级别warn,比info高一级
- Log.e():用于打印程序中的错误信息,一般都代表程序出现严重问题了,必须尽快修复。对应级别error,比warn高一级
protected void onCreate(Bundle savedInstanceState) { |
1.5.2 Logcat的使用
过滤器:
自定义过滤器:在上述图标下拉栏中,选择Edit Filter Configuration
- Log Tag:按照日志的TAG过滤
按照日志的级别过滤:只会显示高于选中级别的日志
Day2 Android基础UI开发
第二章 先从看得到的入手--探究活动
2.1 活动的基本用法
2.1.1 手动创建活动
在下图目录下,右击|新建|Activity|Empty Activity
不要勾选
- Generate a Layout File:自动创建一个对应的布局文件
- Lancher Activity:自动将创建的活动MainActivity设置为当前项目的主活动
2.1.2 创建和加载布局
在下图目录下,右击|新建|Layout Resource File
布局文件的首字母要小写
first_layout.xml
<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>FirstActivity.java
public class FirstActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.first_layout);
//给当前活动加载一个布局
}
}
2.1.3 在AndroidMainfest文件中注册
|
2.1.4 在活动中使用Toast
Toast是Android系统提供的一种非常好的提醒方式
在程序中可以使用它将一些短小的信息通知给用户,这些信息会在一段时间后自动消失,并且不会占用任何屏幕空间,
我们现在就尝试一下如何在活动中使用Toast:通过Button触发Toast,修改FirstActivity.java
public class FirstActivity extends AppCompatActivity {
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()方法
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
在res目录下,新建一个目录menu
右击menu文件夹|新建|Menu Resource File,文件名输入main
修改main.xml文件
<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>修改FirstActivity.java文件,重写onCreateOptionMenu()方法
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
// 通过getMenuInflater()方法能够得到MenuInflater对象
// 再调用它的inflate()方法就可以给当前活动创建菜单了
// inflate的两个参数:
// 第一个: 指定通过哪一个资源文件来创建菜单
// 第二个: 指定我们的菜单项将添加到哪一个Menu对象中
return true;
// 返回true: 表示允许创建的菜单显示出来
}定义菜单响应事件:重写onOptionsItemSelected函数
public boolean onOptionsItemSelected( { 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 销毁一个活动
调用Activity类的finish()方法即可,修改Button监听器中的代码
button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Toast.makeText(FirstActivity.this, "You clicked Button ",Toast.LENGTH_SHORT).show();
finish();
}
});
2.2 使用Intent在活动之间穿梭
2.2.1 使用显式Intent
右击com.example.hw2_Activity包|新建|Activity|Empty Activity,创建一个新活动
这次勾选Generate Layout File,并将布局文件起名为second_layout,但是不要勾选Launcher Activity
进入布局文件,新建一个Button控件
注意:所有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>使用显式Intent启动活动:修改FirstActivity.java中的按钮点击事件
button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
// 第一个参数: 启动活动的上下文
// 第二个参数: 目标活动
startActivity(intent);
}
});
2.2.2 使用隐式Intent
相比于显式Intent,隐式Intent则含蓄了许多,它并不明确指出我们想要启动哪一个活动,而是指定了一系列更为抽象的action和category等信息,然后交由系统去分析这个Intent,并帮我们找出合适的活动去启动。
什么叫作合适的活动呢?简单来说就是可以响应我们这个隐式Intent的活动。
修改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>修改FirstActivity.java中的按钮点击事件
button1.setOnClickListener(new View.OnClickListener() {
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的用法
打开网页:修改FirstActivity.java中的按钮点击事件
button1.setOnClickListener(new View.OnClickListener() {
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);
}
});setData()方法:指定当前Intent正在操作的数据
- 与此对应,我们还可以在< intent-filter
>标签中再配置一个< data
>标签,用于更精确地指定当前活动能够响应什么类型的数据。<
data >标签中主要可以配置以下内容。
- android:scheme:用于指定数据的协议部分,如上例中的http部分
- android: host:用于指定数据的主机名部分,如上例中的www.baidu.com部分
- android:port:用于指定数据的端口部分,一般紧随在主机名之后
- android:path:用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容
- android:mimeType:用于指定可以处理的数据类型,允许使用通配符的方式进行指定。
- 只有< data >标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent
- 不过一般在< data >标签中都不会指定过多的内容
- 如上面浏览器示例中,其实只需要指定android:scheme为http,就可以响应所有的http协议的Intent了
- 与此对应,我们还可以在< intent-filter
>标签中再配置一个< data
>标签,用于更精确地指定当前活动能够响应什么类型的数据。<
data >标签中主要可以配置以下内容。
写一个能够响应打开网页的活动
- 新建一个活动ThirdActivity
- 修改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>拨打电话:
button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
});
2.2.4 向下一个活动传递数据
给出数据:putExtra()方法
button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
String data = "Hello SecondActivity";
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("extra_data", data);
// 第一个参数是键, 第二个参数是传递的数据
startActivity(intent);
}
});接收数据:getStringExtra()方法
public class SecondActivity extends AppCompatActivity {
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 返回数据给上一个活动
需要返回数据的启动新活动:startActivityForResult()方法
button1.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
}
});返回数据:setResult()方法
button2.setOnClickListener(new View.OnClickListener() {
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();
}
});获取返回数据:onActivityResult()方法
protected void onActivityResult(int requestCode, int resultCode, 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;
}
}按返回键返回:onBackPressed()方法
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
- 其实Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)
- 栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶的位置。而每当我们按下Back键或调用finish()方法去销毁一个活动时,处于栈顶的活动会出栈,这时前一个入栈的活动就会重新处于栈顶的位置
- 系统总是会显示处于栈顶的活动给用户
2.3.2 活动状态
- 运行状态:活动位于返回栈栈顶
- 暂停状态:活动不在栈顶,但是仍然可见
- 停止状态:活动不在栈顶,且不可见
- 销毁状态:活动不在返回栈中
2.3.3 活动的生命周期
2.3.3.1 对应的7个回调方法
- onCreate():它会在活动第一次被创建的时候调用。
- 你应该在这个方法中完成活动的初始化操作,比如说加载布局、绑定事件等
- onStart():这个方法在活动由不可见变为可见的时候调用
- onResume():这个方法在活动准备好和用户进行交互的时候调用。
- 此时的活动一定位于返回栈的栈顶,并且处于运行状态
- onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用。
- 我们通常会在这个方法中将一些消耗CPU的资源释放掉,以及保存一些关键数据
- 但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用
- onStop():这个方法在活动完全不可见的时候调用。
- 它和onPause()方法的主要区别在于,如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行
- onDestroy():这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态
- onRestart():这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了
2.3.3.2 将活动注册为对话框
<activity |
2.3.4 活动被回收了怎么办
存储临时数据:onSaveInstanceState()
public void onSaveInstanceState( { Bundle outState, PersistableBundle outPersistentState)
super.onSaveInstanceState(outState, outPersistentState);
String temoData = "Something you just Typed";
outState.putString("data_key", temoData);
}恢复临时数据: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 活动的启动模式
- 可以在AndroidManifest.xml中,通过给< activity >标签指定android:launchMode属性,来指定启动模式
2.4.1 standard
- 默认启动模式
- 不检查栈中有是否已有这个Activity,总会创建⼀个新Activity,并push到栈顶
2.4.2 singleTop
- 检查栈顶判断是否需要新建Activity
- ⾮栈顶的元素不会检查,所以当FirstActivity不位于栈顶时,再次startActivity(FirstActivity)还会再创建⼀个FirstActivity
2.4.3 singleTask
- 每次启动该活动时系统⾸先会在返回栈中检查是否存在该Activity的实例
- 如果发现已经存在则直接使⽤该实例,并把在这个Activity之上的所有Activity统统出栈
2.4.4 singleInstance
会启用一个新的返回栈来管理指定为singleInstance的Activity
使⽤场景:跨进程(app)间的Activity实例共享,不管是哪个应⽤程序来访问这个Activity,都共⽤同⼀个返回栈
2.5 活动的最佳实践
2.5.1 知晓当前是在哪一个活动
首先,新建一个BaseActivity类,继承自AppCompatActivity
重写OnCreate()方法
public class BaseActivity extends AppCompatActivity {
protected void onCreate( { Bundle savedInstanceState)
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}让BaseActivity称为hw2_activity项目中所有活动的父类
- 修改FirstActivity、SecondActivity、ThirdActivity,让他们不再继承于AppCompatActivity,而是继承自BaseActivity
2.5.2 随时随地退出程序
新建一个类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());
// 只能杀掉当前进程
}
}修改BaseActivity中的代码
public class BaseActivity extends AppCompatActivity {
protected void onCreate( { Bundle savedInstanceState)
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
protected void onDestroy() {
super.onDestroy();
ActivityCollector.removeActivity(this);
}
}在ThirdActivity界面中点击按钮直接退出程序
public class ThirdActivity extends BaseActivity {
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() {
public void onClick(View view) {
Log.d("ThirdActivity", "onClick: ");
ActivityCollector.finishAll();
}
});
}
}
2.5.3 启动进程的最佳写法
启动Secondary进程时,我们可能并不知道它需要哪些参数,要么去问负责这个活动的同学,要么自己去看代码
我们只需要在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 控件与布局
- UI控件:TextView, ImageView, Button, ProgressBar
- UI布局:LinearLayout, RelativeLayout, FrameLayout,
3.2 常用控件
3.2.1 TextView
- layout_width:控件的宽
- layout_height:控件的⾼
- wrap_content:表示和⾃身内容⼀样的⻓度
- match_parent:表示和⽗组件⼀样的⻓度
3.2.2 px、dp、dpi、density 与 sp
- px:pixel,1px代表屏幕上的⼀个物理像素点
- dpi:dots per inch,对角线每英寸的像素点的个数;该值越大表示屏幕越清,\(dpi=\frac{\sqrt{ {height}^2+{width}^2} }{size}\)。
- density:\(density=\frac{dpi}{60}\)。
- dp/dip:density-independent pixel,设备无关像素,\(dp=\frac{px}{density}\)。
- sp:scale-independent pixel,与缩放⽆关的抽象像素
- 与dp近似,但除了受屏幕密度影响外,还受到⽤户字体大小影响(正相关)
3.2.3 EditText
- 监听输⼊内容变化:TextWatcher
3.2.4 ImageView
静态设置
- android:src:指定drawable(本地图片)或bitmap资源(网络图片)
- android:background:指定ImageView背景(如color)
- android:scaleType:设置图片如何缩放以适应ImageView大小;
- 参数如center,centerCrop等
动态设置
setImageResource:添加资源
mImageView.setImageResource(R.drawable.icon_search);
解析成bitmap后,setRotate设置旋转等
svg和png相比有何优势
- 抗拉伸
- 适配分辨率友好
- 占⽤空间小
3.2.5 Dialogs(自学内容)
- https://developer.android.com/guide/topics/ui/dialogs
- 如何生成,展示,隐藏⼀个Dialog?
- 如何⾃定义Dialog样式,添加Button和Listener?
3.3 基本布局
3.3.1 LinearLayout
android:orientation:表示线性布局排列⽅向
- 可选vertical或horizontal
android:layout_gravity:表示指定控件在layout中的对⻬⽅式
- center_vertical只在orientation=“horizontal”时⽣效
- center_horizontal只在orientation=“vertical”时⽣效
android:layout_weight:使用比例的方式指定控件的大小
- 每个控件在排列⽅向上尺寸占比为:self weight / total weight
- 如下图两个View的weight都为1,则两个View的宽度与屏幕宽度⽐均为\(\frac{1}{1+1}=\frac{1}{2}\)
- 两个View的layout_width的规范写法为0dp
- 如果⼀些控件未指定weight,则这些控件按指定width或height展示。其余指定weight的控件对剩余屏幕宽度或高度进行分割
3.3.2 RelativeLayout
3.3.3 padding 与 margin
3.3.4 FrameLayout(自学内容)
3.3.5 ConstraintLayout
对View A在水平和垂直两个方向上指定限制,每⼀方向上⾄少指定⼀个 限制,限制标的可以是其他View或父View
如果对ViewB同时添加了app:layout_constraintRight_toRightOf=“@id/viewA”和 app:layout_constraintLeft_toLeftOf=“@id/viewA”表示什么含义?
ConstraintLayout的使⽤场景(拓展)
- N等分布局
- 角度布局
- 超长限制优化
dependencies {
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
}
3.3.6 自定义控件
可继承任意Android控件,在此基础上添加或重写功能
更好的封装
举例:六个可输入方格、选中框、光标等
⼀种实现:作为⼀个EditText,⾃定义View(SixWordEditText)继承EditText
Override TextView的onDraw⽅法,绘制每个方格样式和⽂字
TextWatcher监听afterTextChanged
SixWorkEdtiText在XML中引⽤
<com.ss.meetx.roomui.widget.SixWordEditText
android:id="@+id/accessCodeEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
提高复用性
如何在XML中实现下面的UI?使⽤什么布局和控件?层级是怎样的?
input_view.xml
构造SearchTextLayout
// constructor
public SearchTextLayout(Context context, AttributeSet attrs) {
LayoutInflater.from(context).inflate(R.layout.input_view, this)
}引用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
滚动布局,默认垂直方向滑动,也⽀持水平方向滑动HorizontalScrollView
直接子View只能有⼀个
如果⽤ScrollView实现右侧的滑动列表应该怎么做?
开发上有什么不便?性能上有什么弊端?
- 重复写n个View,动态添加View比较复杂
- 初始化时会将所有数据项全部加载出来,没有回收和复同;导致内存占用大和OOM
- 用户体验是加载速度慢,卡顿
3.4.2 RecyclerView
核心:View Holder, Adapter,Recycler View
Item Decorator:Item之间Divider(分割线)
Item Animator:添加删除Item的动画
3.4.3 LayoutManager
LinearLayoutManager(左图)
- 线性造型,类似ListView的功能
- 支持上下或左右滑动,每⼀行或⼀列上仅有⼀个item
GridLayoutManager(中图)
- 网格造型,每个item在滑动方向上的尺寸相同
- 可以通过setSpanSizeLookup和getSpanSize,指定条件(如item中text宽度,item的position等),来控制该item占几个位置(即每⼀行有几个item)
StaggeredGridLayoutManager(右图)
- 瀑布流造型,每个item的尺寸可不相同,错落式布局
- 在其constructor中可指定滑动方向和行数(或列数)
自定义LayoutManager(拓展)
- 继承LayoutManager类
- 重写generateDefaultLayoutParams⽅法,直接返回⼀个⻓宽都为WRAP_CONTENT的LayoutParams即可;
- 重写onLayoutChildren⽅法,在这⾥⾯布局Items(显示出来);具体包括分离和回收有效items(detachAndScrapAttachedViews),获取需要布局的items(可见的),再通过addView将这些item添加回去。然后对其测量 (measureChild)确定View的宽高,
- 使⽤layoutDecorated确定View摆放的位置,并设置跟随滑动放缩比例
- 重写canScrollHorizontally和canScrollVertically方法,使它⽀持⽔平或垂直滚动;
- 重写scrollHorizontallyBy和scrollVerticallyBy,并在这里处理滚动工作;
3.4.4 RecyclerView使用示例
3.4.4.1 添加依赖
- 在app/build.gradle中,添加RecyclerView的依赖
implementation 'androidx.recyclerview:recyclerview:1.1.0' |
3.4.4.2 添加相关内容,表示RecyclerView中每一个独立的item
- 创建Java类ItemData,表示每一个item存储的数据
public class ItemData { |
- 创建layout布局文件recyclerview_item.xml,表示每一个item的布局
|
- 创建Java类MyViewHolder,用于存储每个item的控件,控件都有哪些,在recyclerview_item.xml中定义
public class MyViewHolder extends RecyclerView.ViewHolder{ |
3.4.4.3 添加RecyclerView的Adapter
- 创建Java类MyRecyclerViewAdapter,为RecyclerView控件的Adapter
- 这个类继承自RecyclerView.Adapter< MyViewHolder >
- 这个类会重写View.OnClickListener
- 类中包含了Adapter所在的Context,每个item包含的数据
- 这些是在构造函数中需要传入的
- 创建一个方法setItem(int position, ItemData item),表示修改position地方的数据
- 先修改itemList中的数据
- 然后调用this.notifyItemChanged(position)方法,修改item的控件
- 重写多个方法,实现Adapter的功能
- onCreateViewHolder():item框创立时, 调用该方法
- 根据item对应的layout文件recyclerview_item.xml,创建每个item对应的View视图
- 给View视图设置Listener
- 从itemView中获取MyViewHolder并返回
- onAttachedToRecyclerView():将RecycleView附加到Adapter上时, 调用该方法
- 设置当前Adapter负责的RecyclerView
- onDetachedFromRecyclerView():将RecycleView从Adapter解除时, 调用该方法
- 设置当前Adapter负责的RecyclerView
- onBindViewHolder():item显示时, 调用该方法
- 根据item的位置,设置当前item的每个组件的值
- getItemCount():item的数量
- 重写onClick()方法,处理RecyclerView的点击事件
- 添加自定义接口OnItemClickListener,处理item的点击事件
- 需要实现的方法:void onItemClick(RecyclerView parent, View view, int position, ItemData data);
- 通过recyclerView.getChildAdapterPosition()方法,获取当前点击的item的位置
- 执行具体实现的onItemClick()方法
public class MyRecyclerViewAdapter |
3.4.4.4 在Activity中对RecyclerView控件进行相关的设置
- 在onCreate()中调用自定义方法setRecyclerView()
- 在setRecyclerView()中进行RecyclerView控件的初始化操作
- 设置所有item的默认数据
- 设置RecyclerView控件的Adapter
- 设置RecyclerView控件的LayoutManager
- 设置RecyclerView控件的Adapter的点击事件响应方法OnItemClickListener()
public class MainActivity extends AppCompatActivity { |
3.4.5 回收复用机制
Day3 UI进阶
一、Fragment
1.1 Fragment的基本用法和生命周期
1.1.1 Fragment的优点
- 将Activity模块化,将功能分散到小的Fragment中
- 一个Activity可以有多个Fragment,一个Fragment也可以有多个Fragment
- 可以重用、灵活
- 相比View,带有声明周期的概念
1.1.2 静态添加Fragment
定义fragment布局文件:fragment_hello_layout.xml
<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>定义fragment类:HelloFragment.java
public class HelloFragment extends Fragment {
public View onCreateView( { LayoutInflater inflater, ViewGroup container, 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: 是否将载入的视图绑定到根视图中
}
}在 activity布局文件中嵌入 fragment:
activity_main.xml
<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 {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
1.1.3 动态添加/删除Fragment
在Activity布局文件中定义Fragment容器:activity_main.xml
<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>定义⼀个新的MainFragment:
fragment布局文件:fragment_main_layout.xml
<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 {
public View onCreateView( { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
return inflater.inflate(R.layout.fragment_main_layout, container, false);
}
}使用FragmentManager添加Fragment:MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Button mReplaceButton;
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 生命周期
- onAttach/onDetach:Fragment与Activity绑定/解除绑定
- onCreate/onDestroy:进行与View无关的初始化才做
- onCreateView/onDestroyView:渲染出视图布局,进行与View有关的初始化才做
- onActivityCreated:宿主Activity执行onCreate后调用该方法
- onStart/onStop:可见/不可见
- onResume/onPause:可交互/不可交互
1.1.5 Fragment添加到返回栈
addToBackStack:将新的Fragment添加至返回栈,MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private Button mReplaceButton;
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的作用
- 常用于实现可滑动的多个视图
- 容器,类似于RecyclerView
- 需要通过 Adapter 配置内容
- 内容⼀般通过 Fragment 实现
- 可配置 TabLayout 或三⽅库添加 Title
1.2.2 ViewPager + Fragment
在布局xml 中添加ViewPager :fragment_main_layout.xml
<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>定义Fragment
fragment_view_animation.xml
<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
<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
<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>定义配置页面Fragment的Adapter:HelloFragmentViewPagerAdapter.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( { Fragment fragment)
super(fragment);
}
// 根据position的值, 判断创建哪一个Fragment
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
public int getItemCount() {
return FRAGMENTS_Count;
}
}为ViewPager设置Adapter:MainFragment.java
public class MainFragment extends Fragment {
ViewPager2 mViewPager;
public View onCreateView( { LayoutInflater inflater, ViewGroup container, 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
在布局 xml 中继续添加 TabLayout:fragment_main_layout.xml
<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>在代码中对 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;
public View onCreateView( { LayoutInflater inflater, ViewGroup container, 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 之间的通信
- 构造 Fragment 时传递参数(setArguments/getArguments)
- 通过接口和回调
1.3.1 Fragment与Activity之间的通信
1.3.1.1 传参
在activity_main.xml中添加一个文本框,用于测试
<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>通过传参为Fragment指定⼀个背景色:ViewAnimationFragment.java
- Fragment中提供实例化自身对象的静态方法
- 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;
}
public void onCreate( { Bundle savedInstanceState)
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mColor = getArguments().getInt(PARAM_Color);
}
}
public View onCreateView( { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
View view = inflater.inflate(R.layout.fragment_view_animation, container, false);
view.setBackgroundColor(mColor);
return view;
}
}
1.3.1.2 Listener
宿主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 中获取接⼝实例
public void onAttach( { Context context)
super.onAttach(context);
mListener = (MainFragmentListener) context;
}
public View onCreateView( { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
// 创建view, 见之前的MainFragment...
// 按需调⽤改接⼝⽅法进⾏通信
if(mListener != null){
mListener.onMultiTabsViewCreated(adapter.getItemCount());
}
return view;
}
}宿主Activity中实现接口方法,执行相关处理:MainActivity.java
public class MainActivity extends AppCompatActivity
implements MainFragment.MainFragmentListener {
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(自学)
1.4 总结
- Fragment: 灵活,可重⽤,迷你 Activity
- 生命周期、静态/动态添加⽤法
- ViewPager & Fragment
- 和 Activity 通信:Argument、Listener
二、Animation
2.1 视图动画
2.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() {
public void onAnimationStart(Animation animation) {
Log.d(TAG, "onAnimationStart");
}
public void onAnimationEnd(Animation animation) {
Log.d(TAG, "onAnimationEnd");
}
public void onAnimationRepeat(Animation animation) {
Log.d(TAG, "onAnimationRepeat");
}
});
}配合XML方式设置
fragment_view_animation.xml
<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;
}
public void onCreate( { Bundle savedInstanceState)
super.onCreate(savedInstanceState);
Bundle args = getArguments();
if(args != null){
int givenColor = args.getInt(PARAM_Color);
mColor = (givenColor != 0) ? givenColor : mColor;
}
}
public View onCreateView( { LayoutInflater inflater, ViewGroup container, 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;
}
public void onResume() {
super.onResume();
// 启动动画
initAnimation();
if(mRobot != null){
mRobot.startAnimation(mRotateAnimation);
}
}
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() {
public void onAnimationStart(Animation animation) {
Log.d(TAG, "onAnimationStart");
}
public void onAnimationEnd(Animation animation) {
Log.d(TAG, "onAnimationEnd");
}
public void onAnimationRepeat(Animation animation) {
Log.d(TAG, "onAnimationRepeat");
}
});
}
}
2.1.2 视图动画的属性
2.2 属性动画
2.2.1 属性动画 vs 视图动画
- 属性动画:android.animation
- 基于属性的动画
- ⼀切可以连续变化的属性都是动画的元素
- 实现⼀种复杂动画,就是将动画拆解成不同属性组合的过程
- 视图动画:android.view.animation
- 只能对 View 做动画
- 只能对 View 的某些绘制属性做动画
- 只是视觉效果
2.2.2 属性动画的角色构成
- Property:alpha, scaleX, scaleY, rotation, translationX, translationY
- 参数:StartValue, EndValue, Duration
- RepeatCount:number, infinite
- RepeatMode:restart, reverse
- TypeEvaluator:IntEvaluator, ArgbEvaluator
- Interpolator:linear/accelerate/decelerate
2.2.3 Animator:单个属性动画
2.2.3.1 fragment_object_animation.xml
|
2.2.3.2 ObjectAnimationFragment.java
public class ObjectAnimationFragment extends Fragment { |
2.2.4 AnimatorSet:多个属性动画之间的切换
public class ObjectAnimationFragment extends Fragment { |
2.2.5 特点,xml语法
2.2.6 属性动画核心 - ValueAnimator
- 控制某个数值,在某个时间内,在某个区间内进行规律变化
2.2.7 属性动画原理
插值器:决定变化的规律
估值器:决定变化的具体数值
常用系统内置插值器
2.2.8 属性动画 - 注意
- 使用ObjectAnimator 时,⽬标属性必须同时具备getter()及setter() ⽅法
- ObjectAnimator 操作对象宿主⻚⾯退出前台或销毁时,需保证动画任务得到妥善处理,防⽌内存泄漏
2.2.9 属性动画 - 自定义属性
2.3 Activity 切换动画
2.3.1 示例 FadeInOut
2.4 逐帧动画/Drawable 动画
- 逐帧动画可以被当作⼀种特殊的drawable对象
- 逐帧动画会按次序播放⼀系列图⽚
- 逐帧动画会⼀次性将所有图⽚加载到内存中,会有OOM⻛险
2.4.1 示例 AnimationDrawable
2.5 Lottie
- airbnb公司的开源库
- 可以直接导入AE制作的动画素材
- 本质是将所有动画元素抽象成绘制属性
2.5.1 示例
在app/build.gradle中,添加依赖
dependencies {
//...
implementation 'com.airbnb.android:lottie:3.4.2'
}添加资源raw/lottie_raw_rocket.json
修改fragment_lottie_animation.xml
<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>修改LottieAnimationFragment.java
public class LottieAnimationFragment extends Fragment {
private static final String PARAM_Color = "param_color";
public LottieAnimationFragment(){
}
public View onCreateView( { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
return inflater.inflate(R.layout.fragment_lottie_animation, container, false);
}
}
Day4 复杂应用组件
一、进程与线程
1.1 进程、线程
- 进程:资源分配的最小单位 ==> 一个软件
- 如一辆列车
- 线程:CPU调度的最小单位 ==> 一个软件的各个功能
- 如一辆列车的列车长
- 主要区别:
- 一个进程可以有多个线程
- 同一个进程的多个线程,共享进程的资源
1.2 Android中的主线程
- 启动应用时,系统会为该应用创建⼀个称为“main”(主线程)的执行线程。这个线程负责所有和UI界⾯有关的显示、以及响应UI事件监听任务,因此又称座UI线程。
- 划重点:所有跟ui相关的操作都必须放在主线程
1.3 ANR 拓展
ANR:Application Not Responding
- 程序中所有的组件都会运行在UI线程中,所以必须保证该线程的⼯作效率
- UI线程⼀旦出现问题,就会降低⽤户体验
- 如在UI线程中进行耗时操作,如下载文件、查询数据库等就会阻塞UI线程,长时间⽆法响应UI交互操作,给用户带来“卡屏”、“死机”的感觉
二、Handler机制
2.1 Handler机制
Handler机制为Android系统解决了以下两个问题:
- 任务调度
- 线程通信
2.2 Handler的使用场景
2.3 Handler机制简介
本质是消息机制,负责消息的分发以及处理
- 通俗点来说,每个线程都有⼀个“流⽔线”,我们可往这条流⽔线上放“消息”,流⽔线的末端有⼯作⼈员会去处理这些消息。因为流⽔线是单线的,所有消息都必须按照先来后到的形式依次处理
- 放什么消息以及怎么处理消息,是需要我们去⾃定义的。Handler机制相当于提供了这样的⼀套模式,我们只需要“放消息到流⽔线上”,“编写这些消息的处理逻辑”就可以了,流⽔线会源源不断把消息运送到末端处理。
- 最后注意重点:每个线程只有⼀个“流⽔线”,他的基本范围是线程,负责线程内的通信以及线程间的通信。
- 每个线程可以看成⼀个厂房,每个厂房只有⼀个生产线。
2.4 Handler原理:UI线程与消息队列机制
- Message:消息,由MessageQueue统⼀队列,然后交由Handler处理
- MessageQueue:消息队列,⽤来存放Handler发送过来Message,并且按照先⼊先出的规则执⾏
- Handler:处理者,负责发送和处理Message每个Message必须有⼀个对应的Handler
- Looper:消息轮询器,不断的从MessageQueue中抽取Message并执⾏
2.5 Handler常用方法
// ⽴即发送消息 |
2.6 Handler的使用
- 调度Message:
- 建⼀个Handler,实现handleMessage()⽅法
- 在适当的时候给上面的Handler发送消息
- 调度Runnable:
- 建⼀个Handler,然后直接调度Runnable即可
- 取消调度:
- 通过Handler取消已经发送过的Message/Runnable
2.7 Handler的使用举例
2.7.1 发送Runnable对象
- 新建一个Handler对象:new Handler()
- 新建一个Runnable对象 1. 重写run()方法,表示该Runnable对象要做什么事情
- 调用Handler对象的post()方法,将Runnable对象发送出去
启动今日头条app的时候,展示了一个开屏广告,默认播放3秒;在3秒后,需跳转到主界面
如果用户点击了跳过,则应该直接进入主界面
2.7.2 发送Message对象
- 新建一个Handler对象:new Handler(Looper.getMainLopper()) 1. 重写handlerMessage()方法,表示收到不同Message时做出的反应
- 调用Handler对象的post()方法,将Message对象发送出去 1. 可以直接发送msg.what,来代表一个msg
用户在抖音App中,点击下载视频,下载过程中需要弹出Loading窗,下载结束后提示用户下载成功/失败
2.7.3 辨析Runnable与Message
- Runnable会被打包成Message,所以实际上Runnable也是Message
- 没有明确的界限,取决于使⽤的⽅便程度
以下两段代码等价
2.8 Handler总结
- Handler就是Android中的消息队列机制的⼀个应用,可理解为是⼀种⽣产者消费者的模型,解决了Android中的线程内&线程间的任务调度问题
- Handler机制的本质就是⼀个死循环,待处理的Message加到队列⾥⾯,Looper负责轮询执⾏
- 掌握Handler的基本⽤法:立即/延时/定时发送消息、取消消息
三、Android中的多线程
3.1 Thread
- 新建一个类,继承Thread,重写其run()方法
- 调用时,先新建一个实例 1. 可以传入一个String参数,表示线程的名字
- 调用tread.start()方法,开启线程
3.2 ThreadPool
3.2.1 为什么要使用线程池
- 线程的创建和销毁的开销都比较⼤,降低资源消耗
- 线程是可复用的,提⾼响应速度
- 对多任务多线程进行管理,提高线程的可管理性
3.2.2 几种常用的线程池
- 单个任务处理时间比较短且任务数量很大(多个线程的线程池):
- FixedThreadPool 定长线程池
- CachedThreadPool 可缓存线程池
- 执行定时任务(定时线程池):
- ScheduledThreadPool 定时任务线程池
- 特定单项任务(单线程线程池):
- SingleThreadPool 只有⼀个线程的线程池
3.2.3 使用示例
- 接口Java.util.concurrent.ExecutorService表述了异步执行的机制,并且可以让任务在⼀组线程内执行
- 重要函数:
- execute(Runnable):向线程池提交⼀个任务
- submit(Runnbale/Callable):有返回值(Future),可以查询任务的执行状态和执行结果
- shutdown() :关闭线程池
- 创建一个线程池ExecutorService的示例
- 创建一个Runnable对象,并编写其业务逻辑
- 通过service.execute()方法,向线程池提交任务
3.3 AsyncTask(已弃用)
3.4 HandlerThread
- HandlerThread的本质:继承Thread类
& 封装Handler类
- 试想⼀款股票交易App:
- 由于因为股票的行情数据都是实时变化的
- 所以我们软件需要每隔⼀定时间向服务器请求行情数据
- 该轮询调度需要放到子线程,由Handler + Looper去处理和调度
- 试想⼀款股票交易App:
- HandlerThread是Android API提供的⼀个⽅便、便捷的类,使用它我们可以快速的创建⼀个带有Looper的线程。Looper可以用来创建Handler实例
- 创建一个HandlerThread对象
- 使用handlerThread.start()方法,运行线程
- 通过handlerThread.getLooper()方法,获取该线程的Looper
- 通过Looper实例创建Handler,将Handler与该线程关联
HandlerThread的源码
- onLooperPrepared():
- run():运行该线程
- getThreadHandler():
- quit()和quitSafely():停止该线程
3.5 IntentService(不常用, 自学)
3.6 Android多线程总结
四、自定义View
4.1 View绘制的三个重要步骤
- Measure:测量宽高
- Layout:确定位置
- Draw:绘制形状
- 举例说明:
- 首先画⼀个100 x 100的照片框,需要尺子测量出宽高的长度(measure过程)
- 然后确定照片框在屏幕中的位置(layout过程)
- 最后借助尺子用手画出我们的照片框(draw过程)
4.2 绘制流程
4.3 自定义View:重写onDraw
- Canvas:画布
- Paint:画笔
- 坐标轴:
4.3.1 画点
4.3.2 画线
4.3.3 画圆
4.3.4 填充
4.3.5 不规则图形
4.3.6 画文本
4.4 自定义View总结
- 重要绘制流程:
- Measure:测量
- Layout:布局
- Draw:绘制
- 以及几个重要函数:
- invalidate
- requestLayout
- 理解ViewTree 及 ViewGroup 的Measure / Layout / Draw的流程
- View自定义绘制:
- 绘制图形:点、线、圆形、椭圆、矩形、圆⻆矩形
- 绘制文字
Day5 Android中的网络
一、网络基础知识
1.1 网络分层模型
- 应用层Application Layer:互联网上的各种应用
- 如:看网页、发邮件
- 如:HTTP/FTP/SMTP协议
- 传输层Transport Layer
- 网络层Internet Layer
1.2 HTTP协议
HTTP是一个client-server协议,只能由client主动发起请求,server进行响应
- 用户发出请求,服务端进行相应
一个HTTP请求一定要包含Method和URL
- Method:要做什么,如GET、PUT、HEAD、POST、DELETE、TRACE、OPTIONS、CONNETC
- URL:
Request格式:
Response格式:
HTTP状态码:
- 4XX:服务器正常,但客户段出错
- 404:服务器正常,但服务不存在
- 5XX:服务器不正常,无法处理请求(请求过多)
1.3 RESTful API
1.3.1 API
API:Application Programming Interface 应用程序接口
- 用户端使用固定的方式发起请求
- 服务端提供服务,响应该请求
- 使用一个固定的方式,保证不同模块之间进行通信,提高兼容性
- 略去不同模块之间的区别,找到共同的部分
1.3.2 RESTful API
RESTful API:Resource Epresentational State Transfer (资源)表现层状态转换
- API 是面向资源的,资源表达的形式可以是json或者xml,它的url中不包含动词,而是通过HTTP动词表达想要的动作
- 资源:一段文本、一张图片、一首歌曲
- 表现形式:json、xml
- 状态变化:通过HTTP动词实现
- 目的:看 URL 就知道要什么,看HTTP method 就知道干什么
RESTful 只是一种规范,并不是标准
1.4 数据传输格式
后端与前端/客户端需要约定数据传输的格式,以json为例
- 每一组数据都是键值对
- {}括起的数据:单个值
- []括起的数据:数组值
{ |
1.4.1 JSON解析器:gson
常用API
// 将对象转化成Json |
1.4.2 下划线命名 vs 驼峰命名
1.5 实用工具
1.5.1 JSON相关
- JSON 在线辅助网站:转 JavaBean;合法性校验;压缩;优化预览
- GsonFormatPlus:IDEA 插件,JSON 转 JavaBean
- JSON:维基百科,了解 JSON 的来⻰去脉
1.5.2 抓包工具 Charles
1.5.3 抓包工具 Postman–轻松创建请求
二、Android网络通信基础实现
2.1 添加网络权限
在AndroidManifest.xml中,添加网络权限uses-permission
|
2.2 获取网络中的数据
- 新建一个线程HandlerThread,用于执行与网络有关的任务
- 新建一个任务Handler,用于从url中获取数据
- 在run()方法中
- 调用自定义的getDataFromNetwork()方法,获取数据
- 然后根据得到的数据,调用自定义的showDataFromNetwork()方法,刷新界面
|
- 获取数据:getDataFromNetwork(String urlString)方法
- 将传入的urlString转化为URL
- 使用HttpURLConnection建立连接
- 创建实例connection
- 从connection中获取数据
- 先获取为InputStream
- 然后转化为BufferedReader
- 最后,通过BufferedReader.readLine()方法,将其中的数据转化为JSONObject
- 解析JSON:从JSONObject中,读取出所有data.datas[].title/link
- 根据目标JSON文件的结构,反复调用getJSONObject()、getJSONArray()方法
- 如果当前到达了最低层,则调用getString()方法,获取String类型的数据
- 将数据存放入一个ArrayList中
- 将读取到的ArrayList返回
- 注意要使用try,防止操作出现异常
// 从网络中获取数据 |
2.3 使用WebView打开网页
- 新建一个Activity,其中有WebView控件
<?xml version="1.0" encoding="utf-8"?> |
- 切换至WebViewActivity时,将urlString作为参数,添加进intent中
// 使用单个从网络中获取的数据 |
- 在WebViewActivity初始化时,获取urlString参数,打开网页
- 由于现在的网页包含的内容过多,因此要进行一系列设置,才能正常读取页面的数据
public class Lab5_WebViewActivity extends AppCompatActivity { |
三、进阶实现:Retrofit(自学)
Day6 Android存储
一、存储空间概览
1.1 存储空间的区分
- Internal
Storage:是系统分配给应用的专属内部存储空间
- APP专有的
- 用户不可以直接读取(root用户除外)
- 应用卸载时自动清空
- 有且仅有一个
- External Storage:是系统外部存储空间,如 SD卡
- 所有用户均可访问
- 不保证可用性(可挂载/物理移除)
- 可以卸载后仍保留
- 可以有多个
1.2 存储目录
Internal Storage:/
- APP专用:
- data/data/{your.package.name}/ files、cache、db...
- APP专用:
External Storage:
- APP专用:
- /storage/emulated/0/Android/data/{your.package.name}/ files、cache
- 公共文件夹:./
- --- Standard: DCIM、Download、Movies
- --- Others
- APP专用:
1.3 Internal目录的获取
- file目录:context.getFilesDir()
- cache目录:context.getCacheDir()
- 自定义目录:context.getDir(name, mode_private)
1.4 External目录的获取
1.4.1 获取授权
AndroidManifest.xml中声明权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> |
请求授权(Android 6.0及以上)
private final static int CODE_REQUEST_PERMISSION = 1; |
在 Activity 的 onRequestPermissionsResult 方法中获取授权结果
|
1.4.2 Environment APIs
提供了访问环境变量的方法
public class Environment extends Object{}; |
1.4.3 检查外置存储器的可用性
通过Environment.getExternalStorageState();调⽤获取当前外部存储的状态,并以此判断外部存储是否可⽤
1.4.4 External目录的获取
- 应用私有目录:
- file目录:context.getExternalFilesDir(String type)
- cache目录:context.getExternalCacheDir()
- 公共目录:
- 标准目录:Environment.getExternalStoragePublicDirectory(String type)
- 根目录:Environment.getExternalStorageDirectory()
- 标准目录:
- DIRECTORY_ALARMS
- DIRECTORY_DCIM
- DIRECTORY_DOCUMENTS
- DIRECTORY_DOWNLOADS
- DIRECTORY_MOVIES
1.5 注意事项
- 如果用户卸载应⽤,系统会移除保存在应⽤专属存储空间中的⽂件
- 由于这⼀行为,不应使用此存储空间保存⽤户希望独例于应用而保留的任何内容
- 例如,如果应用允许用户拍摄照片,用户会希望即使卸载应⽤后仍可访问这些照⽚
- 因此,应改为使用共享存储空间将此类文件保存到适当的媒体集合中。
更多信息可参考:https://developer.android.com/guide/topics/data?hl=zh-cn
二、键值对
三、SharedPreferences
3.1 介绍
- SharedPreference 就是 Android 提供的数据持久化的⼀个方式,适合单进程,小批量的数据存储和访问。基于 XML 进行实现,本质上还是⽂件的读写,API 相较 File 更简单。
- 以“键-值”对的方式保存数据的xml⽂件,其文件保存在/data/data/[packageName]/shared_prefs目录下
3.2 获取SharedPreferences
3.3 读取SharedPreferences
通过getxxx()方法获取,需要传入(key,defaultValue)
private static final String SP_NAME = "Lab6_SharedPreference"; |
3.4 写SharedPreferences
通过Editor类来提交修改
private static final String SP_NAME = "Lab6_SharedPreference"; |
3.5 SharedPreferences的原理
3.6 注意事项
- SharedPreference 适合场景:小数据
- SharedPreference 每次写入均为全量写入
- 禁止大数据存储在 SharedPreference 中,导致 ANR
官方推荐的DataStore:https://developer.android.com/topic/libraries/architecture/datastore
四、文件File
4.1 流
都是相对调用者而言的
- 按流向分为:
- 输⼊流
- 输出流
- 按传输单位分为:
- 字节流:InputStream 和 OutputStream 基类
- 字符流:Reader 和 Writer 基类
4.2 API
4.3 文件操作
4.4 文件IO读写操作示例
- 创建 File 对象,通过构造函数:
- new File()
- 创建输⼊输出流对象:
- new FileReader()
- new FileWriter()
- 读取 or 写⼊
- read 方法
- write f昂发
- 关闭资源
- 有借有还,再借不难
public class Lab6_FileIO extends AppCompatActivity { |
4.7 拓展:OkIO
- 是在JavaIO基础上再次进行封装的IO框架
- https://square.github.io/okio/
五、数据库
5.1 使用场景
- 重复的数据
- 结构化的数据
- 关系型数据
5.2 数据库的设计
5.3 SQL
5.4 使用示例:Todo List App
5.4.1 定义Contract类
定义表结构、SQL语句
5.4.2 继承SQLiteOpenHelper
执行Create 和 Delete 操作
5.4.3 获取SQLLiteDatabase
5.4.4 Insert
通过ContentValues进行插⼊操作
5.4.5 Query
调用query()方法,返回 Cursor ,对应查询结果集合
当moveToNext返回 -1时,遍历结束
5.4.6 Delete
删除数据库中对应 id 的数据
5.4.7 Update
5.4.8 Debug
adb + sqlite3:http://www.sqlite.org/cli.html
5.4.8 注意事项
5.5 Room Library
https://developer.android.com/jetpack/androidx/releases/room
六、Content Provider
6.1 定义
- 当我们需要在应⽤间共享数据时,ContentProvider 就是⼀个非常值得使用的组件
- 四大组件之⼀,ContentProvider 是⼀种 Android 数据共享机制,无论其内部数据以什么样的方式组织,对外都是提供统⼀的接口
- 通过 ContentProvider可以获取系统的媒体、联系⼈、⽇程等数据
https://developer.android.com/reference/android/content/ContentProvider
6.2 Content Provider架构
6.3 优点
- 跨应用分享数据
- 系统的 providers 有联系人等
- 是对数据层的良好抽象
- 支持精细的权限控制
七、URI
7.1 URI介绍
URI:Uniform Resource Indentifier,唯一标识ContentProvider的数据
7.2 URI使用示例
7.2.1 查询:获取联系人数据
- AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_CONTACTS" /> |
- 在程序中动态请求权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) |
- 通过ContentResolver 查询对应的ContentProvider
// 获取 ContentResolver 对象 |
- 遍历cursor,获取对应字段的值
7.2.2 查询:获取系统相册中的视频文件
- AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
- 在程序中动态请求权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) |
- 通过ContentResolver查询
// 获取 ContentResolver 对象 |
7.2.3 读取URI对应的图片到ImageView中
- AndroidManifest 权限声明
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> |
- 在程序中动态请求权限
- 重写onRequestPermissionsResult()方法,定义授权后的操作
- 重写onActivityResult()方法,定义选择图片之后的操作
|
- 自定义readImageFromStorage()方法通过URI读取图片
- 自定义saveImageToStorage()方法保存URI
public class Constants { |
Day7 Android多媒体基础
一、图片基础
1.1 图片基础:颜色空间
RGB色彩空间(R=Red, G=Green, B=Blue, A=Alpha)
- ALPHA_8:A = 8位,1字节
- RGB_565:R = 5位 G = 6位 B = 5位,2字节
- ARGB_4444:A = 4位 R = 4位 G = 4位 B = 4位,2字节
- ARGB_8888:A = 8位 R = 8位 G = 8位 B = 8位,4字节
1.2 图片存储:位图
位图以像素(方格点)构成图像,放大时会失真
1.3 图片格式:压缩标准方案
JPEG:Joint Photographic Experts Group 有损压缩方案
- 去除冗余的图像和彩色数据
- 压缩比相对较⾼,⽂件大小相对较小
- 不支持透明图和动态图
PNG:全称Portable Network Graphics
- 高压缩比的无损压缩,⽂件大小比jpeg⾼些
- 最多⽀持48位⾊彩
- 支持α通道数据(透明度)
- 是Android开发上常用的图片格式,例如图标
WebP是谷歌提供的⼀种⽀持有损压缩和无损压缩的图片文件格式
- 比JPEG或PNG更好的压缩
- 在Android 4.0(API level 14)中支持有损的WebP图像
- 在Android 4.3(API level 18)和更高版本中支持无损和透明的WebP图像
1.4 图片存储:矢量图
- 矢量图使用线段和曲线描述图像,由数学向量构成图形图像
- 可以被无限放大而不影响质量
二、Android图片加载
2.1 Graphics包:安卓平台2D图形绘制的基础
2.2 Android Bitmap:一种存储像素的数据结构
2.3 图片加载
2.4 加载超大图片
加载的图片过大,可能会出现OOM(Out of Memory)
2.5 大图分块加载
2.6 图片自适应:NinePatchDrawable
2.7 ImageView
ImageView:显示图片的组件
https://developer.android.com/reference/android/widget/ImageView.ScaleType
ImageView.ScaleType:主要值设置所显示的图片如何缩放或移动以适应ImageView的大小
2.8 Drawable
Drawable在 View 2D 绘画里是⼀个很重要的抽象类,抽象出了怎么画,画什么的⼀个概念
有很多子类
2.9 常见图片库
2.10 Glide
2.10.1 Glide的基础使用
- Glide是⼀个快速高效的Android图片加载库,提供了易用的API,高性能、可扩展的图⽚解码管道
在build.grade里面添加引用
implementation 'com.github.bumptech.glide:glide:4.9.0' |
在代码中使用Glide
Glide.with(context) |
2.10.2 Glide流程
2.10.3 Glide RequestBuilder
- RequestBuilder是Glide中请求的骨架,负责携带请求的url和你的设置项来开始⼀个新的加载过程。
使用RequestBuilder 可以指定:
- 你想加载的资源类型(Bitmap, Drawable, 或其他)
- 你要加载的资源地址(url/model)
- 你想最终加载到的View
- 任何你想应用的(⼀个或多个)RequestOption 对象
- 任何你想应用的(⼀个或多个)TransitionOption 对象
- 任何你想加载的缩略图 thumbnail()
RequestBuilder<Drawable> requestBuilder = Glide.with(fragment).asDrawable(); |
2.10.4 Glide RequestOptions
Glide中的很多设置项都可以通过 RequestOptions 类和 apply() 方法来应用到程序中
使用Request Options可以实现(包括但不限于):
- 转换(Transformations)
- 缓存策略(Caching Strategies)
- 组件特有的设置项,例如编码质量,或Bitmap的解码配置等
RequestOptions cropOptions = new RequestOptions(); |
2.11 图片缓存策略
为了提升用户体验,图片展示的目标
- 每次以最快的速度打开图片
- 减少网络流量的消耗
- 都缓存在内存,容易出现OOM
如何控制缓存大小:LRU(Least Recently Used)
三、音视频播放
3.1 视频帧
3.2 视频编码
视频编码:通过特定的压缩技术,将某个视频内容数据转换成另⼀种特定格式文件的方式
3.3 视频封装格式
PC和移动设备上都离不开视频播放,涉及的视频格式也比较多,不同扩展名实际上使用了不同的视频和音频编码
3.4 视频播放
- 视频解码播放的⼤部分流程,整个视频播放的流程如右图所示
- 视频文件都会有特定的封装格式、比特率、时长等信息
- 视频解封装之后,对应视频流和⾳频流。
- 解复用之后的⾳视频有自己独立的参数,包括
- 编码方式、采样率、画面大小等
- 音频参数包括采样率、编码方式和声道数等
3.5 Android音频播放
3.6 Media Player
Android音视频播放相关类:MediaPlayer
3.7 MediaPlayer播放视频
MediaPlayer需要配合SurfaceHolder
3.8 VideoView播放视频
3.9 第三方播放器
Day8 Android多媒体进阶
一、视频介绍
1.1 视觉成像的基础原理
1.1.1 视网膜:视锥细胞
1.1.2 RGB:Red, Green, Blue
1.1.3 YUV:Luma & Chroma
- Luma:黑白值,人眼对其比较敏感
- Chroma:彩度
1.1.4 Chroma下采样:YUV420
1.2 视频是什么
1.3 分辨率、帧率、码率
分辨率:图像内像素的数量,通常使用宽*高表示
- HD:全高清,1920*1080
帧率:每秒钟播放的图片数量,单位是FPS
- 帧率越高,视频越连续
码率:视频文件在单位时间内使用的数据流量,单位是kbps(千位每秒)
- 视频大小 = duration时长(s) × kbps 千位每秒 / 8 = xxMB
1.4 视频编码
1.4.1 原理
- 空间冗余:图像相邻像素之间有较强的相关性
- 时间冗余:视频序列的相邻图像之间内容相似,可以压缩成⼀个关键帧+变化差值
- 感知冗余:在人在观看视频时,人眼无法察觉的信息
1.4.2 I/B/P帧
- I帧:关键帧,去除空间冗余信息
- P帧:前向预测帧,去除时间冗余信息
- B帧:双向预测帧,去除时间冗余信息
1.4.3 编码格式
- ⽬前主要使用的编码格式有H264和H265
- H264可以达到百倍的压缩率
- H265比H264的压缩率增加⼀倍
1.4.4 封装格式
- 把视频码流和音频码流按照⼀定的格式存储在⼀个文件中
- 与编码格式无关
- 常用的格式有MP4,AVI,FLV,RMVB等
二、相机拍照
2.1 调起系统相机
打开系统相机
接收数据,拿到返回的bitmap,并显示在屏幕上
- 接收到的bitmap,默认会被进行压缩
2.2 自定义存储路径
申请存储权限
创建文件
获取content:// URI
- 7.0以上手机不允许使用file:// URI跳出应⽤
- android:authorities:需要是唯一的字符串,后面会用到
- android:resource:@xml/file_paths打开是下面的代码
@xml/file_paths
- 通过external-files-path的方式,保证路径是Android/data/com.bytedance.camera.demo/files/Pictures
设置存储地址
2.3 显示图片
防止内存溢出:
- 获取view的宽高
- 获取图片的宽高
- 使用Options.inJustDecodeBounds = true,不返回实际的bitmap文件,只获取其属性
- 计算缩放比例
- 获取bitmap
- 显示在屏幕上
2.4 显示效果
- 读取的图片旋转角度
- 通过ExitInterface.getAttributeInt()获取旋转角度
- 在matrix中设置要旋转的角度
- 旋转图片
三、简单录制
3.1 调起系统相机
调起相机的录像界面
3.2 显示录制视频
获取拍摄的视频,并显示在界面上,开始播放
3.3 将视频存储到自定义存储路径中
与2.2类似
3.4 查看数据
四、自定义录制
4.1 获取Camera实例
申请权限
可用摄像头的数量
Camera.getNumberOfCameras |
获取后置摄像头
- 使用camera.setDisplayOrientation()方法,旋转预览图片
4.2 摄像头数据实时显示
使用控件:
- SurfaceView
关键类:
- Camera:使用的摄像头
- SurfaceView:预览类
- 一个SurfaceView通常有两个Surface:双缓冲区
- SurfaceHolder:Surface的持有者
- 通过SurfaceHolder对Surface进行生命周期的管理
- SurfaceHolder.Callback:
4.3 SurfaceView
- 拥有独立绘图层的特殊View
- 双缓冲机制
- 通过在Window上面“挖洞”(设置透明区域)进行显示
4.4 处理Activity的声明周期对预览的影响
4.5 拍摄一张实时图片
使用Camera API拍照
mCamera.takePicture(null,null,mPicture); |
拍照后继续预览
4.6 MediaRecorder
4.6.1 MediaRecorder的状态机
4.6.2 开始录制(按部就班)
- 解锁Camera
- 设置音频、视频源
- 设置视频的编码格式、输出效果
- 设置视频文件的保存位置
- 设置Surface
- 让MediaRecorder处于prepare状态
- 开启MediaRecorder
4.6.3 结束录制(按部就班)
- 停止MediaRecorder
- 重置MediaRecorder
- 释放MediaRecorder
- 锁定Camera