Android

[TOC]

Android基础、项目目录结构

项目结构

Project

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

  2. app:项目中的代码、资源等内容几乎都是放置在这个目录下的,开发工作也基本都是在这个目录下进行的。

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

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

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

  6. build.gradle:这是项目全局的gradle构建脚本,通常这个文件中的内容是不需要修改的。

  7. gradle.properties:这个文件是全局的gradle配置文件,在这里配置的属性将会影响到项目中所有的gradle编译脚本。

  8. gradlew和gradlew.bat:这两个文件是用来在命令行界面中执行gradle命令的,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。

  9. HelloWorld.iml:
    iml文件是所有IntelliJ IDEA项目都会自动生成的一个文件(Android Studio是基于IntelliJ IDEA开发的),用于标识这是一个IntelliJ IDEA项目,我们不需要修改这个文件中的任何内容。

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

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

app目录

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

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

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

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

  5. res:这个目录下的内容就有点多了。简单点说,就是你在项目中使用到的所有图片、布局、字符串等资源都要存放在这个目录下。当然这个目录下还有很多子目录,图片放在
    drawable目录下,布局放在layout目录下,字符串放在values目录下,所以你不用担心
    会把整个res目录弄得乱糟糟的。

  6. AndroidManifest.xml:这是你整个Android项目的配置文件,你在程序中定义的所有四大组件都需要在这个文件里注册,另外还可以在这个文件中给应用程序添加权限声明。

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

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

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

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

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

build.gradle文件

HelloWorld项目中有两个build.gradle文件,一个是在最外层目录下的,一个是在app目录下的。

最外层目录build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.0'

}
}
allprojects {
repositories {
jcenter()
}
}

两处repositories 的闭包中都声明了jcenter()

jcenter是一个代码托管仓库,很多Android开源项目都会选择将代码托管到jcenter上,声明了这行配置之后,我们就可以在项目中轻松引用任何jcenter上的开源项目。

dependencies 闭包中使用classpath声明了一个Gradle插件来构建Android项目,声明
com.android.tools.build:gradle:2.2.0 这个插件。其中,最后面的部分是插件的版本号,为2.2.0

app内部build.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apply plugin: 'com.android.application'     // 应用插件

android {
compileSdkVersion 24 // 指定项目的编译版本
buildToolsVersion "24.0.2" // 指定项目构建工具的版本
defaultConfig {
applicationId "com.example.helloworld" // 指定项目的包名
minSdkVersion 15 // 指定项目最低兼容的Android系统版本
targetSdkVersion 24 // 指定的值表示你在该目标版本上已经做过了充分的测试,会启用该版本的新特性(目标版本)
versionCode 1 // 指定项目的版本号
versionName "1.0" // 指定项目的版本名
}
buildTypes {
release {
minifyEnabled false // 指定是否对项目的代码进行混淆,true 表示混淆,false 表示不混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// proguardFiles 用于指定混淆时使用的规则文件,这里指定了两个文件
// 第一个proguard-android.txt 是在Android SDK目录下的,里面是所有项目通用的混淆规则;
// 第二个proguard-rules.pro 是在当前项目的根目录下的,里面可以编写当前项目特有的混淆规则
}
}
}
dependencies { // 指定当前项目所有的依赖关系
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.2.1'

testCompile 'junit:junit:4.12'
}
  • 第一行apply plugin应用了一个插件,一般有两种值可选:com.android.application 表示这是一个应用程序模块,com.android.library 表示这是一个库模块。应用程序模块和库模块的最大区别在于,一个是可以直接运行的,一个只能作为代码库依附于别的应用程序模块来运行

  • dependencies依赖:

    • 通常Android Studio项目一共有3种依
      赖方式:本地依赖、库依赖和远程依赖。

      • 本地依赖可以对本地的Jar包或目录添加依赖关系
      • 库依赖可以对项目中的库模块添加依赖关系
      • 远程依赖则可以对jcenter库上的开源项目添加依赖关系。
    • 观察一下dependencies闭包中的配置,第一行的compile fileTree 就是一个本地依赖声明,它表示将libs目录下所有.jar后缀的文件都添加到项目的构建路径当中。

    • 而第二行的compile则是远程依赖声明,com.android.support:appcompat-v7:24.2.1就是一个标准的远程依赖库格式,其中com.android.support 是域名部分,用于和其他公司的库做区分;appcompat-v7 是组名称,用于和同一个公司中不同的库做区分;24.2.1是版本号,用于和同一个库不同的版本做区分。

      加上这句声明后,Gradle在构建项目时会首先检查一下本地是否已经有这个库的缓存,如果没有的话则会去自动联网下载,然后再添加到项目的构建路径当中。

    • 库依赖声明这里没有用到,它的基本格式是compile project 后面加上要依赖的库名称,比如说有一个库模块的名字叫helper,那么添加这个库的依赖关系只需要加入compile project(':helper') 这句声明即可。

    • 另外剩下的一句testCompile 是用于声明测试用例库的,这个我们暂时用不到,先忽略它就可以了

res资源目录

  • 所有以drawable开头的文件夹都是用来放图片的
  • 所有以mipmap开头的文件夹都是用来放应用图标的
  • 所有以values开头的文件夹都是用来放字符串、样式、颜色等配置的
  • layout文件夹是用来放布局文件的

mipmap开头的文件夹,其实主要是为了让程序能够更好地兼容各种设备。drawable文件夹也是相同的道理,虽然Android Studio没有帮我们自动生成,但是我们应该自己创建drawable-hdpi、drawable-xhdpi、drawable-xxhdpi等文件夹

string.xml字符串资源
1
2
3
<resources>
<string name="app_name">HelloWorld</string>
</resources>

当需要引用时

  • 在代码中通过R.string.app_name 可以获得该字符串的引用。
  • 在XML中通过@string/app_name 可以获得该字符串的引用。

日志工具使用

使用Android的日志工具Log

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

  • Log.v() 。用于打印那些最为琐碎的、意义最小的日志信息。对应级别verbose,是Android日志里面级别最低的一种。
  • Log.d() 。用于打印一些调试信息,这些信息对你调试程序和分析问题应该是有帮助的。对应级别debug,比verbose高一级。
  • Log.i() 。用于打印一些比较重要的数据,这些数据应该是你非常想看到的、可以帮你分析用户行为数据。对应级别info,比debug高一级。
  • Log.w() 。用于打印一些警告信息,提示程序在这个地方可能会有潜在的风险,最好去修复一下这些出现警告的地方。对应级别warn,比info高一级。
  • Log.e() 。用于打印程序中的错误信息,比如程序进入到了catch语句当中。当有错误信息打印出来的时候,一般都代表你的程序出现严重问题了,必须尽快修复。对应级别error,比warn高一级

Android UI控件

控件 属性 作用
android:layout_width 设置宽 match_parent
fill_parent
wrap_content
android:layout_height 设置高 xx dp
android:gravity 内容对齐方式 top 、bottom 、left 、right、center
android:text 文字内容
android:textSize 文字大小 xx sp
android:textColor 文字颜色
android:textAllCaps 文字全部大写 true/false
EditText android:maxLines 指定最大行数 [number]
ImageView android:src 设置图片路径
android:visibility 设置控件可见属性 visible 、invisible 和gone
ProgressBar style="?android:attr/progressBarStyleHorizontal" 水平进度条
ProgressBar android:max 进度条最大值 100

AlertDialog

对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用builder生成一个dialog
AlertDialog.Builder dialog = new AlertDialog.Builder (MainActivity.this);
// 设置标题
dialog.setTitle("This is Dialog");
// 设置内容
dialog.setMessage("Something important.");
// 可否用Back键关闭对话框
dialog.setCancelable(false);
// 设置确定按钮
dialog.setPositiveButton("OK", new DialogInterface.
OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
// 设置取消按钮
dialog.setNegativeButton("Cancel", new DialogInterface.
OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
// 创建dialog
dialog.create();
// 展示dialog
dialog.show();

如果需要自定义对话框视图

1
2
3
4
View view = LayoutInflater.from(mContext).inflate(R.layout.dialog_add_friend, null);
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
builder.setView(view);
submitBtn = view.findViewById(R.id.btn_submit);

ProgressDialog

进度条对话框

1
2
3
4
5
ProgressDialog progressDialog = new ProgressDialog(MainActivity.this);
progressDialog.setTitle("This is ProgressDialog");
progressDialog.setMessage("Loading...");
progressDialog.setCancelable(true);
progressDialog.show();

菜单Menu

1. 新建菜单Menu资源文件

res/menu/*.xml

menu.xml

1
2
3
4
5
6
7
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"/>
</menu>

2. Activity重写onCreateOptionsMenu()方法

1
2
3
4
5
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.main, menu);
return true;
}

inflate([布局文件],Menu)

返回值为true表示允许将创建的菜单显示出来

3. 重写onOptionsItemSelected()方法

来注册菜单响应事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
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:
}
return true;
}

WebView用法

layout

1
2
3
4
5
6
7
8
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

activity

1
2
3
4
5
6
7
8
9
10
11
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = (WebView) findViewById(R.id.web_view);
// 启用JS支持
webView.getSettings().setJavaScriptEnabled(true);
// 当需要从一个网页跳转到另一个网页时,我们希望目标网页仍然在当前WebView中显示,而不是打开系统浏览器
webView.setWebViewClient(new WebViewClient());
// 加载网页 也可以加载本地的html文件
webView.loadUrl("http://www.baidu.com");
}

需声明网络权限

ListView

1. 编写子元素界面

2. 编写Adapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class FriendsAdapter extends BaseAdapter {

// 界面的布局ID
private final int itemLayoutId;
// 数据
private final List<String> friendsName;
// 环境上下文
private final Context mContext;

public FriendsAdapter(int itemLayoutId, List<String> friendsName, Context mContext) {
// 界面的布局ID
this.itemLayoutId = itemLayoutId;
// 数据源
this.friendsName = friendsName;
// 环境上下文
this.mContext = mContext;
}

@Override
public int getCount() {
// 获得数据的长度
if (friendsName != null) {
return friendsName.size();
}
return 0;
}

@Override
public Object getItem(int position) {
// 获得数据的某个元素
if (friendsName != null) {
return friendsName.get(position);
}
return null;
}

@Override
public long getItemId(int position) {
// 获得某个元素的id
return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
//需要加载子Item的布局,并获取布局中的控件元素,而且设置相应的数据源值
//1. 获取子布局
if (null == convertView) {
// 初始化布局
convertView = LayoutInflater.from(mContext).inflate(itemLayoutId, null);
//初始化ViewHolder对象,以方便保存子布局
viewHolder = new ViewHolder();
//给ViewHolder的属性赋值(初始化页面的控件)
viewHolder.friendNameTv = convertView.findViewById(R.id.friend_name);
//保存子布局
convertView.setTag(viewHolder);
} else {
//获取已经保存的子布局
viewHolder = (ViewHolder) convertView.getTag();
}
// 给子布局中的控件设置相应的数据源值
// 得到子Item的数据
String name = friendsName.get(position);
viewHolder.friendNameTv.setText(name);

return convertView;
}

final static class ViewHolder {
TextView friendNameTv;
}
}

3. 给listView对象设置adapter

1
2
adapter = new NewsListAdapter(R.layout.layout_item, newsList, getContext());
list.setAdapter(adapter);

RecyclerView滚动控件

添加依赖
1
2
3
4
5
6
7
dependencies {

compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:recyclerview-v7:24.2.1'
testCompile 'junit:junit:4.12'
}
Layout引用
1
2
3
4
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Adapter自定义适配器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class FruitAdapter extends RecyclerView.Adapter<FruitAdapter.ViewHolder> {
private List<Fruit> mFruitList;
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView fruitImage;
TextView fruitName;
public ViewHolder(View view) {
super(view);
fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
fruitName = (TextView) view.findViewById(R.id.fruit_name);
}
}
public FruitAdapter(List<Fruit> fruitList) {
mFruitList = fruitList;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.fruit_item, parent, false);
ViewHolder holder = new ViewHolder(view);
return holder;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Fruit fruit = mFruitList.get(position);
holder.fruitImage.setImageResource(fruit.getImageId());
holder.fruitName.setText(fruit.getName());
}
@Override
public int getItemCount() {
return mFruitList.size();
}
}

onCreateViewHolder() 方法是用于创建ViewHolder实例

onBindViewHolder() 方法是用于对RecyclerView子项的数据进行赋值

getItemCount() 用于告诉RecyclerView一共有多少子项

调用生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initFruits(); // 初始化水果数据
RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view);
LinearLayoutManager layoutManager = new LinearLayoutManager(this);

// 设置水平排列
// layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);

// 设置瀑布流
// StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(3, StaggeredGridLayoutManager.VERTICAL);

recyclerView.setLayoutManager(layoutManager);
FruitAdapter adapter = new FruitAdapter(fruitList);
recyclerView.setAdapter(adapter);
}
点击事件

点击事件写到onCreateViewHolder()方法

布局

LinearLayout线性布局

1
2
3
4
5
6
7
8
9
<LinearLayout
// 指定排列方向 垂直/水平
android:orientation="vertical/horizontal"
// 指定该控件在布局中的对齐方式
android:layout_gravity="center..."
// 设置比例,设置后相应的宽/高需设置为0dp
android:layout_weight="weight(num)">

</LinearLayout>

RelativeLayout相对布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
// 与父布局的左对齐
android:layout_alignParentLeft="true"
// 与父布局的右对齐
android:layout_alignParentTop="true"
// 在父布局的正中间
android:layout_centerInParent="true"
// 在button3的上方
android:layout_above="@id/button3"
// 在button3的左方
android:layout_toLeftOf="@id/button3"
// 该控件的左边缘与button3的左边缘对齐
android:layout_alignLeft="@id/button3"
android:text="Button 1" />

FrameLayout帧布局

引入布局

1
<include layout="@layout/title" />

自定义布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TitleLayout extends LinearLayout {
public TitleLayout(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.title, this);
Button titleBack = (Button) findViewById(R.id.title_back);
Button titleEdit = (Button) findViewById(R.id.title_edit);
titleBack.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
((Activity) getContext()).finish();
}
});
titleEdit.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(getContext(), "You clicked Edit button",
Toast.LENGTH_SHORT).show();
}
});

}
}

制作9-Patch图片

Android 资源

Android中的资源,一般分为两类:

  • 系统内置资源:Android SDK中所提供的已经定义好的资源,用户可以直接拿来使用。

  • 用户自定义资源:用户自己定义或引入的,只适用于当前应用的资源。

res目录

res目录:可以使用R类访问的资源,放到该目录下。

res子目录 可以存放的资源
res/anim 定义补间动画的XML文件
res/color 定义不同状态下颜色列表的XML文件
res/drawable 各种位图文件(png、jpg、gif、9-Patch) 可以编译成各种drawable对象的XML文件
res/mipmap 应用程序Launcher图标
res/layout 用户界面布局文件
res/menu 菜单资源布局文件(选择菜单、子菜单、上下文菜单)
res/raw 任意类型的原生资源
res/values 各种简单值的XML文件(包括字符串、整数、数组、尺寸等)
res/xml 其它任意的XML文件(可能没有特殊意义的XML文件)

在Android中使用res目录下资源或系统内置资源:

  • 在其它资源文件中使用资源:
    • 使用res目录下资源:@[<pack_name>:]res_type>/<res_name>
    • 使用系统内置资源:@android:<res_type>/<res_name>
  • 在Java代码中使用资源:(使用资源的标识符)
    • 使用res目录下资源:[<pack_name>.]R.res_type.res_name
    • 使用系统内置资源: android.R.res_type.res_name

getResources()使用

1
2
3
4
Resources res = getResources();
String appName = res.getString(R.string.app_name);
Drawable drawable
= res.getDrawable(R.drawable.ic_launcher);

res/values

res/values/String.xml
1
2
3
4
<resources>
<string name="app_name">HelloWorld</string>
<string name="user_name">张三</string>
</resources>
res/values/colors.xml
1
2
3
<resources>
<color name="colorPrimary">#3F51B5</color>
</resources>
res/values/dimens.xml
1
2
3
<resources>
<dimen name="view_length">100dp</dimen>
</resources>
res/values/array.xml

Array资源可以分为三类:

  • string-array:字符串组成的数组形式。
  • integer-array:int型数据组成的数组形式。
  • array:一般数组形式(数据元素类型不限)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resources>
<string-array name="user_name">
<item>张三</item>
<item>李四</item>
</string-array>
<integer-array name="age">
<item>18</item>
<item>20</item>
</integer-array>
<array name="pic">
<item>@drawable/user1</item>
<item>@drawable/user2</item>
</array>
</resources>

java代码中使用

1
2
3
4
5
6
7
Resources res = getResources();
String[] userName = res.getStringArray(R.array.user_name);
int[] age = res.getIntArray(R.array.age);
TypedArray userPic = res.obtainTypedArray(R.array.pic);
//对于TypedArray类型,需要先获得其TypedArray对象,然后再从该对象中依次获得指定下标的元素。

Drawable firstPic = userPic.getDrawable(0);
res/values/style

Style资源可以设置layout视图组件中某一个视图元素的样式;该资源文件位于res/values目录下,文件名自定义

1
2
3
4
5
6
<resources>
<style name="CustomText">
<item name="android:textSize">20sp</item>
<item name="android:textColor">#FF4081</item>
</style>
</resources>

<style>元素中parent属性表明该样式继承的父级元素样式。

在布局控件中使用该style

1
2
3
4
<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
style="@style/CustomText" />
res/values/theme

Theme资源可以设置Activity窗口的样式;该资源文件位于res/values/目录下,文件名自定义。

  • Theme资源对某个Activity或整个Application起作用,而不是单独的视图组件。
  • Theme主题主要用来设置应用窗口的特征信息(如窗口标题、窗口背景、窗口边框等)。

Theme资源的使用需要从两个角度考虑:

  • 在res/values/目录中定义主题资源文件。
  • 在AndroidManifest.xml文件或Activity中为整个Activity或整个Application应用主题
1
2
3
4
5
6
7
<resources>
<style name="CustomTheme" parent="AppTheme">
<item name="windowNoTitle">true</item>
<item name="android:colorBackground">
@color/custom_theme_color</item>
</style>
</resources>

为应用程序应用主题:

  • 在AndroidManifest.xml文件静态绑定:
    • 可以为<application>元素添加 android:theme属性,属性值即为用户刚才所设计的theme资源(@style/CustomTheme )。
    • 可以为<activity>元素添加 android:theme属性,属性值即为用户刚才所设计的theme资源(@style/CustomTheme )。
  • 在Activity文件中动态绑定主题:
    • 使用Activity类的setTheme( R.style.CustomTheme )方法。
    • 该方法必须在setContentView( )方法或getLayoutInflate( ).inflate( )方法之前被调用。
res/mipmap/

在res/目录下默认情况有六个mipmap子目录。

  • res/mipmap-anydpi-v26 : API26及以上使用自适应图标
  • res/mipmap-mdpi: 中分辨率图标
  • res/mipmap-hdpi、mipmap-xhdpi、mipmap-xxhdpi、mipmap-xxxhdpi: 高分辨率图标

这六个子目录中的图片名可以完全相同,Android应用会根据用户设备分辨率的不同而自行选择。

res/drawable/
类型 描述
Bitmap File 图片文件资源
9-patch File 基于适应内容而自动伸缩的图片(扩展名是 .9.png)
Layer List 一个XML文件,管理一系列图片资源的特殊资源
State List 一个XML文件,针对视图控件不同状态而设置的特殊drawable类型
Level List 一个XML文件,根据视图控件不同等级而现实不同的drawable资源
Shape Drawable 一个XML文件,定义包含颜色和渐变的几何图形
Scale Drawable 一个XML文件,根据当前对准值作相应的平铺处理
Transition Drawable 一个XML文件,用两张图片形成一个渐变效果的drawable资源
res/xml/

在res/xml/目录下的XML文件为任意规范的XML文档,这些文档没有其它附加意义,仅仅是存储数据或展示数据。

这些XML文档,不能在其它XML资源中引用。

在Activity中,可以使用Resources对象的getXML()方法,加载到内置的XML解析器中,以方便处理。

该类资源的一个典型应用是:存储应用中使用到的配置信息。

res/raw/

res/raw/目录存储任意原始格式的文件(可能为.txt、.mp3、.flv等等)。

该目录下的文件,一般不能使用在其它XML资源中。

该目录中的文件,同样会被R类所索引;因此在Activity中可以使用R.raw.***方式引用资源;通过Resources对象的openRawResource()方法,可以获得原始对象的输入流,以方便后续使用。

Assets目录

assets目录:无法直接访问的原生资源(只能通过AssetManager来处理)。

Android Studio创建的项目中,默认不包含assets目录,需要手动创建。在res同级目录创建assets目录

  • assets目录中可以建立子目录,建立更灵活的目录结构。
  • assets目录中的文件格式是任意的,不一定必须是XML文件。
  • assets/目录下文件不会被R类索引,即assets/目录下资源不能使用Resources对象获取。
  • assets/目录下资源不会被打包到APK中,即assets目录中适合存放过大的文件。
  • assets目录下的资源不会被R类处理,因此不能使用res目录下资源的访问方式来访问。
  • 在Java代码中,一般是通过getAssets( )方法获得AssetManager对象,然后再加载指定资源,处理该资源。
1
2
3
4
5
6
AssetManager assetManager = getAssets();
try {
InputStream is = assetManager.open("test.txt");
} catch (IOException e) {
e.printStackTrace();
}

在Android中使用assets目录下资源:

  • AssetManager对象中的常用方法:

    方法名 描述
    String[] list(String path) 返回path目录下所有文件组成的字符串数组形式(若path为空,则表示assets目录)
    InputStream open(String filename) 打开指定fileName表示的文件流,返回该输入流
    XmlResourceParser openXmlResourceParser(String filename) 从assets目录下的fileName文件中加载XML解析器
    void close() 关闭当前AssetManager对象

Activity活动

活动基本用法

注册活动

需要在AndroidManifest.xml中的application节点中注册activity

1
<activity android:name=".FirstActivity"/>

若为主Activity

1
2
3
4
5
6
7
8
<activity android:name=".FirstActivity"
android:label="This is FirstActivity">

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

销毁活动

调用finish()方法

使用Intent

显式Intent

1
2
3
4
5
6
7
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivity(intent);
}
});

隐式Intent

<action> 标签中我们指明了当前活动可以响应某个action ,而<category> 标签则包含了一些附加信息,更精确地指明了当前的活动能够响应的Intent中还可能带有的category 。只有<action><category> 中的内容同时能够匹配上Intent中指定的actioncategory 时,这个活动才能响应该Intent

可以新建Intent时,直接将action字符串传进去

每个Intent中只能指定一个action ,但却能指定多个category

可以使用Intent.addCategory(String category)来指定category,如果没有符合的activity,app就会报错

<intent-filter>标签中再配置一个<data> 标签,用于更精确地指定当前活动能够响应什么类型的数据。

1
2
3
4
5
6
7
// 使用系统浏览器打开百度
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
}

我们还可以在<intent-filter> 标签中再配置一个<data> 标签,用于更精确地指定当前活动能够响应什么类型的数据。<data> 标签中主要可以配置以下属性。

  • android:scheme 。用于指定数据的协议部分,如上例中的http部分。
  • android:host 。用于指定数据的主机名部分,如上例中的www.baidu.com部分。
  • android:port 。用于指定数据的端口部分,一般紧随在主机名之后。
  • android:path 。用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。
  • android:mimeType 。用于指定可以处理的数据类型,允许使用通配符的方式进行指定。

只有<data> 标签中指定的内容和Intent中携带的Data完全一致时,当前活动才能够响应该Intent。

向下一个活动传递数据

1
2
3
4
5
6
7
8
9
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String data = "Hello SecondActivity";
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("extra_data", data);
startActivity(intent);
}
});
1
2
3
4
5
6
7
8
9
10
11
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");
Log.d("SecondActivity", data);
}
}

返回数据给上一个活动

启动新activity

1
2
3
4
5
6
7
button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
startActivityForResult(intent, 1);
}
});

新activity返回数据

1
2
3
4
5
6
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}

旧activity接收数据

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {

switch (requestCode) {
case 1:
if (resultCode == RESULT_OK) {
String returnedData = data.getStringExtra("data_return");
Log.d("FirstActivity", returnedData);
}
break;
default:
}
}

onActivityResult()方法带有三个参数,第一个参数requestCode ,即我们在启动活动时传入的请求码。第二个参数resultCode ,即我们在返回数据时传入的处理结果。第三个参数data ,即携带着返回数据的Intent。由于在一个活动中有可能调用startActivityForResult() 方法去启动很多不同的活动,每一个活动返回的数据都会回调到onActivityResult()这个方法中,因此我们首先要做的就是通过检查requestCode 的值来判断数据来源。确定数据是从SecondActivity返回的之后,我们再通过resultCode 的值来判断处理结果是否成功。最后从data 中取值并打印出来,这样就完成了向上一个活动返回数据的工作。

一般通过重写onBackPressed()方法来返回数据和结束activity

1
2
3
4
5
6
7
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra("data_return", "Hello FirstActivity");
setResult(RESULT_OK, intent);
finish();
}

Activity生命周期

返回栈

Android中的活动是可以层叠的。我们每启动一个新的活动,就会覆盖在原活动之上,然后点击Back键会销毁最上面的活动,下面的一个活动就会重新显示出来。

Android是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈(Back Stack)

返回栈

活动状态

  1. 运行状态

    当一个活动位于返回栈的栈顶时,这时活动就处于运行状态。系统最不愿意回收的就是处于运行状态的活动,因为这会带来非常差的用户体验。

  2. 暂停状态

    当一个活动不再处于栈顶位置,但仍然可见时,这时活动就进入了暂停状态。你可能会觉得既然活动已经不在栈顶了,还怎么会可见呢?这是因为并不是每一个活动都会占满整个屏幕的,比如对话框形式的活动只会占用屏幕中间的部分区域,你很快就会在后面看到这种活动。处于暂停状态的活动仍然是完全存活着的,系统也不愿意去回收这种活动(因为它还是可见的,回收可见的东西都会在用户体验方面有不好的影响),只有在内存极低的情况下,系统才会去考虑回收这种活动。

  3. 停止状态

    当一个活动不再处于栈顶位置,并且完全不可见的时候,就进入了停止状态。系统仍然会为这种活动保存相应的状态和成员变量,但是这并不是完全可靠的,当其他地方需要内存时,处于停止状态的活动有可能会被系统回收。

  4. 销毁状态

    当一个活动从返回栈中移除后就变成了销毁状态。系统会最倾向于回收处于这种状态的活动,从而保证手机的内存充足。

活动的生命周期

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

活动的初始化,比如布局、绑定事件:onCreate() -> 活动的转为可见:onStart() -> 活动转为可以与用户进行交互:onResume() -> 活动转为不可见并释放相关资源:onPause() -> 活动释放资源:onStop() -> 活动销毁:onDestory()

activity生命周期

活动被回收了怎么办

onSaveInstanceState() 方法会携带一个Bundle

保存

1
2
3
4
5
6
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
String tempData = "Something you just typed";
outState.putString("data_key", tempData);
}

恢复

1
2
3
4
5
6
7
8
9
10
11
@Override
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);
}
...
}

活动的启动模式

<activity>节点中指定属性 android:launchMode=""

  • standard:默认启动模式

    standard是活动默认的启动模式,在不进行显式指定的情况下,所有活动都会自动使用这种启动模式。因此,到目前为止我们写过的所有活动都是使用的standard模式。经过上一节的学习,你已经知道了Android是使用返回栈来管理活动的,

    在standard模式(即默认情况)下,每当启动一个新的活动,它就会在返回栈中入栈,并处于栈顶的位置。对于使用standard模式的活动,系统不会在乎这个活动是否已经在返回栈中存在,每次启动都会创建该活动的一个新的实例。

    standard

  • singleTop:顶部不重复

    使用singleTop模式。当活动的启动模式指定为singleTop,在启动活动时如果发现返回栈的栈顶已经是该活动,则认为可以直接使用它,不会再创建新的活动实例。

    singleTop

  • singleTask:任务栈中不重复

    当活动的启动模式指定为singleTask,每次启动该活动时系统首先会在返回栈中检查是否存在该活动的实例,如果发现已经存在则直接使用该实例,并把在这个活动之上的所有活动统统出栈,如果没有发现就会创建一个新的活动实例。

    singleTask

  • singleInstance:单独任务栈

    在这种模式下会有一个单独的返回栈来管理这个活动,不管是哪个应用程序来访问这个活动,都共用的同一个返回栈

    singleInstance

跨应用启动Activity

1
2
3
4
5
Intent intent = new Intent();
ComponentName comName = new ComponentName([PackageName],[PackageName/ActivityName]);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(comName);
startActivity(intent);

活动管理

了解当前界面对应活动

创建自己的Activity基类BaseActivity;并让所有activity继承该类

1
2
3
4
5
6
7
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}

当进入新activity时,会打印当前activity的类名

活动管理器

ActivityCollector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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() {
for (Activity activity : activities) {
if (!activity.isFinishing()) {
activity.finish();
}
}
activities.clear();
}
}

BaseActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
ActivityCollector.addActivity(this);
}
@Override
protected void onDestroy() {
super.onDestroy();

ActivityCollector.removeActivity(this);
}
}

在BaseActivity 的onCreate() 方法中调用了ActivityCollector 的addActivity() 方法,表明将当前正在创建的活动添加到活动管理器里。然后在BaseActivity 中重写onDestroy() 方法,并调用了ActivityCollector 的removeActivity()方法,表明将一个马上要销毁的活动从活动管理器里移除。

退出程序

不管你想在什么地方退出程序,只需要调用ActivityCollector.finishAll() 方法就可以了

退出后还可以杀调当前进程

1
android.os.Process.killProcess(android.os.Process.myPid());

启动Activity

1
2
3
4
5
6
public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, [current_activity.class]);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}

可以快速调用含有该方法的Activity

广播接收者BroadcastReceiver

广播机制简介

  • 标准广播 (Normal broadcasts)是一种完全异步执行的广播,在广播发出之后,所有的广播接收器几乎都会在同一时刻接收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。
    
  • 有序广播 (Ordered broadcasts)则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个广播接收器能够收到这条广播消息,当这个广播接收器中的逻辑执行完毕后,广播才会继续传递。所以此时的广播接收器是有先后顺序的,优先级高的广播接收器就可以先收到广播消息,并且前面的广播接收器还可以截断正在传递的广播,这样后面的广播接收器就无法收到广播消息了。
    

接收系统广播

动态注册监听网络变化

新建一个类,让它继承自BroadcastReceiver ,并重写父类的onReceive() 方法。当有广播到来时,onReceive() 方法就会得到执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 创建一个intent过滤器
intentFilter = new IntentFilter();
// 添加想要接收的intent请求
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
// 实例化 Receiver类
networkChangeReceiver = new NetworkChangeReceiver();
// 动态注册广播接收者
registerReceiver(networkChangeReceiver, intentFilter);
}

@Override
protected void onDestroy() {
super.onDestroy();
// 取消注册广播接收者
unregisterReceiver(networkChangeReceiver);
}
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 当收到广播时,弹窗提示网络状态改变
Toast.makeText(context, "network changes", Toast.LENGTH_SHORT).show();
}
}
}

或者当网络状态改变时,提示当前有网络还是无网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainActivity extends AppCompatActivity {
...
class NetworkChangeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 得到ConnectivityManager实例(一个系统服务类,专门用于管理网络连接)
ConnectivityManager connectionManager = (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
// 得到NetworkInfo的实例
NetworkInfo networkInfo = connectionManager.getActiveNetworkInfo();
// 通过调用NetworkInfo的isAvailable()方法,来判断出当前有无网络
if (networkInfo != null && networkInfo.isAvailable()) {
Toast.makeText(context, "network is available",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, "network is unavailable",
Toast.LENGTH_SHORT).show();
}
}
}

}

同时,访问网络状态需要在AndroidManifest.xml中声明权限

静态注册实现开机启动提醒

需要在AndroidManifest.xml中注册receiver

1
2
3
4
5
6
7
8
9
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<!-- 开机事件广播 -->
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>

同时需要添加相应监听开机的权限

之后便可以添加一个Receiver.java,并在onReceive()中编写相应控制逻辑

1
2
3
4
5
6
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
}
}

不要在onReceive() 方法中添加过多的逻辑或者进行任何的耗时操作,因为在广播接收器中是不允许开启线程的,当onReceive() 方法运行了较长时间而没有结束时,程序就会报错。

发送自定义广播

发送标准广播
定义一个自己的广播接收器
1
2
3
4
5
6
7
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_
SHORT).show();
}
}
1
2
3
4
5
6
7
8
9
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">

<intent-filter>
<action android:name="com.example.broadcasttest.MY_BROADCAST"/>
</intent-filter>
</receiver>

接收一个值为com.example.broadcasttest.MY_BROADCAST的广播

1
2
3
4
5
6
7
8
9
10
11
12
public void onClick(View v) {
Intent intent = new Intent("com.example.broadcasttest.MY_BROADCAST");
sendBroadcast(intent);
}

// 同
public void onClick(View v) {
Intent intent = new Intent();
intent.setAction("com.example.broadcasttest.MY_BROADCAST");
intent.setPackage("com.example.broadcasttest");
sendBroadcast(intent);
}

由于广播是使用Intent进行传递的,因此你还可以在Intent中携带一些数据传递给广播接收器。

发送有序广播

发送时

1
2
3
4
5
public void onClick(View v) {
Intent intent = new
Intent("com.example.broadcasttest.MY_BROADCAST");
sendOrderedBroadcast(intent, null);
}

接收时,通过android.priority属性来设置优先级

1
2
3
4
5
6
7
8
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>

同时通过abortBroadcast()方法来截断广播(之后的广播接收器无法收到)

1
2
3
4
5
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received in MyBroadcastReceiver",
Toast.LENGTH_SHORT).show();
abortBroadcast();
}

使用本地广播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager = LocalBroadcastManager.getInstance(this); // 获取实例
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

Intent intent = new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(intent); // 发送本地广播
}
});
intentFilter = new IntentFilter();
intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
localReceiver = new LocalReceiver();
localBroadcastManager.registerReceiver(localReceiver, intentFilter); // 注
册本地广播监听器
}
@Override
protected void onDestroy() {
super.onDestroy();
localBroadcastManager.unregisterReceiver(localReceiver);
}
class LocalReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "received local broadcast", Toast.LENGTH_SHORT).

show();
}
}
}

与动态注册广播接收器一样,不过现在是通过LocalBroadcastManagergetInstance() 方法得到了它的一个实例,然后在注册广播接收器的时候调用的是LocalBroadcastManagerregisterReceiver() 方法,在发送广播的时候调用的是LocalBroadcastManagersendBroadcast() 方法

本地广播是无法通过静态注册的方式来接收

使用广播来实现强制下线功能

@TODO

数据存储 持久化技术

文件存储

  • Context 类中提供了一个openFileOutput() 方法,可以用于将数据存储到指定的文件中。
    • 第一个参数为文件名(默认存储到/data/data/<package name>/files/目录下)
    • 第二个参数是文件的操作模式,主要有两种模式可选,MODE_PRIVATE和MODE_APPEND。其中MODE_PRIVATE是默认的操作模式,表示当指定同样文件名的时候,所写入的内容将会覆盖原文件中的内容,而MODE_APPEND则表示如果该文件已存在,就往文件里面追加内容,不存在就创建新文件。
    • 返回的是一个FileOutputStream 对象
  • Context 类中还提供了一个openFileInput() 方法,用于从文件中读取数据。
    • 只接收一个参数,即要读取的文件名

SD卡文件读写

  1. 判断是否手机插入SD卡,并且程序有读写SD卡的权限。

    1
    2
    3
    Environment.getExternalStorageState().equals(android
    .os.Environment.MEDIA_MOUNTED);

  2. 获得SD卡目录:getExternalFilesDir(“类型" )

    “类型”参数:可为null或右图所列

    • null:表示sdcard/data/android/包名/files
    • Enviroment.DIRECTORY_DCIM(或“DCIM”):表示sdcard/data/android/包名/files/DCIM

手机插入SD卡,在AVD中通过mksdcard命令来创建。

获得读写SD卡的权限需要在AndroidManifest.xml中添加读写权限。

SharedPreferences存储

SharedPreferences文件是使用XML格式来对数据进行管理的。

将数据存储到SharedPreferences中

获取SharedPreferences对象
  • Context 类中的getSharedPreferences() 方法
    • 第一个参数用于指定SharedPreferences文件的名称,如果指定的文件不存在则会创建一个,SharedPreferences文件都是存放在/data/data/<package name>/shared_prefs/目录下
    • 第二个参数用于指定操作模式,目前只有MODE_PRIVATE这一种模式可选,它是默认的操作模式,和直接传入0效果是相同的,表示只有当前的应用程序才可以对这个SharedPreferences文件进行读写。其他几种操作模式均已被废弃
  • Activity 类中的getPreferences() 方法
    • 它只接收一个操作模式参数。会自动将当前活动的类名作为SharedPreferences的文件名。
  • PreferenceManager 类中的getDefaultSharedPreferences() 方法
    • 这是一个静态方法,它接收一个Context 参数,并自动使用当前应用程序的包名作为前缀来命名SharedPreferences文件
使用对象进行存储
  1. 调用SharedPreferences 对象的edit() 方法来获取一个SharedPreferences.Editor 对象。
  2. 向SharedPreferences.Editor 对象中添加数据,比如添加一个布尔型数据就使用putBoolean() 方法,添加一个字符串则使用putString() 方法,以此类推。
  3. 调用apply() 方法将添加的数据提交,从而完成数据存储操作。
1
2
3
4
5
SharedPreferences.Editor editor = getSharedPreferences("data", MODE_PRIVATE).edit();
editor.putString("name", "Tom");
editor.putInt("age", 28);
editor.putBoolean("married", false);
editor.apply();

从SharedPreferences中读取数据

获得SharedPreferences对象后调用相应的getString() 、getInt() 和getBoolean() 方法来获取

SQLite数据库存储

创建数据库

提供了一个SQLiteOpenHelper帮助类,借助这个类就可以非常简单地对数据库进行创建和升级。SQLiteOpenHelper是一个抽象类

SQLiteOpenHelper中有两个抽象方法,分别是onCreate() 和onUpgrade() ,我们必须在自己的帮助类里面重写这两个方法,然后分别在这两个方法中去实现创建、升级数据库的逻辑。

SQLiteOpenHelper中还有两个非常重要的实例方法:getReadableDatabase() 和getWritableDatabase() 。这两个方法都可以创建或打开一个现有的数据库(如果数据库已存在则直接打开,否则创建一个新的数据库),并返回一个可对数据库进行读写操作的对象。

不同的是,当数据库不可写入的时候getReadableDatabase() 方法返回的对象将以只读的方式去打开数据库,而getWritableDatabase() 方法则将出现异常。

SQLiteOpenHelper构造方法
  • 较少参数构造方法接收4个参数
    • 第一个参数是Context
    • 第二个参数是数据库名,创建数据库时使用的就是这里指定的名称。
    • 第三个参数允许我们在查询数据的时候返回一个自定义的Cursor,一般都是传入null
    • 第四个参数表示当前数据库的版本号,可用于对数据库进行升级操作。
    • 构建出SQLiteOpenHelper的实例之后,再调用它的getReadableDatabase() 或getWritableDatabase() 方法就能够创建数据库了,数据库文件会存放在/data/data/ <package name>/databases/目录下。
    • 此时,重写的onCreate() 方法也会得到执行,所以通常会在这里去处理一些创建表的逻辑。

SQLite不像其他的数据库拥有众多繁杂的数据类型,它的数据类型很简单,integer 表示整型,real 表示浮点型,text 表示文本类型,blob 表示二进制类

使用SQLiteOpenHelper实现类

直接new 实现类,当数据库不存在时,onCreate()会被执行;当构造方法版本号大于原先版本号时,onUpgrade()会被执行

获取数据库对象

调用SQLiteOpenHelper的getReadableDatabase() 或getWritableDatabase() 方法是可以用于创建和升级数据库的。

这两个方法还都会返回一个QLiteDatabase 对象,借助这个对象就可以对数据进行CRUD操作了。

或者使用SQLiteDatabase.openOrCreateDatabse(dbName,null)来获取

添加数据

  • SQLiteDatabase 中提供了一个insert() 方法,这个方法就是专门用于添加数据的。
    • 第一个参数是表名,我们希望向哪张表里添加数据,这里就传入该表的名字。
    • 第二个参数用于在未指定添加数据的情况下给某些可为空的列自动赋值NULL ,一般我们用不到这个功能,直接传入null 即可。
    • 第三个参数是一个ContentValues 对象,它提供了一系列的put() 方法重载,用于向ContentValues 中添加数据,只需要将表中的每个列名以及相应的待
      添加数据传入即可。

更新数据

  • SQLiteDatabase 中也提供了一个非常好用的update() 方法,用于对数据进行更新,这个方法接收4个参数
    • 第一个参数和insert() 方法一样,也是表名,在这里指定去更新哪张表里的数据。
    • 第二个参数是ContentValues 对象,要把更新数据在这里组装进去。
    • 第三、第四个参数用于约束更新某一行或某几行中的数据,不指定的话默认就是更新所有行。==第三参数相当于where部分,第四参数为new String[]为第三参数的占位符提供内容==

删除数据

  • SQLiteDatabase 中提供了一个delete() 方法,专门用于删除数据,这个方法接收3个参数
  • 第一个参数仍然是表名
  • 第二、第三个参数又是用于约束删除某一行或某几行的数据,不指定的话默认就是删除所有行。

查询数据

  • SQLiteDatabase中还提供了一个query() 方法用于对数据进行查询。这个方法的参数非常复杂,最短的一个方法重载也需要传入7个参数。
    • 第一个参数,表名,表示我们希望从哪张表中查询数据。
    • 第二个参数用于指定去查询哪几列,如果不指定则默认查询所有列。
    • 第三、第四个参数用于约束查询某一行或某几行的数据,不指定则默认查询所有行的数据。
    • 第五个参数用于指定需要去group by的列,不指定则表示不对查询结果进行groupby操作。
    • 第六个参数用于对group by之后的数据进行进一步的过滤,不指定则表示不进行过滤。
    • 第七个参数用于指定查询结果的排序方式,不指定则表示使用默认的排序方式。
query()方法参数 对应SQL部分 描述
table from table_name 指定查询的表名
columns select column1, column2 指定查询的列名
selection where column = value 指定where 的约束条件
selectionArgs 为where 中的占位符提供具体的值
groupBy group by column 指定需要group by 的列
having having column = value 对group by 后的结果进一步约束
orderBy order by column1, column2 指定查询结果的排序方式

调用query() 方法后会返回一个Cursor 对象,查询到的所有数据都将从这个对象中取出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 查询Book表中所有的数据
Cursor cursor = db.query("Book", null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
// 遍历Cursor对象,取出数据并打印
String name = cursor.getString(cursor.getColumnIndex
("name"));
String author = cursor.getString(cursor.getColumnIndex
("author"));
int pages = cursor.getInt(cursor.getColumnIndex("pages"));
double price = cur
("price"));
} while (cursor.moveToNext());
}
cursor.close();
Cursor类方法
方法名称 方法描述
getCount() 获得总的数据项数
isFirst() 判断是否第一条记录
isLast() 判断是否最后一条记录
moveToFirst() 移动到第一条记录
moveToLast() 移动到最后一条记录
move(int offset) 移动到指定记录
moveToNext() 移动到下一条记录
moveToPrevious() 移动到上一条记录
getColumnIndexOrThrow(String columnName) 根据列名称获得列索引
getInt(int columnIndex) 获得指定列索引的int类型值
getString(int columnIndex) 获得指定列缩影的String类型值

使用SQL操作数据库

  • 添加数据的方法如下:

    1
    2
    3
    4
    db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
    new String[] { "The Da Vinci Code", "Dan Brown", "454", "16.96" });
    db.execSQL("insert into Book (name, author, pages, price) values(?, ?, ?, ?)",
    new String[] { "The Lost Symbol", "Dan Brown", "510", "19.95" });
  • 更新数据的方法如下:

    1
    db.execSQL("update Book set price = ? where name = ?", new String[] { "10.99", "The Da Vinci Code" });
  • 删除数据的方法如下:

    1
    db.execSQL("delete from Book where pages > ?", new String[] { "500" });
  • 查询数据的方法如下:

    1
    db.rawQuery("select * from Book", null);

可以看到,除了查询数据的时候调用的是SQLiteDatabase的rawQuery() 方法,其他的操作都是调用的execSQL() 方法。

使用LitePal操作数据库

LitePal是一款开源的Android数据库框架,它采用了对象关系映射(ORM)的模式,并将我们平时开发最常用到的一些数据库功能进行了封装,使得不用编写一行SQL语句就可以完成各种建表和増删改查的操作。LitePal的项目主页上也有详细的使用文档,地址是:guolindev/LitePal: An Android library that makes developers use SQLite database extremely easy. (github.com)

配置LitePal

1. 添加LitePal依赖

就是编辑app/build.gradle文件,在dependencies闭包中添加如下内容

1
2
3
4
5
6
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:23.2.0'
testCompile 'junit:junit:4.12'
compile 'org.litepal.android:core:1.4.1'
}
2. 配置litepal.xml文件

右击app/src/main目录→NewDirectory,创建一个assets目录,然后在assets目录下再新建一个litepal.xml文件,接着编辑litepal.xml文件中的内容

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value="BookStore" ></dbname>
<version value="1" ></version>
<list>
</list>
</litepal>
  • <dbname>标签用于指定数据库名
  • <version> 标签用于指定数据库版本号
  • <list>标签用于指定所有的映射模型
3. 配置LitePalApplication
1
2
3
4
5
6
7
8
9
10
11
12
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.litepaltest">
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>

将项目的application配置为org.litepal.LitePalApplication,这样才能让LitePal的所有功能都可以正常工作

创建和升级数据库

创建Book类,定义好相关属性(表的列名)

litepal.xml<list>标签内添加<mapping class="com.example.litepaltest.Book"></mapping>

使用LitePal添加数据

LitePal进行表管理操作时不需要模型类有任何的继承结构,但是进行CRUD操作时就不行了,必须要让 相关表类 继承自DataSupport 类才行,如Book表需要让Book类继承DataSupport类,然后调用.save()方法将数据存入数据库

使用LitePal更新数据

对于LitePal来说,对象是否已存储就是根据调用model.isSaved() 方法的结果来判断的,返回true 就表示已存储,返回false 就表示未存储。

方法一:对已存储的对象,重新调用.save()方法进行更新

方法二:更新从数据库中查到的对象

1
2
3
4
5
6
7
{
Book book = new Book();
book.setPrice(14.95);
book.setPress("Anchor");
book.updateAll("name = ? and author = ?", "The Lost Symbol", "Dan
Brown");
}

**想把一个字段的值更新成默认值时,是不可以使用上面的方式来set 数据的。**将为数据更新成默认值的操作,LitePal统一提供了一个setToDefault() 方法,然后传入相应的列名就可以实现了。

1
2
3
Book book = new Book();
book.setToDefault("pages");
book.updateAll();

使用LitePal删除数据

  • 方法一

    调用过save() 方法的对象,或者是通过LitePal提供的查询API查出来的对象,都是可以直接使用delete() 方法来删除数据的。

  • 方法二

    1
    DataSupport.deleteAll(Book.class, "price < ?", "15");

    这里调用了DataSupport.deleteAll() 方法来删除数据,其中deleteAll() 方法的第一个参数用于指定删除哪张表中的数据,Book.class就意味着删除Book表中的数据,后面的参数用于指定约束条件。

使用LitePal查询数据

@TODO

ContentProvider内容提供者

内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。目前,使用内容提供器是Android实现跨程序共享数据的标准方式。

运行时权限

运行时权限机制

Android现在将所有的权限归成了两类,一类是普通权限,一类是危险权限。准确地讲,其实还有第三类特殊权限,不过这种权限使用得很少,因此不在本书的讨论范围之内。普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,而不需要用户再去手动操作了,比如在BroadcastTest项目中申请的两个权限就是普通权限。危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须要由用户手动点击授权才可以,否则程序就无法使用相应的功能。

查看Android系统中完整的权限列表:Manifest.permission | Android Developers (google.cn)

程序运行时申请权限

Android6.0之前

Android6.0之前,不需要动态申请权限就可以使用危险权限

1
2
3
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);

但需要在AndroidManifest.xml中申请静态权限

1
<uses-permission android:name="android.permission.CALL_PHONE" />

Android6.0以后,动态申请权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new
String[]{ Manifest.permission.CALL_PHONE }, 1);
} else {
call();
}
}
});
}

private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.
PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_
SHORT).show();
}
break;
default:
break;
}
}
}
  1. 先判断用户是不是已经给过授权

    借助的是ContextCompat.checkSelfPermission()方法。

    • checkSelfPermission() 方法:第一个参数是Context;第二个参数是具体的权限名
    • 方法的返回值和PackageManager.PERMISSION_GRANTED 做比较,相等就说明用户已经授权,不等就表示用户没有授权
  2. 如果已经授权,则调用相关执行逻辑

  3. 如果没有授权,则需要调用ActivityCompat.requestPermissions() 方法来向用户申请授权

    • requestPermissions() 方法
      • 第一个参数要求是Activity的实例
      • 第二个参数是一个String 数组,存放要申
        请的权限名
      • 第三个参数是请求码(唯一值)这里传入了1
  4. 最终都会回调到onRequestPermissionsResult() 方法中,而授权的结果则会封装在rantResults 参数当中。这里我们只需要判断一下最后的授权结果,如果用户同意的话就调用call() 方法来拨打电话,如果用户拒绝的话我们只能放弃操作,并且弹出一条失败提示。

访问其他程序中数据

  • 内容提供器的用法:
    • 使用现有的内容提供器来读取和操作相应程序中的数据
    • 创建自己的内容提供器给我们程序的数据提供外部访问接口

ContentResolver的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver() 方法获取到该类的实例。

ContentResolver中提供了一系列的方法用于对数据进行CRUD操作,其中insert() 方法用于添加数据,update() 方法用于更新数据,delete() 方法用于删除数据,query() 方法用于查询数据。

  1. 不同于SQLiteDatabaseContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri 参数代替,这个参数被称为内容URI

    • 内容URI给内容提供器中的数据建立了唯一标识符

      • 主要由两部分组成:authority和path

        • authority是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。一般为[PackageName].provider

        • path则是用于对同一应用程序中不同的表做区分的,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了com.example.app.provider/table1和com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。

        • 内容URI标准格式写法

          1
          2
          content://com.example.app.provider/table1
          content://com.example.app.provider/table2
  2. 得到内容URI字符串之后,我们还需要将它解析成Uri 对象才可以作为参数传入

    需要调用Uri.parse()方法

    1
    Uri uri = Uri.parse("content://com.example.app.provider/table1");
  3. 接下来可以使用ContentResolver来查询其他程序 数据库中表的数据了

    1
    2
    3
    4
    5
    6
    Cursor cursor = getContentResolver().query(
    uri,
    projection,
    selection,
    selectionArgs,
    sortOrder);
    ContentResolver.query()方法参数 对应SQL部分 描述
    uri from table_name 指定查询某个应用程序下的某一张表
    projection select column1, column2 指定查询的列名
    selection where column = value 指定where 的约束条件
    selectionArgs 为where 中的占位符提供具体的值
    sortOrder order by column1, column2 指定查询结果的排序方式
  • insert(uri, contentValues);
  • update(uri, contentValues, “column1 = ? and column2 = ?”, new String[] {“text”, “1”});
  • delete(uri, “column2 = ?”, new String[] { “1” });

读取系统联系人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class MainActivity extends AppCompatActivity {
ArrayAdapter<String> adapter;
List<String> contactsList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView contactsView = (ListView) findViewById(R.id.contacts_view);
adapter = new ArrayAdapter<String>(this, android.R.layout. simple_list_
item_1, contactsList);
contactsView.setAdapter(adapter);
// 判断权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_
CONTACTS) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{ Manifest.
permission.READ_CONTACTS }, 1);
} else {
readContacts();
}

}
private void readContacts() {
Cursor cursor = null;
try {
// 查询联系人数据
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.
Phone.CONTENT_URI, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
// 获取联系人姓名
String displayName = cursor.getString(cursor.getColumnIndex
(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 获取联系人手机号
String number = cursor.getString(cursor.getColumnIndex
(ContactsContract.CommonDataKinds.Phone.NUMBER));
contactsList.add(displayName + "\n" + number);
}
// 通知刷新adapter
adapter.notifyDataSetChanged();
}
} catch (Exception e) {
e.printStackTrace();
} finally {

if (cursor != null) {
cursor.close();
}
}
}

// 接收权限申请返回结果
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.
PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_
SHORT).show();
}
break;
default:
}
}
}

同时还需在AndroidManifest.xml中添加权限

1
<uses-permission android:name="android.permission.READ_CONTACTS" />

创建自己的内容提供器

创建内容提供器的步骤

  1. 新建一个类去继承ContentProvider 的方式来创建一个自己的内容提供器。该类有6个抽象方法

    1. onCreate():初始化内容提供器的时候调用。。通常会在这里完成对数据库的创建和升级等操作,返回true 表示内容提供器初始化成功,返回false则表示失败。

    2. query():从内容提供器中查询数据。使用uri 参数来确定查询哪张表,projection 参数用于确定查询哪些列,selection 和selectionArgs 参数用于约束查询哪些行,sortOrder 参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。

    3. insert():向内容提供器中添加一条数据。使用uri 参数来确定要添加到的表,待添加的数据保存在values 参数中。添加完成后,返回一个用于表示这条新记录的URI。

    4. update():更新内容提供器中已有的数据。使用uri 参数来确定更新哪一张表中的数据,新数据保存在values 参数中,selection 和selectionArgs 参数用于约束更新哪些行,受影响的行数将作为返回值返回。

    5. delete():从内容提供器中删除数据。使用uri 参数来确定删除哪一张表中的数可以看到,几乎每一个方法都会带有Uri 这个参数,这个参数也正是调用ContentResolver的增删改查方法时传递过来的。而现在,我们需要对传入的Uri 参数进行解析,从中分析出调用方期望访问的表和数据。

    6. getType():根据传入的内容URI来返回相应的MIME类型。

      一个内容URI所对应的MIME字符串主要由3部分组成

      • 必须以vnd 开头。
      • 如果内容URI以路径结尾,则后接android.cursor.dir/ ;如果内容URI以id结尾,则后接android.cursor.item/
      • 最后接上vnd.<authority>.<path>
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public class MyProvider extends ContentProvider {
      ...

      @Override
      public String getType(Uri uri) {
      switch (uriMatcher.match(uri)) {
      case TABLE1_DIR:
      return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";
      case TABLE1_ITEM:
      return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";
      case TABLE2_DIR:
      return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";
      case TABLE2_ITEM:
      return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";
      default:
      break;
      }
      return null;
      }
      }
  2. URI参数

    • 标准URI写法

      1
      content://[Package Name]/[Table Name]/[ID]
    • 通配符

      • *:表示匹配任意长度的任意字符
        • 如匹配任意表:content://[Package Name]/*
      • #:表示匹配任意长度的数字
        • 如匹配table表任意一行内容:content://[Package Name]/table/#
  3. UriMatcher类:

    可以轻松地实现匹配内容URI的功能

    • addURI() 方法: 接收3个参数,分别为authoritypath 和一个自定义代码传进去
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public class MyProvider extends ContentProvider {
    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;
    private static UriMatcher uriMatcher;
    static {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);
    uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM);
    uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_DIR);
    uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM);
    }
    ...
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[]
    selectionArgs, String sortOrder) {
    switch (uriMatcher.match(uri)) {
    case TABLE1_DIR:
    // 查询table1表中的所有数据
    break;
    case TABLE1_ITEM:
    // 查询table1表中的单条数据
    break;
    case TABLE2_DIR:
    // 查询table2表中的所有数据
    break;
    case TABLE2_ITEM:
    // 查询table2表中的单条数据
    break;
    default:
    break;
    }
    ...
    }
    ...
    }

实现跨程序数据共享

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
public class DatabaseProvider extends ContentProvider {
public static final int BOOK_DIR = 0;
public static final int BOOK_ITEM = 1;
public static final int CATEGORY_DIR = 2;
public static final int CATEGORY_ITEM = 3;
public static final String AUTHORITY = "com.example.databasetest.provider";

private static UriMatcher uriMatcher;
private MyDatabaseHelper dbHelper;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
}
@Override
public boolean onCreate() {
dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[]
selectionArgs, String sortOrder) {
// 查询数据

SQLiteDatabase db = dbHelper.getReadableDatabase();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
cursor = db.query("Book", projection, selection, selectionArgs, null,
null, sortOrder);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
cursor = db.query("Book", projection, "id = ?", new String[] { bookId },
null, null, sortOrder);
break;
case CATEGORY_DIR:
cursor = db.query("Category", projection, selection, selectionArgs,
null, null, sortOrder);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
cursor = db.query("Category", projection, "id = ?", new String[]
{ categoryId }, null, null, sortOrder);
break;
default:
break;

}
return cursor;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 添加数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
Uri uriReturn = null;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
case BOOK_ITEM:
long newBookId = db.insert("Book", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" +
newBookId);
/**
* Uri uri = Uri.parse("content://org.providers.prods/word");
* Uri resultUri = ContentUris.withAppendedId(uri,5);
* // 返回resultUri为:content://org.providers.prods/word/5
* Long id = ContentUris.parseId(resultUri);
* // 返回5
*/
break;
case CATEGORY_DIR:
case CATEGORY_ITEM:
long newCategoryId = db.insert("Category", null, values);
uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" +
newCategoryId);
break;
default:

break;
}
return uriReturn;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[]
selectionArgs) {
// 更新数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int updatedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
updatedRows = db.update("Book", values, selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);
updatedRows = db.update("Book", values, "id = ?", new String[]
{ bookId });
break;
case CATEGORY_DIR:
updatedRows = db.update("Category", values, selection,
selectionArgs);

break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
updatedRows = db.update("Category", values, "id = ?", new String[]
{ categoryId });
break;
default:
break;
}
return updatedRows;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据
SQLiteDatabase db = dbHelper.getWritableDatabase();
int deletedRows = 0;
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
deletedRows = db.delete("Book", selection, selectionArgs);
break;
case BOOK_ITEM:
String bookId = uri.getPathSegments().get(1);

deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
break;
case CATEGORY_DIR:
deletedRows = db.delete("Category", selection, selectionArgs);
break;
case CATEGORY_ITEM:
String categoryId = uri.getPathSegments().get(1);
deletedRows = db.delete("Category", "id = ?", new String[]
{ categoryId });
break;
default:
break;
}
return deletedRows;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case BOOK_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book";
case BOOK_ITEM:

return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book";
case CATEGORY_DIR:
return "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category";
case CATEGORY_ITEM:
return "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category";
}
return null;
}
}

其中还需要在AndroidManifest.xml中注册provider

1
2
3
4
5
6
<provider
android:name=".DatabaseProvider"
android:authorities="com.example.databasetest.provider"
android:enabled="true"
android:exported="true">
</provider>
  • android:name 属性指定了DatabaseProvider的类名
  • android:authorities 属性指定了DatabaseProviderauthority
  • enabledexported 属性则是根据我们刚才勾选的状态自动生成的,这里表示允许DatabaseProvider被其他应用程序进行访问;Enabled 属性表示是否启用

另写一程序来调用该数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MainActivity extends AppCompatActivity {
private String newId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button addData = (Button) findViewById(R.id.add_data);
addData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 添加数据
Uri uri = Uri.parse("content://com.example.databasetest.provider/book");
ContentValues values = new ContentValues();
values.put("name", "A Clash of Kings");
values.put("author", "George Martin");
values.put("pages", 1040);
values.put("price", 22.85);
Uri newUri = getContentResolver().insert(uri, values);

newId = newUri.getPathSegments().get(1);
}
});
Button queryData = (Button) findViewById(R.id.query_data);
queryData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 查询数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/book");
Cursor cursor = getContentResolver().query(uri, null, null, null,
null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor. getColumnIndex
("name"));
String author = cursor.getString(cursor. getColumnIndex
("author"));
int pages = cursor.getInt(cursor.getColumnIndex ("pages"));
double price = cursor.getDouble(cursor. getColumnIndex
("price"));
Log.d("MainActivity", "book name is " + name);
Log.d("MainActivity", "book author is " + author);

Log.d("MainActivity", "book pages is " + pages);
Log.d("MainActivity", "book price is " + price);
}
cursor.close();
}
}
});
Button updateData = (Button) findViewById(R.id.update_data);
updateData.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 更新数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
ContentValues values = new ContentValues();
values.put("name", "A Storm of Swords");
values.put("pages", 1216);
values.put("price", 24.05);
getContentResolver().update(uri, values, null, null);
}
});
Button deleteData = (Button) findViewById(R.id.delete_data);
deleteData.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View v) {
// 删除数据
Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId);
getContentResolver().delete(uri, null, null);
}
});
}
}

使用网络技术

使用Http访问网络

使用HttpURLConnection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// new一个Url对象
URL url = new URL("http://www.baidu.com");
// new HttpURLConnection 新建连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// 可设置请求方式 GET/POST
connection.setRequestMethod("GET");
// 设置连接超时毫秒数
connection.setConnectTimeout(8000);
// 设置读取超时毫秒数
connection.setReadTimeout(8000);

// 如果为POST,获取输出流
OutputStream os = connection.getOutputStream();
// 传入post参数
os.write("username=张三&age=20".getBytes());

// 获得输入流
InputStream in = connection.getInputStream();

// 进行字节流读取相关操作

// 关闭流
in.close();
os.close();
// 关闭连接
connection.disconnect();

使用OkHttp

导入依赖

编辑app/build.gradle,添加依赖

1
2
3
dependencies {
compile 'com.squareup.okhttp3:okhttp:3.10.0'
}
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建一个OkHttpClient的实例
OkHttpClient client = new OkHttpClient();
// 创建一个(空)Request 对象:
Request request = new Request.Builder().build();
// 或者在Builder中设置url
Request request = new Request.Builder()
.url("http://www.baidu.com")
.build();

// 之后调用OkHttpClient的newCall() 方法来创建一个Call 对象,并调用它的execute() 方法来发送请求并获取服务器返回的数据
Response response = client.newCall(request).execute();

// 得到Response返回的具体内容
String responseData = response.body().string();

// 如果为POST 我们需要先构建出一个Request Body对象来存放待提交的参数
RequestBody requestBody = new FormBody.Builder()
.add("username", "admin")
.add("password", "123456")
.build();
// Request.Builder中调用一下post() 方法,并将RequestBody 对象传入
Request request = new Request.Builder()
.url("http://www.baidu.com")
.post(requestBody)
.build();
// 调用execute() 方法来发送请求并获取服务器返回的数据即可。
get同步请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 创建OkHttpClient对象
OkHttpClient client = new OkHttpClient.Builder()
.cache() // 设置缓存目录(有默认)
.connectTimeout() // 设置连接超时(有默认)
.cookieJar() // cookie的读取,保存(需自己实现)
.build();
// 创建Request对象
Request request = new Request.Builder()
.url(address)
.build();
// 发送请求,获取响应
Call call = client.newCall(request);
// 发送同步请求,需手动创建子线程
new Thread(){
public void run(){
Response response = call.execute();//等待服务端进行响应(等待过程中会阻塞)
// 判断响应状态是否正常
if(response.isSuccessful()){
// 获取服务端响应的内容(文本)
String str = response.body().string();
}
}
}
get异步请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//发送异步方式的get请求
private void getAsync(){
//2.创建请求对象
Request request = new Request.Builder()
.url(Constant.BASE_URL +
"LoginServlet?username=张三&password=123")
.build();
//3.发送请求,获取响应
Call call = okHttpClient.newCall(request);
//异步请求,不需要手动创建子线程
call.enqueue(new Callback(){
@Override
public void onFailure(Call call, IOException e) {
//请求失败时执行
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//请求成功时执行
Log.e("onResponse",Thread.currentThread().getName());
Log.e("异步get请求的结果",response.body().string());
//执行在子线程中,不能直接修改用户界面
//可以借助Handler实现

//在实现文件下载功能时,如何获取网络文件的数据
// InputStream is = response.body().byteStream();
// FileOutputStream fos = new FileOutputStream(
// getExternalFilesDir(null).getAbsolutePath() + "/a.jpg");
}
});
Log.e("okhttp","异步请求已发送");
}

异步post:发送文本

MIME 参考手册 (w3school.com.cn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//异步方式的post请求,发送文本数据
private void postAsync(){
//2.创建请求体对象
RequestBody requestBody = RequestBody.create(
MediaType.parse("text/plain;charset=utf-8"), // 根据上传类型选择相应MIME
"username=张三&password=123");
//3.创建请求对象
Request request = new Request.Builder()
.url(Constant.BASE_URL + "LoginServlet")
.post(requestBody)//设置请求方式为POST
.build();
//4.发送请求,获取响应
Call call = okHttpClient.newCall(request);
//异步请求,不需要手动创建子线程
call.enqueue(new Callback(){
@Override
public void onFailure(Call call, IOException e) {
//请求失败时执行
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//请求成功时执行
Log.e("onResponse",Thread.currentThread().getName());
Log.e("异步get请求的结果",response.body().string());
//执行在子线程中,不能直接修改用户界面
//可以借助Handler实现
}
});
}
异步post:发送表单
1
2
3
4
5
6
7
8
9
10
11
//2.创建请求体对象
FormBody formBody = new FormBody.Builder()
.add("username","张三")
.add("password","123")
.build();

//3.创建请求对象
Request request = new Request.Builder()
.url(Constant.BASE_URL + "LoginServlet")
.post(formBody)//设置请求方式为POST
.build();
post本地选择文件上传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//文件上传
private void uploadFile(){
Intent intent = new Intent();
intent.setAction(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, 1);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1 && resultCode == RESULT_OK){
ContentResolver contentResolver = getContentResolver();
Cursor cursor = contentResolver.query(data.getData(),
null,null,null,null);
if (cursor.moveToFirst()){
String imagePath = cursor.getString(cursor.getColumnIndex("_data"));
//创建请求体对象
File file = new File(imagePath);
RequestBody requestBody = RequestBody.create(
MediaType.parse("application/octet-stream"),
file);
//创建请求对象
Request request = new Request.Builder()
.url(Constant.BASE_URL + "UploadServlet")
.post(requestBody)
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
Log.e("上传文件的结果",response.body().string());
}
});
}
}
}
post下载远端
1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onResponse(Call call, Response r) throws IOException{
InputStream is = response.body().byteStream();
File file = new File(Environment.getExternalStorageDirectory(),
"test.txt");
FileOutputStream fos = new FileOutputStream(file);
int len = 0;
byte[] buf = new byte[128];
while ((len = is.read(buf)) != -1){
fos.write(buf, 0, len);
}
}

解析XML格式数据

Pull解析方式

解析的xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<apps>
<app>
<id>1</id>
<name>Google Maps</name>
<version>1.0</version>
</app>
<app>

<id>2</id>
<name>Chrome</name>
<version>2.1</version>
</app>
<app>
<id>3</id>
<name>Google Play</name>
<version>2.3</version>
</app>
</apps>

java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
private void parseXMLWithPull(String xmlData) {
try {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
// 创建xmlPull解析器
// XmlPullParser parser = Xml.newPullParser();
XmlPullParser xmlPullParser = factory.newPullParser();
// 初始化解析器
xmlPullParser.setInput(new StringReader(xmlData));
// 当前读取的事件类型
int eventType = xmlPullParser.getEventType();
String id = "";
String name = "";
String version = "";
// 如果没有读取到文档的末尾
while (eventType != XmlPullParser.END_DOCUMENT) {
// 获得当前节点的名称
String nodeName = xmlPullParser.getName();
switch (eventType) {
// 开始标签
case XmlPullParser.START_TAG: {
if ("id".equals(nodeName)) {
id = xmlPullParser.nextText();
} else if ("name".equals(nodeName)) {
name = xmlPullParser.nextText();
} else if ("version".equals(nodeName)) {
version = xmlPullParser.nextText();
}
break;
}
// 结束标签
case XmlPullParser.END_TAG: {
if ("app".equals(nodeName)) {
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
break;
}
default:
break;
}
eventType = xmlPullParser.next();

}
} catch (Exception e) {
e.printStackTrace();
}
}
  1. 首先要获取到一个XmlPullParserFactory 的实例
  2. 借助这个实例得到XmlPullParser 对象
  3. 然后调用XmlPullParser 的setInput() 方法将服务器返回的XML数据设置进去就可以开始解析了。
  4. 解析的过程也非常简单,通过getEventType() 可以得到当前的解析事件
  5. 然后在一个while循环中不断地进行解析如果当前的解析事件不等于XmlPullParser.END_DOCUMENT,说明解析工作还没完成,调用next() 方法后可以获取下一个解析事件。
  6. 在while 循环中,我们通过getName() 方法得到当前节点的名字,如果发现节点名等于id、name或version,就调用nextText() 方法来获取节点内具体的内容,每当解析完一个app节点后就将获取到的内容打印出来。

SAX解析方式

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyHandler extends DefaultHandler {
@Override
public void startDocument() throws SAXException {
// startDocument() 方法会在开始XML解析的时候调用
}
@Override
public void startElement(String uri, String localName, String qName, Attributes
attributes) throws SAXException {
// startElement() 方法会在开始解析某个节点的时候调用
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// characters() 方法会在获取节点中内容的时候调用
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
// endElement() 方法会在完成解析某个节点的时候调用
}

@Override
public void endDocument() throws SAXException {
// endDocument() 方法会在完成整个XML解析的时候调用
}
}

解析上述XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class ContentHandler extends DefaultHandler {
private String nodeName;
private StringBuilder id;
private StringBuilder name;
private StringBuilder version;
@Override
public void startDocument() throws SAXException {
id = new StringBuilder();
name = new StringBuilder();

version = new StringBuilder();
}
@Override
public void startElement(String uri, String localName, String qName, Attributes
attributes) throws SAXException {
// 记录当前节点名
nodeName = localName;
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
// 根据当前的节点名判断将内容添加到哪一个StringBuilder对象中
if ("id".equals(nodeName)) {
id.append(ch, start, length);
} else if ("name".equals(nodeName)) {
name.append(ch, start, length);
} else if ("version".equals(nodeName)) {
version.append(ch, start, length);
}
}
@Override

public void endElement(String uri, String localName, String qName) throws
SAXException {
if ("app".equals(localName)) {
Log.d("ContentHandler", "id is " + id.toString().trim());
Log.d("ContentHandler", "name is " + name.toString().trim());
Log.d("ContentHandler", "version is " + version.toString().trim());
// 最后要将StringBuilder清空掉
id.setLength(0);
name.setLength(0);
version.setLength(0);
}
}
@Override
public void endDocument() throws SAXException {
super.endDocument();
}
}

调用

1
2
3
4
5
6
7
8
9
10
11
12
13
private void parseXMLWithSAX(String xmlData) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
XMLReader xmlReader = factory.newSAXParser().getXMLReader();
ContentHandler handler = new ContentHandler();
// 将ContentHandler的实例设置到XMLReader中
xmlReader.setContentHandler(handler);
// 开始执行解析
xmlReader.parse(new InputSource(new StringReader(xmlData)));
} catch (Exception e) {
e.printStackTrace();
}
}

解析JSON格式数据

测试Json数据

1
2
3
[{"id":"5","version":"5.5","name":"Clash of Clans"},
{"id":"6","version":"7.0","name":"Boom Beach"},
{"id":"7","version":"3.5","name":"Clash Royale"}]

使用JSONObject

转成Json字符串
1
2
3
4
5
6
7
8
// JSONObject转json字符串
JSONObject jsonObject = new JSONObject();
jsonObject.put(String name,Object value);
jsonObject.put(String name,Object value);

String jsonStr = jsonObject.toString();

// 若是数组,写个循环生成JSONObject,然后用JSONArray对象的put方法
读取JSON字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void parseJSONWithJSONObject(String jsonData) {
try {
// 读入JSON数组字符串 新建JSONArray对象
JSONArray jsonArray = new JSONArray(jsonData);
for (int i = 0; i < jsonArray.length(); i++) {
// 得到json对象
JSONObject jsonObject = jsonArray.getJSONObject(i);
// 获得对应数据
String id = jsonObject.getString("id");
String name = jsonObject.getString("name");
String version = jsonObject.getString("version");
Log.d("MainActivity", "id is " + id);
Log.d("MainActivity", "name is " + name);
Log.d("MainActivity", "version is " + version);
}
} catch (Exception e) {
e.printStackTrace();
}
}

使用Gson

添加依赖

编辑app/build.gradle文件,在dependencies闭包中添加

1
compile 'com.google.code.gson:gson:2.8.5'
使用
Builder设置Gson属性
1
2
3
4
5
Gson gson = new GsonBuilder()
.serializeNulls() // 允许导出null
.setPrettyPrinting() // 格式化s
.setDateFormat("yyyy-MM-dd") // 设置日期输出格式
.create(); // 生成配置好的Gson对象
对象转json字符串
1
2
Gson gson = new Gson();
String jsonStr = gson.toJson(Object);
字符串直接解析对象
1
2
3
4
jsonData = """{"name":"Tom","age":20}"""
////////////////
Gson gson = new Gson();
Person person = gson.fromJson(jsonData, Person.class);
Json字符串为对象数组
1
List<Person> people = gson.fromJson(jsonData, new TypeToken<List<Person>>() {}.getType());

网络编程实践

定义一个Http工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class HttpUtil {
// 发送Http GET请求
public static String sendHttpRequest(String address) {
HttpURLConnection connection = null;
try {
URL url = new URL(address);

connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
return response.toString();
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
} finally {
if (connection != null) {
connection.disconnect();
}
}
}


/**
* 封装好的post请求
*
* @param uri url
* @param parameters 请求参数
* @return post请求
*/
public static String sendPost(String uri, Map<String, String> parameters) {
StringBuffer postUrlSB = new StringBuffer(serverUrl + uri);
if (parameters != null) {
postUrlSB.append("?");
Set<Map.Entry<String, String>> entries = parameters.entrySet();
int count = 1;
int size = parameters.size();
for (Map.Entry<String, String> entry : entries) {
String key = entry.getKey();
String value = entry.getValue();
if (count++ < size) {
postUrlSB.append(key).append("=").append(value).append("&");
} else {
postUrlSB.append(key).append("=").append(value);
}
}
}
String postUrl = postUrlSB.toString();
URL url = null;
HttpURLConnection connection = null;
String meg = null;
try {
url = new URL(postUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-type", "text/plain;charset=UTF-8");
InputStream is = connection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
meg = br.readLine();
br.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
}
return meg;
}

}

开启子线程来接收返回值

Java回调机制

1. 定义一个HttpCallbackListener接口
1
2
3
4
5
public interface HttpCallbackListener {
void onFinish(String response);

void onError(Exception e);
}
  • onFinish() 方法表示当服务器成功响应我们请求的时候调用:onFinish() 方法中的参数代表着服务器返回的数据。
  • onError() 表示当进行网络操作出现错误的时候调用:onError() 方法中的参数记录着错误的详细信息。
2.修改原HttpUtil类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class HttpUtil {
public static void sendHttpRequest(final String address, final
HttpCallbackListener listener) {
new Thread(new Runnable() {
@Override

public void run() {
HttpURLConnection connection = null;
try {
URL url = new URL(address);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(8000);
connection.setReadTimeout(8000);
connection.setDoInput(true);
connection.setDoOutput(true);
InputStream in = connection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader
(in));
StringBuilder response = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
response.append(line);
}
if (listener != null) {
// 回调onFinish()方法
listener.onFinish(response.toString());
}
} catch (Exception e) {

if (listener != null) {
// 回调onError()方法
listener.onError(e);
}
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}).start();
}
}

我们首先给sendHttpRequest() 方法添加了一个HttpCallbackListener 参数

并在方法的内部开启了一个子线程,然后在子线程里去执行具体的网络操作。

子线程中是无法通过return语句来返回数据的,因此这里我们将服务器响应的数据传入了HttpCallbackListener的onFinish() 方法中,如果出现了异常就将异常原因传入到onError() 方法中。
现在sendHttpRequest() 方法接收两个参数了,因此我们在调用它的时候还需要将HttpCallbackListener的实例传入

1
2
3
4
5
6
7
8
9
10
HttpUtil.sendHttpRequest(address, new HttpCallbackListener() {
@Override
public void onFinish(String response) {
// 在这里根据返回内容执行具体的逻辑
}
@Override
public void onError(Exception e) {
// 在这里对异常情况进行处理
}
});

使用OkHttp

使用

1
2
3
4
5
6
7
8
9
10
public class HttpUtil {
...
public static void sendOkHttpRequest(String address, okhttp3.Callback callback) {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(address)
.build();
client.newCall(request).enqueue(callback);
}
}

okhttp3.Callback :是OkHttp库中自带的一个回调接口

1
2
3
4
5
6
7
8
9
10
11
HttpUtil.sendOkHttpRequest("http://www.baidu.com", new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// 得到服务器返回的具体内容
String responseData = response.body().string();
}
@Override
public void onFailure(Call call, IOException e) {
// 在这里对异常情况进行处理
}
});

Service服务

服务(Service)是Android中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还要求长期运行的任务

Android多线程

  1. 继承Thread类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyThread extends Thread {
    @Override

    public void run() {
    // 处理具体的逻辑
    }
    }


    // 调用
    new MyThread().start();
  2. 实现Runnable接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyThread implements Runnable {

    @Override
    public void run() {
    // 处理具体的逻辑
    }
    }

    // 调用
    MyThread myThread = new MyThread();
    new Thread(myThread).start();

    或者匿名内部类

    1
    2
    3
    4
    5
    6
    new Thread(new Runnable() {
    @Override
    public void run() {
    // 处理具体的逻辑
    }
    }).start();

异步消息更新UI

定义一个Handler对象,然后实现handleMessage()方法来实现消息接收后更新UI;同时定义Message对象,通过handler.sendMessage()来将消息发soon给给handler进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final int UPDATE_TEXT = 1;
private TextView text;
private Handler handler = new Handler() {
public void handleMessage(Message msg) {

switch (msg.what) {
case UPDATE_TEXT:
// 在这里可以进行UI操作
text.setText("Nice to meet you");
break;
default:
break;
}
}
};
...
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.change_text:
new Thread(new Runnable() {
@Override
public void run() {
Message message = new Message();
message.what = UPDATE_TEXT;

handler.sendMessage(message); // 将Message对象发送出去
}
}).start();
break;
default:
break;
}
}
}
异步消息处理机制
  1. Message
    Message是在线程之间传递的消息,它可以在内部携带少量的信息,用于在不同线程之间交换数据。上一小节中我们使用到了Message的what字段,除此之外还可以使用arg1 和arg2字段来携带一些整型数据,使用obj 字段携带一个Object 对象。
  2. Handler
    Handler顾名思义也就是处理者的意思,它主要是用于发送和处理消息的。发送消息一般是使用Handler的sendMessage() 方法,而发出的消息经过一系列地辗转处理后,最终会传递到Handler的handleMessage() 方法中。
  3. MessageQueue
    MessageQueue是消息队列的意思,它主要用于存放所有通过Handler发送的消息。这部分消息会一直存在于消息队列中,等待被处理。每个线程中只会有一个MessageQueue对象。
  4. Looper
    Looper是每个线程中的MessageQueue的管家,调用Looper的loop() 方法后,就会进入到一个无限循环当中,然后每当发现MessageQueue中存在一条消息,就会将它取出,并传递到Handler的handleMessage() 方法中。每个线程中也只会有一个Looper 对象。

使用AsyncTask类

AsyncTask是一个抽象类,要创建一个子类去继承它。

在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下。

  • Params 。在执行AsyncTask时需要传入的参数,可用于在后台任务中使用。
  • Progress 。后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。
  • Result 。当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型

重写方法

  1. onPreExecute()
    这个方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

  2. doInBackground(Params...)
    这个方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return 语句来将任务的执行结果返回,如果AsyncTask的第三个泛型参数指定的是Void ,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress (Progress...) 方法来完成。

  3. onProgressUpdate(Progress...)
    当在后台任务中调用了publishProgress(Progress...) 方法后,onProgressUpdate (Progress...) 方法就会很快被调用,该方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新。

  4. onPostExecute(Result)
    当后台任务执行完毕并通过return 语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
@Override
protected void onPreExecute() {
progressDialog.show(); // 显示进度对话框
}

@Override
protected Boolean doInBackground(Void... params) {
try {
while (true) {
int downloadPercent = doDownload(); // 这是一个虚构的方法
publishProgress(downloadPercent);
if (downloadPercent >= 100) {
break;
}
}
} catch (Exception e) {
return false;
}
return true;
}
@Override
protected void onProgressUpdate(Integer... values) {
// 在这里更新下载进度
progressDialog.setMessage("Downloaded " + values[0] + "%");
}

@Override
protected void onPostExecute(Boolean result) {
progressDialog.dismiss(); // 关闭进度对话框
// 在这里提示下载结果
if (result) {
Toast.makeText(context, "Download succeeded", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context, " Download failed", Toast.LENGTH_SHORT).show();
}
}
}

Service服务

定义一个服务

右击新建New→Service→Service;将服务命名为MyService,Exported 属性表示是否允许除了当前程序之外的其他程序访问这个服务,Enabled 属性表示是否启用这个服务。将两个属性都勾中,点击Finish完成创建。

AndroidManifest.xml的application节点内已自动生成Service的声明

1
2
3
4
5
6
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">

</service>

可添加android:process属性来指定服务创建新进程,示例值::myservice;则新进程名则为[packagename]:myservice

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class MyService extends Service {

public MyService() {
}

@Override
public IBinder onBind(Intent intent) {
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("MyService", "onStartCommand executed");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyService", "onDestroy executed");
}

}
  • onCreate() 方法会在服务创建的时候调用
  • onStartCommand() 方法会在每次服务启动的时候调用
  • onDestroy() 方法会在服务销毁的时候调用

启动和停止服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
Intent startIntent = new Intent(this, MyService.class);
startService(startIntent); // 启动服务
break;
case R.id.stop_service:
Intent stopIntent = new Intent(this, MyService.class);
stopService(stopIntent); // 停止服务
break;

default:
break;
}
}

服务自行运行完毕停止:在服务类中根据需要调用stopSelf()方法

活动和服务绑定

创建一个专门的Binder 对象来对下载功能进行管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyService extends Service {
private DownloadBinder mBinder = new DownloadBinder();
class DownloadBinder extends Binder {

public void startDownload() {
Log.d("MyService", "startDownload executed");
}
public int getProgress() {
Log.d("MyService", "getProgress executed");
return 0;
}
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
...
}

当一个活动和服务绑定了之后,就可以调用该服务里的Binder 提供的方法了

活动创建了一个ServiceConnection 的匿名类,在里面重写了onServiceConnected()方法和onServiceDisconnected() 方法,这两个方法分别会在活动与服务成功绑定以及活动与服务的连接断开的时候调用

构建出了一个Intent 对象,然后调用bindService() 方法将MainActivity和MyService进行绑定。

bindService() 方法接收3个参数:

  • 第一个参数就是刚刚构建出的Intent 对象
  • 第二个参数是前面创建出的ServiceConnection的实例
  • 第三个参数则是一个标志位,这里传入BIND_AUTO_CREATE 表示在活动和服务进行绑定后自动创建服务。这会使得MyService中的onCreate() 方法得到执行,但onStartCommand() 方法不会执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MyService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
// 非正常情况下服务被终止执行
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 绑定服务成功以后执行
downloadBinder = (MyService.DownloadBinder) service;
downloadBinder.startDownload();
downloadBinder.getProgress();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...

Button bindService = (Button) findViewById(R.id.bind_service);
Button unbindService = (Button) findViewById(R.id.unbind_service);
bindService.setOnClickListener(this);
unbindService.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
...
case R.id.bind_service:
Intent bindIntent = new Intent(this, MyService.class);
bindService(bindIntent, connection, BIND_AUTO_CREATE); // 绑定服务
break;
case R.id.unbind_service:
unbindService(connection); // 解绑服务
break;
default:
break;
}
}
}

服务Service的生命周期

img

一旦在项目的任何位置调用了Context的startService() 方法,相应的服务就会启动起来,并回调onStartCommand() 方法。

如果这个服务之前还没有创建过,onCreate() 方法会先于onStartCommand() 方法执行。服务启动了之后会一直保持运行状态,直到stopService()或stopSelf() 方法被调用。

注意,虽然每调用一次startService() 方法,onStartCommand() 就会执行一次,但实际上每个服务都只会存在一个实例。所以不管你调用了多少次startService()方法,只需调用一次stopService() 或stopSelf()方法,服务就会停止下来了。

另外,还可以调用Context的bindService() 来获取一个服务的持久连接,这时就会回调服务中的onBind() 方法。

类似地,如果这个服务之前还没有创建过,onCreate() 方法会先于onBind() 方法执行。之后,调用方可以获取到onBind() 方法里返回的IBinder 对象的实例,这样就能自由地和服务进行通信了。只要调用方和服务之间的连接没有断开,服务就会一直保持运行状态。
当调用了startService() 方法后,又去调用stopService() 方法,这时服务中的onDestroy()方法就会执行,表示服务已经销毁了。类似地,当调用了bindService() 方法后,又去调用unbindService() 方法,onDestroy() 方法也会执行,这两种情况都很好理解。

但是需要注意,我们是完全有可能对一个服务既调用了startService() 方法,又调用了bindService() 方法的,这种情况下该如何才能让服务销毁掉呢?根据Android系统的机制,一个服务只要被启动或者被绑定了之后,就会一直处于运行状态,必须要让以上两种条件同时不满足,服务才能被销毁。所以,这种情况下要同时调用stopService()和unbindService() 方法,onDestroy() 方法才会执行。

服务的更多技巧

使用前台服务

只是创建通知后,调用startForeground()对象来显示前台服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyService extends Service {

...
@Override
public void onCreate() {
super.onCreate();
Log.d("MyService", "onCreate executed");
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
Notification notification = new NotificationCompat.Builder(this)
.setContentTitle("This is content title")
.setContentText("This is content text")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),
R.mipmap.ic_launcher))
.setContentIntent(pi)
.build();
startForeground(1, notification);
}
...
}

使用IntentService

应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyService extends Service {
...

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 处理具体的逻辑
stopSelf(); // 在子线程中添加该语句,可让服务执行完后自行停止
}
}).start();
return super.onStartCommand(intent, flags, startId);
}
}

或者简单的 继承IntentService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService"); // 调用父类的有参构造函数
}
@Override
protected void onHandleIntent(Intent intent) {
// 在当前方法中处理耗时逻辑
// 打印当前线程的id
Log.d("MyIntentService", "Thread id is " + Thread.currentThread(). getId());
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("MyIntentService", "onDestroy executed");
}
}

要在子类中去实现onHandleIntent() 这个抽象方法,在这个方法中可以去处理一些具体的逻辑,而且不用担心ANR的问题,因为这个方法已经是在子线程中运行的了。另外根据ntentService 的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了onDestroy() 方法,在这里也打印了一行日志,以证实服务是不是停止掉了

使用JobService

Android8.0(26)之后推荐使用JobService

重写方法变为onHandleWork()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyJobIntentService extends JobIntentService {
// JobIntentService会使用单独的线程来执行该方法的代码
@Override
protected void onHandleWork(Intent intent) {
// 该方法内可以执行任何耗时任务,比如下载文件等,此处只是让线程暂停20秒
long endTime = System.currentTimeMillis() + 20 * 1000;
while (System.currentTimeMillis() < endTime) {
synchronized (this) {
try {
wait(endTime - System.currentTimeMillis());
} catch (Exception e) {
}
}
}
// 耗时任务执行完成
}
}

AndroidManifest.xml中配置Service

1
2
3
<service android:name=".MyJobIntentService" android:permission="android.permission.BIND_JOB_SERVICE">
<!--android.permission.BIND_JOB_SERVICE这个权限一定要有,否则程序会崩溃-->
</service>

启动服务

1
2
3
4
5
6
7
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
int JOB_ID = 88;
Intent work = new Intent();
work.putExtra("key","value");
MyJobIntentService.enqueueWork(MainActivity.this,
MyJobIntentService.class,JOB_ID, work);
}

服务的最佳实践——完整版的下载示例

@TODO

基于位置的服务

百度地图API

申请API

前往百度开放服务平台 (baidu.com)注册开发者

来到控制台 | 百度地图开放平台 (baidu.com)创建应用

应用名称随意,应用类型选择【Android SDK】

获取开发版(debug) SHA1

前往系统的.android目录,默认位于~/.android

输入命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
E:\code\Android\.android>keytool -list -v -keystore debug.keystore
输入密钥库口令:
密钥库类型: jks
密钥库提供方: SUN

您的密钥库包含 1 个条目

别名: androiddebugkey
创建日期: 2021-2-27
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: C=US, O=Android, CN=Android Debug
发布者: C=US, O=Android, CN=Android Debug
序列号: 1
有效期为 Sat Feb 27 18:40:11 CST 2021 至 Mon Feb 20 18:40:11 CST 2051
证书指纹:
MD5: 84:9D:C6:94:D8:2F:C2:90:00:00:00:00:00:00:00:00
SHA1: D2:4C:7C:89:85:83:9A:68:1C:6F:FF:0F:00:00:00:00:00:00:00:00
SHA256: EF:5A:67:E6:15:68:64:05:AD:95:90:7F:0F:5C:34:2B:79:54:15:C0:85:94:1E:9B:00:00:00:00:00:00:00:00
签名算法名称: SHA1withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 1


*******************************************
*******************************************



Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore debug.keystore -destkeystore debug.keystore -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

获取发布板(release) SHA1

Android百度地图开发-第一篇:申请、搭建百度地图 - zhangmiao14 - 博客园 (cnblogs.com)创建相应的签名文件,并获取相应的SHA1

(测试,可以相同)

包名正常设置
点击提交
可复制key,或者[设置]修改上述配置

使用百度定位

官方开发文档:Android地图SDK | 百度地图API SDK (baidu.com)

下载&配置SDK
下载SDK

SDK下载 - 百度LBS开放平台 (baidu.com)

(根据需要)选择基础定位和基础地图,选择jar格式、标准开发包选择下载

配置SDK

解压下载的jar包,然后进入libs目录

选择其中的BaiduLBS_Android.jar文件放入项目中app/libs目录

1
2
3
4
5
6
// gradle中需配置

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
}

之后新建src/main/jniLibs目录,将 剩余所有 复制到此处

项目配置
AndroidManifest配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.lbstest">
<!-- 添加权限:start -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>

<!-- 可删去 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- 添加权限:end -->
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<!-- 添加key:start 需要修改value为申请到的key值 -->
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="开发者 key" />
<!-- 添加key:end -->
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</service>
</application>
</manifest>
代码

动态申请权限代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
public class MainActivity extends AppCompatActivity {
public LocationClient mLocationClient;

private TextView positionText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mLocationClient = new LocationClient(getApplicationContext());
// 注册一个定位监听器,当获得位置时,就会回调该监听器
mLocationClient.registerLocationListener(new MyLocationListener());
setContentView(R.layout.activity_main);
positionText = (TextView) findViewById(R.id.position_text_view);

//动态申请权限
// 申请权限较多,建立权限数组
List<String> permissionList = new ArrayList<>();
// 如果没有该权限,就将该权限添加到权限数组中
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.ACCESS_FINE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.READ_PHONE_STATE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.
permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED) {
permissionList.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
}
// 如果权限数组不为空,则证明有权限未申请
if (!permissionList.isEmpty()) {
// 将该申请的权限转换为String[]数组
String [] permissions = permissionList.toArray(new String[permissionList.
size()]);
// 进行权限的申请
ActivityCompat.requestPermissions(MainActivity.this, permissions, 1);
} else {

requestLocation();
}
}
private void requestLocation() {
mLocationClient.start();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
switch (requestCode) {
case 1:
if (grantResults.length > 0) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "必须同意所有权限才能使用本程序",
Toast.LENGTH_SHORT).show();
// 如果有权限未被同意,则退出程序
finish();
return;
}
}
//同意所有权限后,加载位置
requestLocation();
} else {
Toast.makeText(this, "发生未知错误", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
}
}

// 获得相应参数
public class MyLocationListener implements BDLocationListener {
@Override
public void onReceiveLocation(BDLocation location) {
runOnUiThread(new Runnable() {
@Override
public void run() {
StringBuilder currentPosition = new StringBuilder();
currentPosition.append("纬度:").append(location.getLatitude()).

append("\n");
currentPosition.append("经线:").append(location.getLongitude()).
append("\n");
currentPosition.append("定位方式:");
if (location.getLocType() == BDLocation.TypeGpsLocation) {
currentPosition.append("GPS");
} else if (location.getLocType() ==
BDLocation.TypeNetWorkLocation) {
currentPosition.append("网络");
}
positionText.setText(currentPosition);
}
});
}
@Override
public void onConnectHotSpotMessage(String s, int i) {
}
}
}
实时更新位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainActivity extends AppCompatActivity {
...
private void requestLocation() {
initLocation();
mLocationClient.start();
}
private void initLocation(){
LocationClientOption option = new LocationClientOption();
option.setScanSpan(5000);
mLocationClient.setLocOption(option);
}
@Override
protected void onDestroy() {
super.onDestroy();

mLocationClient.stop();
}
...
}

选择定位模式

上述方式为网络定位

切换到GPS定位。手机设置:位置信息模式→高精确度

定位模式进行指定,一共有3种模式可选:Hight_Accuracy、Battery_Saving和Device_Sensors。

Hight_Accuracy表示高精确度模式,会在GPS信号正常的情况下优先使用GPS定位,在无法接收GPS信号的时候使用网络定位。

Battery_Saving表示节电模式,只会使用网络进行定位。

Device_Sensors表示传感器模式,只会使用GPS进行定位。

其中,Hight_Accuracy是默认的模式。

1
2
3
4
5
6
7
private void initLocation(){
LocationClientOption option = new LocationClientOption();
option.setScanSpan(5000);
// 设置定位模式
option.setLocationMode(LocationClientOption.LocationMode.Device_Sensors);
mLocationClient.setLocOption(option);
}

显示位置相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 设置LocationClientOption的 需要获取当前位置详细的地址信息
option.setIsNeedAddress(true);

StringBuilder currentPosition = new StringBuilder();
currentPosition.append("纬度:").append(location.getLatitude()).
append("\n");
currentPosition.append("经线:").append(location.getLongitude()).
append("\n");
currentPosition.append("国家:").append(location.getCountry()).
append("\n");
currentPosition.append("省:").append(location.getProvince()).
append("\n");
currentPosition.append("市:").append(location.getCity()).
append("\n");
currentPosition.append("区:").append(location.getDistrict()).
append("\n");
currentPosition.append("街道:").append(location.getStreet()).
append("\n");
currentPosition.append("定位方式:");
if (location.getLocType() == BDLocation.TypeGpsLocation) {
currentPosition.append("GPS");
} else if (location.getLocType() ==
BDLocation.TypeNetWorkLocation) {
currentPosition.append("网络");
}
positionText.setText(currentPosition);

使用百度地图

添加地图控件

1
2
3
4
5
<com.baidu.mapapi.map.MapView
android:id="@+id/bmapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true" />

添加Application继承类

1
2
3
4
5
6
7
8
9
10
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化百度地图
SDKInitializer.initialize(this);
}
}
/*
* 同时使AndroidManifest中<application>节点的 name属性为该类

java代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MainActivity extends AppCompatActivity {
...
private MapView mapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化要在 setContentView 前
mLocationClient = new LocationClient(getApplicationContext());
mLocationClient.registerLocationListener(new MyLocationListener());

// 添加下面这个,就不用添加自定义Application类了
SDKInitializer.initialize(getApplication());

setContentView(R.layout.activity_main);

mapView = (MapView) findViewById(R.id.bmapView);
...
}
...
/*
* 重写相应方法,及时释放资源
*/
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();

mLocationClient.stop();
mapView.onDestroy();
}
...
}

定位到当前位置

先获得地图总控制器

1
BaiduMap baiduMap = mapView.getMap();
  1. 设置缩放级别

    1
    2
    3
    4
    // 设置缩放级别(3~21),值越大地图越精细
    MapStatusUpdate update = MapStatusUpdateFactory.zoomTo(12.5f);
    // 最后需要调用该方法 使update的设置生效
    baiduMap.animateMapStatus(update);
  2. 设置经纬度

    1
    2
    3
    LatLng ll = new LatLng(39.915, 116.404);
    MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
    baiduMap.animateMapStatus(update);

显示所在位置光标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class MainActivity extends AppCompatActivity {
...
private BaiduMap baiduMap;
private boolean isFirstLocate = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 初始化要在 setContentView 前
mLocationClient = new LocationClient(getApplicationContext());
mLocationClient.registerLocationListener(new MyLocationListener());

setContentView(R.layout.activity_main);

mapView = (MapView) findViewById(R.id.bmapView);
// 获得Map控制器
baiduMap = mapView.getMap();

// 启用我的位置显示
baiduMap.setMyLocationEnabled(true);
...
}
private void navigateTo(BDLocation location) {
/*
* 地图定位到当前位置
*/
// 判断是不是第一次定位
// 定位到当前位置,同时设置精度
if (isFirstLocate) {
LatLng ll = new LatLng(location.getLatitude(), location.getLongitude());
MapStatusUpdate update = MapStatusUpdateFactory.newLatLng(ll);
baiduMap.animateMapStatus(update);
update = MapStatusUpdateFactory.zoomTo(16f);
baiduMap.animateMapStatus(update);
isFirstLocate = false;
}
/*
* 地图上当前位置 显示我的图标
*/
// 封装设备当前所在位置
MyLocationData.Builder locationBuilder = new MyLocationData.Builder();
locationBuilder.latitude(location.getLatitude());
locationBuilder.longitude(location.getLongitude());
MyLocationData locationData = locationBuilder.build();
// 设置上“我的位置”
baiduMap.setMyLocationData(locationData);
}
public class MyLocationListener implements BDLocationListener {
@Override

public void onReceiveLocation(BDLocation location) {

if (location.getLocType() == BDLocation.TypeGpsLocation
|| location.getLocType() == BDLocation.TypeNetWorkLocation) {
// 初始化地图
navigateTo(location);
}
}
}

@Override
protected void onDestroy() {
super.onDestroy();
mLocationClient.stop();
mapView.onDestroy();
// 关闭我的位置显示,节约资源
baiduMap.setMyLocationEnabled(false);
}
}

最佳的UI体验——MaterialDesign实战

Toolbar

项目主题由AndroidManifest中的android:theme指定,默认继承的Theme.AppCompat.Light.DarkActionBar含有ActionBar

指定一个不带ActionBar的主题:Theme.AppCompat.NoActionBar(深色主题) 和Theme.AppCompat.Light.NoActionBar(浅色主题)这两种主题可选

而AppTheme的style中指定的item有以下几种

ThemeStyleColor

使用TooBar

1
2
3
4
5
6
7
8
9
10
11
12
<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.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>

使用xmlns:app 指定了一个新的命名空间。由于Material Design是在Android 5.0系统中才出现的,而很多的Material属性在5.0之前的系统中并不存在,那么为了能够兼容之前的老系统,我们就不能使用android:attribute 这样的写法了,而是应该使用app:attribute 。

继续进阶—应该掌握的高级技巧

全局获取Context技巧

定义一个MyApp类,继承Application

1
2
3
4
5
6
7
8
9
10
11
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
context = getApplicationContext();
}

public static Context getContext() {
return context;
}
}

指定name属性<application android:name="MyApplication" ...>

使用Intent传递对象

Serializable方式

直接让实体类实现Serializable接口

1
public class Person implements Serializable{

传递时

1
2
3
4
5
6
Person person = new Person();
person.setName("Tom");
person.setAge(20);
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("person_data", person);
startActivity(intent);

接收时

1
Person person = (Person) getIntent().getSerializableExtra("person_data");

Parcelable方式

实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Person implements Parcelable {
private String name;
private int age;
...
@Override
public int describeContents() {
// describeContents() 方法直接返回0就可以了
return 0;

}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); // 写出name
dest.writeInt(age); // 写出age
/**
* 而writeToParcel() 方法中我们需要调用Parcel的writeXxx() 方法,将Person 类中的字段一一写出。注意,字符串型数据就调用writeString()方法,整型数据就调用writeInt() 方法,以此类推。
**/
}
public static final Parcelable.Creator<Person> CREATOR = new Parcelable.
Creator<Person>() {
@Override
public Person createFromParcel(Parcel source) {
Person person = new Person();
person.name = source.readString(); // 读取name
person.age = source.readInt(); // 读取age
return person;
// 读取存入,并返回对象
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}

};
}

传递方法相同

读取时

1
Person person = (Person) getIntent().getParcelableExtra("person_data");

自己的日志打印工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class LogUtil {
public static final int VERBOSE = 1;
public static final int DEBUG = 2;
public static final int INFO = 3;
public static final int WARN = 4;
public static final int ERROR = 5;
public static final int NOTHING = 6;
public static int level = VERBOSE;
public static void v(String tag, String msg) {
if (level <= VERBOSE) {
Log.v(tag, msg);
}
}
public static void d(String tag, String msg) {
if (level <= DEBUG) {
Log.d(tag, msg);
}
}
public static void i(String tag, String msg) {
if (level <= INFO) {
Log.i(tag, msg);
}
}
public static void w(String tag, String msg) {
if (level <= WARN) {
Log.w(tag, msg);
}
}
public static void e(String tag, String msg) {

if (level <= ERROR) {
Log.e(tag, msg);
}
}
}

比如让level 等于VERBOSE 就可以把所有的日志都打印出来,让level 等于WARN 就可以只打印警告以上级别的日志,让level 等于NOTHING 就可以把所有日志都屏蔽掉。

创建定时任务

Alarm机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class LongRunningService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;

}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(new Runnable() {
@Override
public void run() {
// 在这里执行具体的逻辑操作
}
}).start();
// 获得Alarm管理
AlarmManager manager = (AlarmManager) getSystemService(ALARM_SERVICE);
int anHour = 60 * 60 * 1000; // 这是一小时的毫秒数
// 下次执行的时间
long triggerAtTime = SystemClock.elapsedRealtime() + anHour;
Intent i = new Intent(this, LongRunningService.class);
// 启动的意图
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
// 进行设置
manager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtTime, pi);
return super.onStartCommand(intent, flags, startId);
}
}

要求时间准确,使用setExact()代替set()

Doze模式:调用AlarmManager的setAndAllowWhileIdle() 或setExactAndAllowWhileIdle() 方法就能让定时任务即使在Doze模式下也能正常执行

Android权限

Manifest.permission | Android Developers (google.cn)

常见权限

1
2
3
4
5
6
7
8
9
10
11
<uses-permission android:name="android.permission.INTERNET" /><!--    网络权限-->
<!-- API28+还需在application节点中添加-9属性 -->

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><!-- 访问网络状态权限-->

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/><!-- 监听开机权限-->

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><!-- 读外部存储权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><!-- 写外部存储权限-->
<!-- 从Android10开始,需要在application节点中添加android:requestLegacyExternalStorage="true"属性-->

运行时权限

动态申请运行时权限代码示例

权限组名 权限名
CALENDAR READ_CALENDAR
WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS
WRITE_CONTACTS
GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE
CALL_PHONE
READ_CALL_LOG
WRITE_CALL_LOG
ADD_VOICEMAIL
USE_SIP
PROCESS_OUTGOING_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS
RECEIVE_SMS
READ_SMS
RECEIVE_WAP_PUSH
RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE
WRITE_EXTERNAL_STORAGE

Git学习

Git高级用法

分支的用法

  1. 查看所有分支

    1
    git branch
  2. 创建分支

    1
    git branch [name]
  3. 切换分支

    1
    git checkout [name]
  4. 合并branch_a分支到master分支

    1
    2
    3
    4
    git checkout master

    # 合并branch_a分支到当前分支
    git merge branch_a
  5. 删除分支

    1
    git branch -D [name]

与远程协作

  1. 将远程代码下载到本地

    1
    git clone [url]
  2. 将本地更改提交到远程

    1
    git push origin master
  3. 抓取远程到本地

    1
    git fetch origin master

    同步下来的代码存放到origin/master分支上

  4. 查看远程版本库修改了什么(配合3)

    查看后可以使用git merge合并分支到当前分支

    1
    git diff origin/master
  5. 拉取最新代码并合并到本地(3,4的操作)

    1
    git pull origin master

零散知识块

Fragment

Fragment使用

Acitivity静态添加Fragment:

布局中添加控件

1
2
3
4
5
<fragment android:name="<Fragment全类名>"
android:layout_width=""
android:layout_height=""
android:id="@+id/fragment_one"
/>

使用全类名直接指定相应fragment,activity就会自动嵌入该fragment

Activity动态添加和管理Fragment:

  1. 布局文件添加<FrameLayout>并设置ID属性

  2. activity中动态替换Fragment代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private void replaceFragment(Fragment fragment){
    // 得到Fragment管理实例
    FragmentManager fragmentManager = getSupportFragmentManager();
    // 开启一个事务
    FragmentTransaction transaction = fragmentManager.beginTransaction();
    // 进行替换Fragment,参数1为控件ID,参数二为fragment实例(new得到)
    transaction.replace(R.id.framelayout, fragment);

    // 根据需要;将fragment添加到返回栈,这样back返回的时候将是之前添加的fragment,直到所有fragment都弹出
    // transaction.addToBackStack(null);

    // 提交事务
    transaction.commit();
    }

Fragment和Activity进行通信

使用bundle通信

Activity给Fragment传递消息

1
2
3
4
5
6
7
8
// 新建一个Bundle对象
Bundle bundle = new Bundle();
// 放入相应数据键值对
bundle.putString("message","I'm from Activity");
// new一个fragment实例
MyFragment myFragment = new MyFragment();
// 将参数传递进去
myFragment.setArguments(bundle);

Fragment接收的时候

1
2
3
4
5
6
7
8
9
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
// 获取传递的信息
Bundle bundle = this.getArguments();
// 根据键获取传递信息中的某个值
String msg = bundle.getString("message");
Log.d(TAG,"From Acitvity: "+msg);
}

使用Java接口进行通信

为Fragment通信新建一个接口

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Fragment通信接口
*/
public interface IFragmentcallback {
/**
* 发送消息给Activity 方法
*/
void sendMsgToActivity(String msg);
/**
* 从Activity获取消息 方法
*/
String getMsgFromActivity(String msg);
}

Acitvity代码

1
2
3
4
5
6
7
8
9
10
11
myFragment.setCallback(new IFragmentcallback(){
@Override
void sendMsgToActivity(String msg){
Log.d(TAG,"From Fragment: "+msg);
}

@Override
String getMsgFromActivity(String msg){
return "Hi,I'm from Activity.";
}
})

Fragment代码

1
2
3
4
private IFragmentcallback callback;
public void setCallback(IFragmentcallback callback){
this.callback = callback;
}

其他方案

eventBus,LiveData…

Fragment生命周期

Fragment生命周期

1.打开界面

onCreate() -> onCreateView() ->onActivityCreated() ->onStart() ->onResume()

2.按下主屏键

onPause() ->onStop()

3.重新打开界面.

onStart() ->onResume()

4.按后退键

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

在fragment栈中被覆盖:onPause()- >onStop()- >onDestroyView()

fragment栈 重新显示:onCreateView() ->onActivityCreated() ->onStart() ->onResume()

ViewPager2与Fragment

AndroidProjectPractice/ViewPagerTest at main · Forgo7ten/AndroidProjectPractice (github.com)