Android四大组件

一 、四大组件官网指南

1.Activity

https://developer.android.google.cn/guide/components/activities/intro-activities?hl=zh-cn

https://developer.android.google.cn/guide/topics/ui/shortcuts/creating-shortcuts?hl=zh-cn

https://developer.android.google.cn/guide/fragments?hl=zh-cn

https://developer.android.google.cn/guide/components/intents-filters?hl=zh-cn

2.service 

https://developer.android.google.cn/guide/components/services?hl=zh-cn

3.broadcast 

https://developer.android.google.cn/guide/components/broadcasts?hl=zh_cn

4.ContentProvider 

https://developer.android.google.cn/guide/topics/providers/content-providers?hl=zh_cn

二、 Activity

1.1 Activity页面启

1.1.1 Activity的启动和结束

通过startActivity方法可以从当前页面跳到新页面,具体格式如“startActivity(new Intent(源页面.this, 目标页面.class));”。譬如以下代码便在重写后的点击方法onClick中执行页面跳转动作。

// 活动类直接实现点击监听器的接口View.OnClickListener
public class ActStartActivity extends AppCompatActivity implements 
View.OnClickListener {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_act_start);
       // setOnClickListener来自于View,故而允许直接给View对象注册点击监听器
       findViewById(R.id.btn_act_next).setOnClickListener(this);
 }
   @Override
   public void onClick(View v) { // 点击事件的处理方法
       if (v.getId() == R.id.btn_act_next) {
           // 从当前页面跳到指定的新页面
           //startActivity(new Intent(ActStartActivity.this, 
ActFinishActivity.class));
           startActivity(new Intent(this, ActFinishActivity.class)); 
    }
      } 
}

在Java代码中,调用finish方法即可关闭当前页面。比如要求点击箭头图标或完成按钮都返回上一页面,则需给箭头图标和完成按钮分别注册点击监听器,然后在onClick方法中调用finish方法。下面便是添加了finish方法的新页面代码例子:

// 活动类直接实现点击监听器的接口View.OnClickListener
public class ActFinishActivity extends AppCompatActivity implements 
View.OnClickListener {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       setContentView(R.layout.activity_act_finish);
       // 给箭头图标注册点击监听器,ImageView由View类派生而来
       findViewById(R.id.iv_back).setOnClickListener(this);
       // 给完成按钮注册点击监听器,Button也由View类派生而来
       findViewById(R.id.btn_finish).setOnClickListener(this);
 }
   @Override
   public void onClick(View v) { // 点击事件的处理方法
       if (v.getId() == R.id.iv_back || v.getId() == R.id.btn_finish) {
           finish(); // 结束当前的活动页面
    }
 } 
}

另外,所谓“打开页面”或“关闭页面”沿用了浏览网页的叫法,对于App而言,页面的真实名称是“活动”—Activity。打开某个页面其实是启动某个活动startActivity;关闭某个页面其实是结束某个活动finish。

1.1.2 Activity的生命周期

每次创建新的活动页面,自动生成的Java代码都给出了onCreate方法,该方法用于执行活动创建的相关操作,包括加载XML布局、设置文本视图的初始文字、注册按钮控件的点击监听,等等。onCreate方法所代表的创建动作,正是一个活动最开始的行为,除了onCreate,活动还有其他几种生命周期行为,它们对应的方法说明如下:

1.onCreate:创建活动。此时会把页面布局加载进内存,进入了初始状态。

2.onStart:开启活动。此时会把

 注意,如果一个Activity已经启动过,并且存在当前应用的Activity任务栈中,启动模式为singleTask, singleInstance或singleTop(此时已在任务栈顶端),那么在此启动或回到这个Activity的时候,不会创建新的实例,也就是不会执行onCreate方法,而是执行onNewIntent方法。

参考:

https://developer.android.google.cn/guide/components/activities/activity-lifecycle?hl=zh-cn

1.1.3 Activity的启动模式

Android系统给每个正在运行的App都分配了活动栈,栈里面容纳着已经创建且尚未销毁的活动信息。鉴于栈是一种先进后出、后进先出的数据结构,故而后面入栈的活动总是先出栈,假设 3 个活动的入栈顺序为:活动A→活动B→活动C,则它们的出栈顺序将变为:活动C→活动B→活动A,可见活动C结束之后会返回活动B,而不是返回活动A或者别的地方。假定某个App分配到的活动栈大小为 3 ,该App先后打开两个活动,此时活动栈的变动情况如图所示。

 然后按下返回键,依次结束已打开的两个活动,此时活动栈的变动情况如图所示。

注意,前述的出入栈情况仅是默认的标准模式,实际上Android允许在创建活动时指定该活动的启动模式,通过启动模式控制活动的出入栈行为。

App提供了两种办法用于设置活动页面的启动模式,其一是修改AndroidManifest.xml,在指定的activity节点添加属性android:launchMode,表示本活动以哪个启动模式运行。其二是在代码中调用Intent对象的setFlags方法,表明后续打开的活动页面采用该启动标志。

1.在配置文件中指定启动模式

打开AndroidManifest.xml,给activity节点添加属性android:launchMode,属性值填入standard表示采取标准模式,当然不添加属性的话默认就是标准模式。具体的activity节点配置内容示例如下:

<activity android:name=".JumpFirstActivity" android:launchMode="standard" />

  1. 默认启动模式 standard

Activity 的默认模式就是 standard。在该模式下,启动的 Activity 会依照启动顺序被依次压入 Task 栈中:

  1. 栈顶复用模式 singeTop

在该模式下,如果栈顶 Activity 为我们要新建的 Activity(目标Activity),那么就不会重复创建新的Activity。

应用场景:

适合开启渠道多、多应用开启调用的 Activity,通过这种设置可以避免已经创建过的 Activity 被重复创建,多数通过动态设置使用。例如微信支付宝支付界面。

  1. 栈内复用模式 singeTask

与 singleTop 模式相似,只不过 singleTop 模式是只是针对栈顶的元素,而 singleTask 模式下,如果task 栈内存在目标 Activity 实例,则将 task 内的对应 Activity 实例之上的所有 Activity 弹出栈,并将对应 Activity 置于栈顶,获得焦点。

应用场景:

程序主界面 :我们肯定不希望主界面被创建多次,而且在主界面退出的时候退出整个 App 是最好的效果。耗费系统资源的Activity :对于那些及其耗费系统资源的 Activity,我们可以考虑将其设为 singleTask模式,减少资源耗费。

  1. 全局唯一模式 singeInstance

在该模式下,我们会为目标 Activity 创建一个新的 Task 栈,将目标 Activity 放入新的 Task,并让目标Activity获得焦点。新的 Task 有且只有这一个 Activity 实例。 如果已经创建过目标 Activity 实例,则不会创建新的 Task,而是将以前创建过的 Activity 唤醒。

2.动态设置启动模式

在上述所有情况,都是我们在Manifest中通过 launchMode 属性设置的,这个被称为静态设置,动态设置是通过 Java 代码设置的。通过 Intent 动态设置 Activity启动模式intent.setFlags();如果同时有动态和静态设置,那么动态的优先级更高

  1. FLAG_ACTIVITY_NEW_TASK

此 Flag 跟 singleInstance 很相似,在给目标 Activity 设立此 Flag 后,会根据目标 Activity 的 affinity 进行匹配,如果已经存在与其affinity 相同的 task,则将目标 Activity 压入此 Task。反之没有的话,则新建一个 task,新建的 task 的 affinity 值与目标 Activity 相同,然后将目标 Activity 压入此栈。但它与 singleInstance 有不同的点,两点需要注意的地方:新的 Task 没有说只能存放一个目标 Activity,只是说决定是否新建一个 Task,而 singleInstance模式下新的 Task 只能放置一个目标 Activity。在同一应用下,如果 Activity 都是默认的 affinity,那么此 Flag 无效,而 singleInstance 默认情况也会创建新的 Task。

  1. FLAG_ACTIVITY_SINGLE_TOP

此 Flag 与静态设置中的 singleTop 效果相同

  1. FLAG_ACTIVITY_CLEAR_TOP

当设置此 Flag 时,目标 Activity 会检查 Task 中是否存在此实例,如果没有则添加压入栈。如果有,就将位于 Task 中的对应 Activity 其上的所有 Activity 弹出栈,此时有以下两种情况:

①如果同时设置 Flag_ACTIVITY_SINGLE_TOP ,则直接使用栈内的对应 Activity。

intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_SINGLE_TOP);

②没有设置,则将栈内的对应 Activity 销毁重新创建。

3.启动模式的实际应用

在两个活动之间交替跳转、登录成功后不再返回登录页面,分别介绍如下。

1 .在两个活动之间交替跳转假设活动A有个按钮,点击该按钮会跳到活动B;同时活动B也有个按钮,点击按钮会跳到活动A;从首页打开活动A之后,就点击按钮在活动A与活动B之间轮流跳转。

此时活动页面的跳转流程为:首页→活动A→活动B→活动A→活动B→活动A→活动B→…多次跳转之后想回到首页,正常的话返回流程是这样的:…→活动B→活动A→活动B→活动A→活动B→活动A→首页,可见要按下许多次返回键才能返回首页。

如果我们希望回来的时候应该按照这个流程:…→活动B→活动A→首页,或者按照这个流程:…→活动A→活动B→首页,总之已经返回了的页面,决不再返回第二次。

对于不允许重复返回的情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TOP,即使活动栈里面存在待跳转的活动实例,也会重新创建该活动的实例,并清除原实例上方的所有实例,保证栈中最多只有该活动的唯一实例,从而避免了无谓的重复返回。于是活动A内部的跳转代码就改成了下面这般:

JumpFirstActivity.java

// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, JumpSecondActivity.class);
// 当栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例 
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 设置启动标志 
startActivity(intent);  // 跳转到意图对象指定的活动页面
当然活动B内部的跳转代码也要设置同样的启动标志:

JumpSecondActivity.java

// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, JumpFirstActivity.class);
// 当栈中存在待跳转的活动实例时,则重新创建该活动的实例,并清除原实例上方的所有实例 
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 设置启动标志 
startActivity(intent);  // 跳转到意图指定的活动页面

这下两个活动的跳转代码都设置了FLAG_ACTIVITY_CLEAR_TOP,运行测试App发现多次跳转之后,每个活动仅会返回一次而已。

2 .登录成功后不再返回登录页面,很多App第一次打开都要求用户登录,登录成功再进入App首页,如果这时按下返回键,发现并没有回到上一个登录页面,而是直接退出App了。不光登录页面,登录之前的其他页面包括获取验证码、找回密码等页面都不应回去,每次登录成功之后,整个App就焕然一新仿佛忘记了有登录页面这回事。

对于回不去的登录页面情况,可以设置启动标志FLAG_ACTIVITY_CLEAR_TASK,该标志会清空当前活动栈里的所有实例。不过全部清空之后,意味着当前栈没法用了,必须另外找个活动栈才行,也就是同时设置启动标志FLAG_ACTIVITY_NEW_TASK,该标志用于开辟新任务的活动栈。于是离开登录页面的跳转代码变成下面这样:

// 创建一个意图对象,准备跳到指定的活动页面
Intent intent = new Intent(this, LoginSuccessActivity.class);
// 设置启动标志:跳转到新页面时,栈中的原有实例都被清空,同时开辟新任务的活动栈 
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK |
               Intent.FLAG_ACTIVITY_NEW_TASK); 
startActivity(intent);  // 跳转到意图指定的活动页面

运行测试App,登录成功进入首页之后,点击返回键果然没回到登录页面。

1.2 在Activity之间传递消息

1.2.1显式Intent和隐式Intent

Intent的中文名是意图,简单地说,就是传递消息。Intent是各个组件之间信息沟通的桥梁,既能在Activity之间沟通,又能在Activity与Service之间沟通,也能在Activity与Broadcast之间沟通。总而言之,Intent用于Android各组件之间的通信,它主要完成下列 3 部分工作:

( 1 )标明本次通信请求从哪里来、到哪里去、要怎么走。

( 2 )发起方携带本次通信需要的数据内容,接收方从收到的意图中解析数据。

( 3 )发起方若想判断接收方的处理结果,意图就要负责让接收方传回应答的数据内容。

Intent的组成:

指定意图对象的目标有两种方式,一种是显式Intent,另一种是隐式Intent。

1 .显式Intent,直接指定来源活动与目标活动,属于精确匹配在构建一个意图对象时,需要指定两个参数,第一个参数表示跳转的来源页面,即“来源Activity.this”;第二个参数表示待跳转的页面,即“目标Activity.class”。具体的意图构建方式有如下 3 种:

( 1 )在Intent的构造函数中指定,示例代码如下:

 Intent intent = new Intent(this, ActNextActivity.class);  // 创建一个目标确定的意图

( 2 )调用意图对象的setClass方法指定,示例代码如下:

Intent intent = new Intent();  // 创建一个新意图

intent.setClass(this, ActNextActivity.class); // 设置意图要跳转的目标活动

( 3 )调用意图对象的setComponent方法指定,示例代码如下:

Intent intent = new Intent();  // 创建一个新意图

// 创建包含目标活动在内的组件名称对象

ComponentName component = new ComponentName(this, ActNextActivity.class);

intent.setComponent(component);  // 设置意图携带的组件信息

2 .隐式Intent,没有明确指定要跳转的目标活动,只给出一个动作字符串让系统自动匹配,属于模糊匹配。通常App不希望向外部暴露活动名称,只给出一个事先定义好的标记串,隐式Intent便起到了标记过滤作用。这个动作名称标记串,可以是自己定义的动作,也可以是已有的系统动作。常见系统动作的取值说明。

动作名称既可以通过setAction方法指定,也可以通过构造函数Intent(String action)直接生成意图对象。当然,由于动作是模糊匹配,因此有时需要更详细的信息,Uri数据可通过构造函数Intent(String action, Uri uri)在生成对象时一起指定,也可通过setData方法指定;Category可通过addCategory方法指定,之所以用add而不用set方法,是因为一个意图允许设置多个Category,方便一起过滤。

下面是一个调用系统拨号程序的代码例子,其中就用到了Uri:

String phoneNo = "12345";

Intent intent = new Intent();  // 创建一个新意图

intent.setAction(Intent.ACTION_DIAL);  // 设置意图动作为准备拨号

Uri uri = Uri.parse("tel:" + phoneNo);  // 声明一个拨号的Uri

intent.setData(uri);  // 设置意图前往的路径

startActivity(intent);  // 启动意图通往的活动页面

隐式Intent配置方式譬如创建一个App模块,AndroidManifest.xml里的intent-filter就是配置文件中的过滤器。像最常见的首页活动MainAcitivity,它的activity节点下面便设置了action和category的过滤条件。其中android.intent.action.MAIN表示App的入口动作,而android.intent.category.LAUNCHER表示在桌面

上显示App图标,配置样例如下:

<activity android:name=".MainActivity">

   <intent-filter>

       <action android:name="android.intent.action.MAIN" />

       <category android:name="android.intent.category.LAUNCHER" />

   </intent-filter>

</activity>

1.2.2向下一个Activity发送数据

Intent对象的setData方法只指定到达目标的路径,并非本次通信所携带的参数信息,真正的参数信息存放在Extras中。Intent重载了很多种putExtra方法传递各种类型的参数,包括整型、双精度型、字符串等基本数据类型,甚至Serializable这样的序列化结构。只是调用putExtra方法显然不好管理,Android引入了Bundle。

Bundle内部用于存放消息的数据结构是Map映射,既可添加或删除元素,还可判断元素是否存在。开发者若要把Bundle数据全部打包好,只需调用一次意图对象的putExtras方法;若要把Bundle数据全部取出来,也只需调用一次意图对象的getExtras方法。Bundle对象操作各类型数据的读写方法说明见表 。

代码示例如下:

// 创建一个意图对象,准备跳到指定的活动页面

Intent intent = new Intent(this,ActReceiveActivity.class);

Bundle bundle = new Bundle();  // 创建一个新包裹

// 往包裹存入名为request_time的字符串

bundle.putString("request_time", DateUtil.getNowTime());

// 往包裹存入名为request_content的字符串

bundle.putString("request_content", tv_send.getText().toString());

intent.putExtras(bundle);  // 把快递包裹塞给意图

startActivity(intent);  // 跳转到意图指定的活动页面

然后在下一个活动中获取意图携带包裹数据的代码例子:

// 从布局文件中获取名为tv_receive的文本视图

TextView tv_receive = findViewById(R.id.tv_receive);

// 从上一个页面传来的意图中获取快递包裹

Bundle bundle = getIntent().getExtras();

// 从包裹中取出名为request_time的字符串

String request_time = bundle.getString("request_time");

// 从包裹中取出名为request_content的字符串

String request_content = bundle.getString("request_content");

String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s",

                           request_time, request_content);

tv_receive.setText(desc);  // 把请求消息的详情显示在文本视图上

1.2.3向上一个Activity返回数据

数据传递经常是相互的,上一个页面不但把请求数据发送到下一个页面,有时候还要处理下一个页面的应答数据,所谓应答发生在下一个页面返回到上一个页面之际。如果只把请求数据发送到下一个页面,上一个页面调用startActivity方法即可;如果还要处理下一个页面的应答数据,此时就得分多步处理,详细步骤说明如下:

步骤一,上一个页面打包好请求数据,调用startActivityForResult方法执行跳转动作,表示需要处理下一个页面的应答数据,该方法的第二个参数表示请求代码,它用于标识每个跳转的唯一性。跳转代码示

String request = "你吃饭了吗?来我家吃吧";

// 创建一个意图对象,准备跳到指定的活动页面

Intent intent = new Intent(this, ActResponseActivity.class);

Bundle bundle = new Bundle();  // 创建一个新包裹

// 往包裹存入名为request_time的字符串

bundle.putString("request_time", DateUtil.getNowTime());

// 往包裹存入名为request_content的字符串

bundle.putString("request_content", request);

intent.putExtras(bundle);  // 把快递包裹塞给意图

// 期望接收下个页面的返回数据。第二个参数为本次请求代码

startActivityForResult(intent, 0);

步骤二,下一个页面接收并解析请求数据,进行相应处理。接收代码示例如下:

// 从上一个页面传来的意图中获取快递包裹

Bundle bundle = getIntent().getExtras();

// 从包裹中取出名为request_time的字符串

String request_time = bundle.getString("request_time");

// 从包裹中取出名为request_content的字符串

String request_content = bundle.getString("request_content");

String desc = String.format("收到请求消息:\n请求时间为%s\n请求内容为%s",  request_time, request_content);

tv_request.setText(desc);  // 把请求消息的详情显示在文本视图上

步骤三,下一个页面在返回上一个页面时,打包应答数据并调用setResult方法返回数据包裹。setResult方法的第一个参数表示应答代码(成功还是失败),第二个参数为携带包裹的意图对象。返回代码示例如下:

String response = "我吃过了,还是你来我家吃";

Intent intent = new Intent();  // 创建一个新意图

Bundle bundle = new Bundle();  // 创建一个新包裹

// 往包裹存入名为response_time的字符串

bundle.putString("response_time", DateUtil.getNowTime());

// 往包裹存入名为response_content的字符串

bundle.putString("response_content", response);

intent.putExtras(bundle);  // 把快递包裹塞给意图

// 携带意图返回上一个页面。RESULT_OK表示处理成功

setResult(Activity.RESULT_OK, intent);

finish();  // 结束当前的活动页面

步骤四,上一个页面重写方法onActivityResult,该方法的输入参数包含请求代码和结果代码,其中请求代码用于判断这次返回对应哪个跳转,结果代码用于判断下一个页面是否处理成功。如果下一个页面处理成功,再对返回数据解包操作,处理返回数据的代码示例如下:

// 从下一个页面携带参数返回当前页面时触发。其中requestCode为请求代码,

// resultCode为结果代码,intent为下一个页面返回的意图对象

@Override

protected void onActivityResult(int requestCode, int resultCode, Intent intent)

{ // 接收返回数据

   super.onActivityResult(requestCode, resultCode, intent);

      // 意图非空,且请求代码为之前传的0,结果代码也为成功

   if (intent!=null && requestCode==0 && resultCode== Activity.RESULT_OK) {

       Bundle bundle = intent.getExtras(); // 从返回的意图中获取快递包裹

       // 从包裹中取出名叫response_time的字符串

       String response_time = bundle.getString("response_time");

       // 从包裹中取出名叫response_content的字符串

       String response_content = bundle.getString("response_content");

       String desc = String.format("收到返回消息:\n应答时间为:%s\n应答内容为:%s",

                                   response_time, response_content);

       tv_response.setText(desc); // 把返回消息的详情显示在文本视图上

 }

}

注意:startActivityForResult被标记过时的方法,官方建议使用registerForActivityResult

1.3 Activity附加信息

1.3.1资源文件配置字符串

res\values目录下面的strings.xml就用来配置字符串形式的参数,发现里面已经存在名为app_name的字符串参数,它配置的是当前模块的应用名称。现在可于app_name下方补充一行参数配置,参数名称叫作“weather_str”,参数值则为“晴天”,具体的配置内容如下所示:

<string name="weather_str">晴天</string>

打开活动页面的Java代码,调用getString方法即可根据“R.string.参数名称”获得指定参数的字符串值。获取代码示例如下:

// 显示字符串资源

private void showStringResource() {

   String value = getString(R.string.weather_str); // 从strings.xml获取名叫

weather_str的字符串值a

   tv_resource.setText("来自字符串资源:今天的天气是"+value); // 在文本视图上显示文字

}

上面的getString方法来自于Context类,由于页面所在的活动类AppCompatActivity追根溯源来自Context这个抽象类,因此凡是活动页面代码都能直接调用getString方法。

1.3.2元数据配置信息

尽管资源文件能够配置字符串参数,然而有时候为安全起见,某个参数要给某个活动专用,并不希望其他活动也能获取该参数,此时就不方便到处使用getString了。

Activity提供了元数据(Metadata)的概念,元数据是一种描述其他数据的数据,它相当于描述固定活动的参数信息。打开AndroidManifest.xml,在测试活动的activity节点内部添加meta-data标签,通过属性name指定元数据的名称,通过属性value指定元数据的值。仍以天气为例,添加meta-data标签之后的activity节点如下所示:

<activity android:name=".MetaDataActivity">

   <meta-data android:name="weather" android:value="晴天" />

</activity>

元数据的value属性既可直接填字符串,也可引用strings.xml已定义的字符串资源,引用格式形如“@string/字符串的资源名称”。下面便是采取引用方式的activity节点配置:

<activity android:name=".MetaDataActivity">

   <meta-data

              android:name="weather"

              android:value="@string/weather_str" />

</activity>

配置好了activity节点的meta-data标签,再回到Java代码获取元数据信息,获取步骤分为下列 3 步:

①调用getPackageManager方法获得当前应用的包管理器。

②调用包管理器的getActivityInfo方法获得当前活动的信息对象。

③活动信息对象的metaData是Bundle包裹类型,调用包裹对象的getString即可获得指定名称的参数值。

把上述 3 个步骤串起来,得到以下的元数据获取代码:

// 显示配置的元数据

private void showMetaData() {

   try {

       PackageManager pm = getPackageManager(); // 获取应用包管理器

       // 从应用包管理器中获取当前的活动信息

       ActivityInfo act = pm.getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);

       Bundle bundle = act.metaData; // 获取活动附加的元数据信息

       String value = bundle.getString("weather"); // 从包裹中取出名叫weather的字符串

       tv_meta.setText("来自元数据信息:今天的天气是"+value); // 在文本视图上显示文字

  } catch (Exception e) {

       e.printStackTrace();

 }

}

1.3.3注册快捷方式

譬如在手机桌面上长按支付宝图标,会弹出如图所示的快捷菜单。

点击菜单项“扫一扫”,直接打开支付宝的扫码页面;点击菜单项“付钱”,直接打开支付宝的付款页面;点击菜单项“收钱”,直接打开支付宝的收款页面。如此不必打开支付宝首页,即可迅速跳转到常用的App页面,这便是所谓的快捷方式。

meta-data标签除了前面说到的name属性和value属性,还拥有resource属性,该属性可指定一个XML文件,表示元数据想要的复杂信息保存于XML数据之中。借助元数据以及指定的XML配置,方可完成快捷方式功能,具体的实现过程说明如下:

①首先打开res/values目录下的strings.xml,在resources节点内部添加下述的 3 组(每组两个,共 6 个)字符串配置,这 3 组 6 个字符串的配置定义示例如下:

<string name="first_short">first</string>

<string name="first_long">启停活动</string>

<string name="second_short">second</string>

<string name="second_long">来回跳转</string>

<string name="third_short">third</string>

<string name="third_long">登录返回</string>

②接着在res目录下创建名为xml的文件夹,并在该文件夹创建shortcuts.xml,这个XML文件用来保存 3 组菜单项的快捷方式定义,文件内容如下所示:

<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">

   <shortcut

       android:shortcutId="first"

       android:enabled="true"

       android:icon="@mipmap/ic_launcher"

       android:shortcutShortLabel="@string/first_short"

       android:shortcutLongLabel="@string/first_long">

       <!-- targetClass指定了点击该项菜单后要打开哪个活动页面 -->

       <intent

           android:action="android.intent.action.VIEW"

           android:targetPackage="com.example.chapter04"

           android:targetClass="com.example.chapter04.ActStartActivity" />

       <categories android:name="android.shortcut.conversation"/>

   </shortcut>

   <shortcut

       android:shortcutId="second"

       android:enabled="true"

       android:icon="@mipmap/ic_launcher"

       android:shortcutShortLabel="@string/second_short"

       android:shortcutLongLabel="@string/second_long">

       <!-- targetClass指定了点击该项菜单后要打开哪个活动页面 -->

       <intent

           android:action="android.intent.action.VIEW"

           android:targetPackage="com.example.chapter04"

           android:targetClass="com.example.chapter04.JumpFirstActivity" />

       <categories android:name="android.shortcut.conversation"/>

   </shortcut>

   <shortcut

                    android:shortcutId="third"

       android:enabled="true"

       android:icon="@mipmap/ic_launcher"

       android:shortcutShortLabel="@string/third_short"

       android:shortcutLongLabel="@string/third_long">

       <!-- targetClass指定了点击该项菜单后要打开哪个活动页面 -->

       <intent

           android:action="android.intent.action.VIEW"

           android:targetPackage="com.example.chapter04"

           android:targetClass="com.example.chapter04.LoginInputActivity" />

       <categories android:name="android.shortcut.conversation"/>

   </shortcut>

</shortcuts>

由上述的XML例子中看到,每个shortcut节点都代表了一个菜单项,该节点的各属性说明如下:

①shortcutId:快捷方式的编号。

②enabled:是否启用快捷方式。true表示启用,false表示禁用。

③icon:快捷菜单左侧的图标。

④shortcutShortLabel:快捷菜单的短标签。

⑤shortcutLongLabel:快捷菜单的长标签。优先展示长标签的文本,长标签放不下时才展示短标签的文本。

以上的节点属性仅仅指明了每项菜单的基本规格,点击菜单项之后的跳转动作还要由shortcut内部的intent节点定义,该节点主要有targetPackage与targetClass两个属性需要修改,其中targetPackage属性固定为当前App的包名,而targetClass属性描述了菜单项对应的活动类完整路径。

然后打开AndroidManifest.xml,找到MainActivity所在的activity节点,在该节点内部补充如下的元数据配置,其中name属性为android.app.shortcuts,而resource属性为@xml/shortcuts:

<meta-data android:name="android.app.shortcuts"android:resource="@xml/shortcuts" />

这行元数据的作用,是告诉App首页有个快捷方式菜单,其资源内容参见位于xml目录下的shortcuts.xml。完整的activity节点配置示例如下:

<activity android:name=".MainActivity">

   <intent-filter>

       <action android:name="android.intent.action.MAIN" />

       <category android:name="android.intent.category.LAUNCHER" />

   </intent-filter>

   <!-- 指定快捷方式。在桌面上长按应用图标,就会弹出@xml/shortcuts所述的快捷菜单 -->

   <meta-data

              android:name="android.app.shortcuts"

              android:resource="@xml/shortcuts" />

</activity>

三 、Service

Service 是 Android 提供一个允许长时间留驻后台的一个组件,最常见的用法就是做轮询操作 或者想在后台做一些事情,比如后台下载更新。

3.1 Service 的生命周期

3.2 使用 Service 的方式有两种

  1. startService() 启动 Service。
  2. bindService() 启动 Service。

3.2.1 Service 相关方法

方法

说明

onCreate()

Service 第一次被创建后立即回调该方法,该方法在整个生命周期中只会调用一次

onDestory()

Service 被关闭时会回调该方法,该方法只会回调一次

onStartCommand(intent,flag,startId)

多次调用 startService()方法,不会创建新的 Service 对象,而是继续复用前面产生的 Service 对象,回调 onStartCommand() 方法

IBinder onOnbind(intent)

Service 必须实现的方法,该方法会返回一个 IBinder 对象,app 通过该对象与 Service 组件进行通信

onUnbind(intent)

Service 上绑定的所有客户端都断开时会回调该方法

3.2.2 startService() 启动 Service

首次启动会创建一个 Service 实例,依次调用 onCreate() 和 onStartCommand() 方法

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

验证 startService() 启动 Service 的调用顺序

public class MsTestService extends Service {  

    private final String TAG = "MsTestService";    

    //必须要实现的方法  

    @Override  

    public IBinder onBind(Intent intent) {  

        Log.i(TAG, "onBind方法被调用!");  

        return null;  

    }



    //Service被创建时调用  

    @Override  

    public void onCreate() {  

        Log.i(TAG, "onCreate方法被调用!");  

        super.onCreate();  

    }



    //Service被启动时调用  

    @Override  

    public int onStartCommand(Intent intent, int flags, int startId) {  

        Log.i(TAG, "onStartCommand方法被调用!");  

        return super.onStartCommand(intent, flags, startId);  

    }



    //Service被关闭之前回调  

    @Override  

    public void onDestroy() {  

        Log.i(TAG, "onDestory方法被调用!");  

        super.onDestroy();  

    }  }

修改 AndroidManifest.xml 完成 Service 注册,在 </activity> 后添加

<!-- 配置 Service 组件,同时配置一个 action --><service android:name=".MsTestService">

    <intent-filter>

        <action android:name="cn.twle.android.startservice.MS_TEST_SERVICE"/>

    </intent-filter></service>

修改 activity_main.xml 添加两个按钮

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center_horizontal"

    android:orientation="horizontal" >



    <Button

        android:text="启动 Service"

        android:id="@+id/service_start"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />



    <Button

        android:text="停止 Service"

        android:id="@+id/service_stop"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

修改 MainActivity.java

在按钮的点击事件中分别 调用 startService() 和 stopService()

public class MainActivity extends AppCompatActivity {



    @Override  

    protected void onCreate(Bundle savedInstanceState) {  

        super.onCreate(savedInstanceState);  

        setContentView(R.layout.activity_main);



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

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



        //创建启动 Service 的 Intent

        final Intent it = new Intent(this,MsTestService.class);



        //为两个按钮设置点击事件,分别是启动与停止 service  

        service_start.setOnClickListener(new View.OnClickListener() {              

            @Override  

            public void onClick(View v) {  

                startService(it);                 

            }  

        });



        service_stop.setOnClickListener(new View.OnClickListener() {           

            @Override  

            public void onClick(View v) {  

                stopService(it);



            }  

        });  

    }  }

运行效果如下

3.2.3 bindService() 启动 Service

bindService(Intent Service,ServiceConnection conn,int flags)

参数

说明

service

通过该 Intent 指定要启动的 Service

conn

ServiceConnection 对象,用户监听访问者与 Service 间的连接情况,连接成功回调该对象中的 onServiceConnected(ComponentName,IBinder) 方法;

如果 Service 所在的宿主由于异常终止或者其它原因终止,导致 Service 与访问者间断开连接时调用 onServiceDisconnected(CompanentName ) 方法,主动通过 unBindService() 方法断开并不会调用上述方法

flags

指定绑定时是否自动创建 Service** (如果 Service 还未创建),参数可以是 0(不自动创建),BIND_AUTO_CREATE (自动创建)

①当首次使用 bindService() 绑定一个 Service 时,系统会实例化一个 Service 实例,并调用其 onCreate() 和 onBind() 方法,然后调用者就可以通过 IBinder 和 Service 进行交互了,此后如果再次使用 bindService() 绑定 Service,系统不会创建新的 Sevice 实例,也不会再调用 onBind() 方法,只会直接把 IBinder 对象传递给其他后来增加的客户端。

②想要解除与服务的绑定,只需调用 unbindService(),此时 onUnbind() 和 onDestory() 方法将会被调用。这是一个客户端的情况,假如是多个客户端绑定同一个 Service 的话, 那么当一个客户完成和 service 之间的互动后,它调用 unbindService() 方法来解除绑定。

③当所有的客户端都和 service 解除绑定后,系统会销毁 Service (除非 Service 也被 startService() 方法开启)

与 startService() 不同的是,bindService() 模式下的 Service 是与调用者相互关联的,可以理解为 "一条绳子上的蚂蚱", 要死一起死, 在 bindService()后,一旦调用者销毁,那么 Service 也立即终止

④onServiceConnected() 方法中有一个 IBinder 对象,该对象即可实现与被绑定 Service 之间的通信

我们实现 Service 类时,默认需要实现 IBinder onBind() 方法,该方法返回的 IBinder 对象会传到 ServiceConnection 对象中的 onServiceConnected 的参数,就可以在这里通过这个IBinder与Service进行通信。

验证 BindService 启动 Service 的顺序

public class MsTestService extends Service {

    private final String TAG = "MsTestService";  

    private int count;  

    private boolean quit;

    //定义onBinder方法所返回的对象  

    private MsBinder binder = new MsBinder();  

    public class MsBinder extends Binder  

    {  

        public int getCount()  

        {  

            return count;  

        }  

    }

    //必须实现的方法,绑定改Service时回调该方法  

    @Override  

    public IBinder onBind(Intent intent) {  

        Log.i(TAG, "onBind方法被调用!");  

        return binder;  

    }

    //Service被创建时回调  

    @Override  

    public void onCreate() {  

        super.onCreate();  

        Log.i(TAG, "onCreate方法被调用!");  

        //创建一个线程动态地修改count的值  

        new Thread()  

        {  

            public void run()   

            {  

                while(!quit)  

                {  

                    try  

                    {  

                        Thread.sleep(1000);  

                    }catch(InterruptedException e){e.printStackTrace();}  

                    count++;  

                }  

            };  

        }.start();

    }

    //Service断开连接时回调  

    @Override  

    public boolean onUnbind(Intent intent) {  

        Log.i(TAG, "onUnbind方法被调用!");  

        return true;  

    }

    //Service被关闭前回调  

    @Override  

    public void onDestroy() {  

        super.onDestroy();  

        this.quit = true;  

        Log.i(TAG, "onDestroyed方法被调用!");  

    }

    @Override  

    public void onRebind(Intent intent) {  

        Log.i(TAG, "onRebind方法被调用!");  

        super.onRebind(intent);  

    }  }

修改 AndroidManifest.xml 完成 Service 注册,在 </activity> 后添加

<!-- 配置 Service 组件,同时配置一个 action --><service android:name=".MsTestService">

    <intent-filter>

        <action android:name="cn.twle.android.bindservice.MS_TEST_SERVICE"/>

    </intent-filter></service>

修改 activity_main.xml 添加两个按钮

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center_horizontal"

    android:orientation="horizontal" >



    <Button

        android:text="锁定 Service"

        android:id="@+id/service_lock"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />



    <Button

        android:text="解除锁定"

        android:id="@+id/service_unlock"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />



    <Button

        android:text="获取状态"

        android:id="@+id/service_status"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

修改 MainActivity.java

public class MainActivity extends AppCompatActivity {

    //保持所启动的Service的IBinder对象,同时定义一个ServiceConnection对象  

    MsTestService.MsBinder binder;  

    private ServiceConnection conn = new ServiceConnection() {

        // Activity 与 Service 断开连接时回调该方法

        @Override  

        public void onServiceDisconnected(ComponentName name) {  

            Log.i("MsTestService","------Service DisConnected-------");  

        }

        // Activity 与 Service 连接成功时回调该方法  

        @Override  

        public void onServiceConnected(ComponentName name, IBinder service) {  

            Log.i("MsTestService","------Service Connected-------");  

            binder = (MsTestService.MsBinder) service;  

        }  

    };

    @Override  

    protected void onCreate(Bundle savedInstanceState) {  

        super.onCreate(savedInstanceState);  

        setContentView(R.layout.activity_main);

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

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

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

        final Intent it = new Intent(this,MsTestService.class);



        service_lock.setOnClickListener(new View.OnClickListener() {            

            @Override  

            public void onClick(View v) {  

                //绑定service  

                bindService(it, conn, Service.BIND_AUTO_CREATE);                  

            }  

        });



        service_unlock.setOnClickListener(new View.OnClickListener() {  

            @Override  

            public void onClick(View v) {  

                //解除service绑定  

                unbindService(conn);                  

            }  

        });



        service_status.setOnClickListener(new View.OnClickListener() {  

            @Override  

            public void onClick(View v) {  

                Toast.makeText(getApplicationContext(), "Service的count的值为:"  

                        + binder.getCount(), Toast.LENGTH_SHORT).show();  

            }  

        });  

    }  }

运行效果如下

3.3 IntentService 耗时操作

如果我们直接把耗时线程放到 Service 中的 onStart() 方法中,虽然可以这样做,但是很容易 会引起 ANR 异常 ( Application Not Responding),这是因为

Service 不是一个单独的进程,它和它的应用程序在同一个进程中

Service 也不是一个线程,我们应该避免在 Service 中进行耗时操作

为了解决可能需要在 Service 中进行耗时的任务,Android 提供了 IntentService。

IntentService 是继承自 Service 并用来处理异步请求的一个类。

IntentService 中有一个工作线程来处理耗时操作,请求的 Intent 记录会加入队列

IntentService 的一般用法

①客户端通过 startService(Intent) 来启动 IntentService。

②不需要手动的去控制 IntentService ,当任务执行完后 IntentService 会自动停止。

③IntentService 可以被启动多次,每个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent() 回调方法中执行,并且每次只会执行一个工作线程,执行完一,再到二这样。

代码示例

public class MsService extends IntentService{

    private final String TAG = "IntentService:ms";

    //必须实现父类的构造方法

    public MsService()

    {

        super("MsService");

    }



    //必须重写的核心方法  

    @Override  

    protected void onHandleIntent(Intent intent) {  

        //Intent 是从 Activity 发过来的,携带识别参数,根据参数不同执行不同的任务



        String action = intent.getExtras().getString("param");



        if(action.equals("s1"))

            Log.i(TAG,"启动service1");  

        else if(action.equals("s2"))

            Log.i(TAG,"启动service2");  

        else if(action.equals("s3"))

            Log.i(TAG,"启动service3");



        //让服务休眠2秒  

        try{  

            Thread.sleep(2000);  

        }catch(InterruptedException e){e.printStackTrace();}          

    }



    //重写其他方法,用于查看方法的调用顺序  

    @Override  

    public IBinder onBind(Intent intent) {  

        Log.i(TAG,"onBind");  

        return super.onBind(intent);  

    }



    @Override  

    public void onCreate() {  

        Log.i(TAG,"onCreate");  

        super.onCreate();  

    }



    @Override  

    public int onStartCommand(Intent intent, int flags, int startId) {  

        Log.i(TAG,"onStartCommand");  

        return super.onStartCommand(intent, flags, startId);  

    }



    @Override  

    public void setIntentRedelivery(boolean enabled) {  

        super.setIntentRedelivery(enabled);  

        Log.i(TAG,"setIntentRedelivery");  

    }



    @Override  

    public void onDestroy() {  

        Log.i(TAG,"onDestroy");  

        super.onDestroy();  

    }

}

修改 AndroidManifest.xml 注册 Service

<service android:name=".MsService" android:exported="false">  

    <intent-filter >  

        <action android:name="cn.twle.android.intentservice"/>  

    </intent-filter>  </service>

修改 MainActivity.java 启动三次服务

public class MainActivity extends Activity {



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);



        Intent it1 = new Intent(MainActivity.this,MsService.class);

        Bundle b1 = new Bundle();

        b1.putString("param", "s1");

        it1.putExtras(b1);



        Intent it2 = new Intent(MainActivity.this,MsService.class);

        Bundle b2 = new Bundle();

        b2.putString("param", "s2");

        it2.putExtras(b2);



        Intent it3 = new Intent(MainActivity.this,MsService.class);

        Bundle b3 = new Bundle();

        b3.putString("param", "s3");

        it3.putExtras(b3);



        //接着启动多次IntentService,每次启动,都会新建一个工作线程

        //但始终只有一个IntentService实例

        startService(it1);

        startService(it2);

        startService(it3);

    }}

日志输出如下

四、 BroadcastReceiver

Android 系统自己在很多时候都会发送广播,比如电量低或者充足,刚启动完,插入耳机,输入法改变等, 发生这些时间,系统都会发送广播,这个叫系统广播。

每个 APP 都会收到,如果想让一个应用在接收到广播的时候做一些操作,比如:为应用注册一个用于监视开机的 BroadcastReceiver ,当接收到开机广播就做自己的事情。

4.1 两种广播类型

①标准广播

完全异步执行的广播,发出广播后,所有的广播接收器几乎会在同一时刻收到这条广播通知

②有序广播

同步执行的一种广播,发出广播后,同一时间只有一个广播接收者能收到,当这个广播接收者的逻辑执行完后,才会传递到下一个广播接收者。

前一个广播接收者还可以截断广播,让广播不会继续传递。

4.2 两种注册广播的方式

①动态注册

就是在 Java 代码中指定 Intent-filter,然后添加不同的 Action。想监听什么广播就写什么 Action。

动态注册需要应用程序启动后才能接收广播信息

注意: 动态注册的广播一定要调用 unregisterReceive() 取消广播注册

②静态注册

在 AndroidManifest.xml 设置 <receiver> 就可以让 APP 在未启动的情况下接收到广播。

4.2.1动态注册

一个 网络状态广播 的例子,演示接收 网络状态变更 的广播

自定义一个 BroadcastReceiver,在 onReceive() 方法中完成广播要处理的事务,在 MainActivity.java 同一目录下创建文件 MsBroadcastReceiver.java

public class MsBroadcastReceiver extends BroadcastReceiver{

    @Override

    public void onReceive(Context context, Intent intent) {

        Toast.makeText(context,"网络状态发生改变~",Toast.LENGTH_SHORT).show();

    }}

修改 MainActivity.java 动态注册广播

public class MainActivity extends AppCompatActivity {



    MsBroadcastReceiver mReceiver;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

Button btn_sendby=findViewById(R.id.btn_sendbr);

btn_sendby.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        intent.setAction("com.example.myapplication.MY_BROADCAST");

sendBroadcast(intent);

    }
});

}

//在onResume()中进行动态广播的注册

@Override
protected void onResume() {
    super.onResume();
    mReceiver=new MyReceiver();
    IntentFilter intentFilter=new IntentFilter();
    intentFilter.addAction("com.example.myapplication.MY_BROADCAST");// 只有持有相同的action的接受者才能接收此广播
    registerReceiver(mReceiver,intentFilter);//第一个参数为广播接收器对象,第二个参数为IntentFilter对象
}



    //别忘了将广播取消掉



    @Override

    protected void onDestroy() {

        super.onDestroy();

        unregisterReceiver(mReceiver);

    }}

4.2.2静态注册

我们以接收开机广播为例讲解如何 静态注册 广播

在 MainActivity.java 目录下创建 MsBootCompleteReceiver,重写 onReceive 完成事务处理

public class MsBootCompleteReceiver extends BroadcastReceiver {

    private final String ACTION_BOOT = "android.intent.action.BOOT_COMPLETED";

    @Override

    public void onReceive(Context context, Intent intent) {

    if (ACTION_BOOT.equals(intent.getAction()))

        Toast.makeText(context, "开机完毕~", Toast.LENGTH_LONG).show();

    }}

在 AndroidManifest.xml 中对该 MsBootCompleteReceiver 进行注册,添加开机广播的 intent-filter

<receiver android:name=".MsBootCompleteReceiver">

    <intent-filter>

        <action android:name = "android.intent.action.BOOT_COMPLETED" />

    </intent-filter></receiver>

同时加上 android.permission.RECEIVE_BOOT_COMPLETED 的权限

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

然后重启下手机会发现过了一会儿,就会弹出开机完毕这个 Toast 的了

注意事项

广播更多的时候扮演的是一个打开其它组件的角色,比如启动 Service, Notification 提示 , Activity 等

不要在广播里添加过多逻辑或者进行任何耗时操作因为在广播中是不允许开辟线程的,当 onReceiver() 方法运行较长时间 ( 超过 10 秒 ) 还没有结束的话,那么程序会报错

4.3 发送广播

BroadcastReceiver 接收广播,如何发送广播。

发送广播的流程

  1. 自定义一个 BroadcastReceiver ,重写 onReceive() 方法,然后注册广播
  2. 发送标准广播 sendBroadcast(intent);
  3. 发送有序广播 sendOrderedBroadcast(intent,null)
  4. 可以在 AndroidManifest.xml 中的 <Intent-filter> 中通过 android:priority="100" 设置优先级,值越大优先级越高,越先收到广播,优先级可选范围 -1000 - 1000
  5. 可以调用 abortBroadcast() 截断广播,让其不再继续传递

代码示例

在 MainActivity.java 目录下新建一个广播接收者 MsBroadcastReceiver.java

public class MsBroadcastReceiver extends BroadcastReceiver {

    private final String ACTION_BOOT = "cn.twle.android.sendbroadcast.MS_BROADCAST";

    @Override

    public void onReceive(Context context, Intent intent) {

        if( ACTION_BOOT.equals(intent.getAction()))

        Toast.makeText(context, "收到告白啦~",Toast.LENGTH_LONG).show();

    }}

修改 AndroidManifest.xml 注册 MsBroadcastReceiver ,写上 Intent-filter

<receiver android:name=".MsBroadcastReceiver">

    <intent-filter>

        <action android:name="cn.twle.android.sendbroadcast.MS_BROADCAST"/>

    </intent-filter></receiver>

然后修改 activity_main.xml 添加一个按钮 发送告白

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center_horizontal"

    android:orientation="vertical" >

    <Button

        android:text="发送告白"

        android:id="@+id/btn_send"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

修改 MainActivity.java 完成广播发送

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

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

        btn_send.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

Intent intent = new Intent();
intent.setAction("cn.twle.android.sendbroadcast.MS_BROADCAST");
//广播自定义意图,要设置接收器所在的namespace名,以及全类名。
intent.setComponent(new ComponentName("cn.twle.android.sendbroadcast","cn.twle.android.sendbroadcast.MsBroadcastReceiver"));
sendBroadcast(intent);

            }

        });

    }}

4.4 LocalBroadcast本地广播

前面说的广播都是 全局广播,意味着我们 APP 发出的广播,其它 APP 都会接收到, 或者其它 APP 发送的广播,我们的 APP 也会接收到,这样容易引起一些安全性的问题。

Android 提供了 本地广播 的机制,使用该机制发出的广播只会在 APP 内部传播,而且 广播接收者也只能收到本应用发出的广播。

本地广播 发出的广播只会在 APP 内部传播,而且 广播接收者也只能收到本应用发出的广播

本地广播无法通过静态注册方式来接受,相比起系统全局广播更加高效

使用流程

  1. 使用 LocalBroadcastManager 来管理广播
  2. 调用 LocalBroadcastManager.getInstance() 获得实例 mLBM
  3. 调用 mLBM.registerReceiver() 注册广播
  4. 调用 mLBM.sendBroadcase() 发送广播
  5. 调用 mLBM.unregisterReceiver() 取消注册广播

注意事项

在广播中启动 Activity 的话需要为 intent 加入 FLAG_ACTIVITY_NEW_TASK 标记,不然会报错,因为需要一个栈来存放新打开的 Activity。

广播中弹出 AlertDialog 的话,需要设置对话框的类型为 TYPE_SYSTEM_AERT 不然无法弹出。

4.5 Android 系统广播

Android 系统内置了很多广播,下表列出了平时可能会用到的一些

系统广播常量

说明

Intent.ACTION_AIRPLANE_MODE_CHANGED

关闭或打开飞行模式时的广播

Intent.ACTION_BATTERY_CHANGED

充电状态,或者电池的电量发生变化,不能通过组建声明接收这个广播,只有通过 Context.registerReceiver() 注册

Intent.ACTION_BATTERY_LOW

表示电池电量低

Intent.ACTION_BATTERY_OKAY

表示电池电量充足,即从电池电量低变化到饱满时会发出广播

Intent.ACTION_BOOT_COMPLETED

在系统启动完成后,这个动作被广播一次 ( 只有一次 )

Intent.ACTION_CAMERA_BUTTON

按下照相时的拍照按键(硬件按键)时发出的广播

Intent.ACTION_CLOSE_SYSTEM_DIALOGS

当屏幕超时进行锁屏时,当用户按下电源按钮,长按或短按(不管有没跳出话框),进行锁屏时,android系统都会广播此Action消息

Intent.ACTION_CONFIGURATION_CHANGED

设备当前设置被改变时发出的广播(包括的改变:界面语言,设备方向等,请参考 Configuration.java)

Intent.ACTION_DATE_CHANGED

设备日期发生改变时会发出此广播

Intent.ACTION_DEVICE_STORAGE_LOW

设备内存不足时发出的广播,此广播只能由系统使用,其它APP不可用?

Intent.ACTION_DEVICE_STORAGE_OK

设备内存从不足到充足时发出的广播,此广播只能由系统使用,其它APP不可用?

Intent.ACTION_DOCK_EVENT

发出此广播的地方 frameworks\base\services\java\com \android\server\DockObserver.java

Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE

移动APP完成之后,发出的广播(移动是指:APP2SD)

Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE

正在移动APP时,发出的广播(移动是指:APP2SD)

Intent.ACTION_GTALK_SERVICE_CONNECTED

Gtalk已建立连接时发出的广播

Intent.ACTION_GTALK_SERVICE_DISCONNECTED

Gtalk已断开连接时发出的广播

Intent.ACTION_HEADSET_PLUG

在耳机口上插入耳机时发出的广播

Intent.ACTION_INPUT_METHOD_CHANGED

改变输入法时发出的广播

Intent.ACTION_LOCALE_CHANGED

设备当前区域设置已更改时发出的广播

Intent.ACTION_MANAGE_PACKAGE_STORAGE

Intent.ACTION_MEDIA_BAD_REMOVAL

未正确移除SD卡(正确移除SD卡的方法:设置--SD卡和设备内存--卸载SD卡),但已把SD卡取出来时发出的广播。 扩展介质(扩展卡)已经从 SD 卡插槽拔出,但是挂载点 (mount point) 还没解除 (unmount)

Intent.ACTION_MEDIA_BUTTON

按下 "Media Button" 按键时发出的广播,假如有"Media Button" 按键的话(硬件按键)

Intent.ACTION_MEDIA_CHECKING

插入外部储存装置,比如SD卡时,系统会检验SD卡,此时发出的广播?

Intent.ACTION_MEDIA_EJECT

已拔掉外部大容量储存设备发出的广播(比如SD卡,或移动硬盘),不管有没有正确卸载都会发出此广播?用户想要移除扩展介质(拔掉扩展卡)。

Intent.ACTION_MEDIA_MOUNTED

插入SD卡并且已正确安装(识别)时发出的广播

Intent.ACTION_MEDIA_NOFS

Intent.ACTION_MEDIA_REMOVED

外部储存设备已被移除,不管有没正确卸载,都会发出此广播?

Intent.ACTION_MEDIA_SCANNER_FINISHED

已经扫描完介质的一个目录

Intent.ACTION_MEDIA_SCANNER_SCAN_FILE

Intent.ACTION_MEDIA_SCANNER_STARTED

开始扫描介质的一个目录

Intent.ACTION_MEDIA_SHARED

扩展介质的挂载被解除 (unmount),因为它已经作为 USB 大容量存储被共享

Intent.ACTION_MEDIA_UNMOUNTABLE

Intent.ACTION_MEDIA_UNMOUNTED

扩展介质存在,但是还没有被挂载 (mount)。

Intent.ACTION_NEW_OUTGOING_CALL

Intent.ACTION_PACKAGE_ADDED

设备上新安装了一个应用程序包。一个新应用包已经安装在设备上,数据包括包名(最新安装的包程序不能接收到这个广播)

Intent.ACTION_PACKAGE_CHANGED

一个已存在的应用程序包已经改变,包括包名

Intent.ACTION_PACKAGE_DATA_CLEARED

清除一个应用程序的数据时发出的广播(在设置--应用管理--选中某个应用,之后点清除数据时?),用户已经清除一个包的数据,包括包名(清除包程序不能接收到这个广播)

Intent.ACTION_PACKAGE_INSTALL

触发一个下载并且完成安装时发出的广播,比如在电子市场里下载应用?

Intent.ACTION_PACKAGE_REMOVED

成功的删除某个APK之后发出的广播,一个已存在的应用程序包已经从设备上移除,包括包名(正在被安装的包程序不能接收到这个广播)

Intent.ACTION_PACKAGE_REPLACED

替换一个现有的安装包时发出的广播(不管现在安装的APP比之前的新还是旧,都会发出此广播?)

Intent.ACTION_PACKAGE_RESTARTED

用户重新开始一个包,包的所有进程将被杀死,所有与其联系的运行时间状态应该被移除,包括包名(重新开始包程序不能接收到这个广播)

Intent.ACTION_POWER_CONNECTED

插上外部电源时发出的广播

Intent.ACTION_POWER_DISCONNECTED

已断开外部电源连接时发出的广播

Intent.ACTION_PROVIDER_CHANGED

Intent.ACTION_REBOOT

重启设备时的广播

Intent.ACTION_SCREEN_OFF

屏幕被关闭之后的广播

Intent.ACTION_SCREEN_ON

屏幕被打开之后的广播

Intent.ACTION_SHUTDOWN

关闭系统时发出的广播

Intent.ACTION_TIMEZONE_CHANGED

时区发生改变时发出的广播

Intent.ACTION_TIME_CHANGED

时间被设置时发出的广播

Intent.ACTION_TIME_TICK

当前时间改变,每分钟都发送,不能通过组件声明来接收,只有通过Context.registerReceiver()方法来注册

Intent.ACTION_UID_REMOVED

一个用户ID已经从系统中移除发出的广播

Intent.ACTION_UMS_CONNECTED

设备已进入USB大容量储存状态时发出的广播

Intent.ACTION_UMS_DISCONNECTED

设备已从USB大容量储存状态转为正常状态时发出的广播

Intent.ACTION_USER_PRESENT

Intent.ACTION_WALLPAPER_CHANGED

设备墙纸已改变时发出的广播

五、ContentProvider

ContentProvider可以让我们的 APP 访问别的应用,或者说一些 ContentProvider 暴露给我们的一些数据,比如手机联系人,短信等,如果我们想对这些数据进行读取或者修改,就需要用到 ContentProvider。

ContentProvider 也允许我们将自己应用里的数据暴露出来,给其它的应用进行读取或操作。

当我们想允许自己 APP 的数据开放给别的 APP 进行读取操作,我们就需要让 APP 实现 ContentProvider 类,同时注册一个 URI,然后其它的 APP 就可以通过 ContentResolver 根据 URI 就可以访问我们 APP 的数据

数据可以是任何内容,比如数据库,一个文件,一个 XML 或者其它

5.1 ContentProvider 的运行原理

5.2 URI

URI,统一资源标识符(Uniform Resource Identifier,或 URI)是一个用于标识某一互联网资源名称的字符串

ContentProvider 使用以content:// 开头的 URI 协议

content://cn.twle.android.provider.MsLanguageProvider/lang/kotlin

(1)content://协议头,功能类似 http:// 和 ftp:// ,ContentProvider 只会识别以 content:// 开头的 URI 协议

(2)cn.twle.android.provider.MsLanguageProvider

ContentProvider 所在的全限定类名,就是 包名+类名

(3)lang

资源部分,如果想访问 lang 下的所有资源,则可以去掉 /kotlin

(4)kotlin

就是 lang 下的 kotlin 资源

5.3系统内建的 ContentProvider

大多时候,我们使用 ContentProvider 并不是自己暴露自己的数据,而是通过 ContentResolver 来读取其它应用的信息,比如读取系统 APP,信息,联系人,多媒体信息

Android 系统提供了大量的内建 ContentProvider ,拿最常用的联系人为例

5.3.1读取手机联系人

修改 MainActivity.java 读取手机联系人

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        getContacts();

    }



    private void getContacts(){

        // 1. 查询 raw_contacts 表获得联系人的 id

        ContentResolver resolver = getContentResolver();

        Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;

        //查询联系人数据

        Cursor cursor = resolver.query(uri, null, null, null, null);

        while(cursor.moveToNext())//cursor查询指的是第一条记录前的记录,moveToNext()指向当前的第一条记录。

        {

            //获取联系人姓名,手机号码

            String cName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));

            String cNum = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));

            System.out.println("姓名:" + cName + " 号码:" + cNum);

        }

        cursor.close();

    }}

然后修改 AndroidManifest.xml 加入读联系人的权限

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

5.3.2查询指定电话的联系人信息

修改 MainActivity.java 查询指定电话的联系人信息

public class MainActivity extends AppCompatActivity {



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);



        // 你需要修改要查询的电话号码,因为你那里不一定保留了 中国移动

        queryContact("10086");

    }



    private void queryContact(String number){

            Uri uri = Uri.parse("content://com.android.contacts/data/phones/filter/" + number);

            ContentResolver resolver = getContentResolver();

            Cursor cursor = resolver.query(uri, new String[]{"display_name"}, null, null, null);

            if (cursor.moveToFirst()) {

                String name = cursor.getString(0);

                System.out.println(number + " 对应的联系人名称 :" + name);

            }

        cursor.close();

    }}

然后修改 AndroidManifest.xml 加入读联系人的权限

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

5.3.3添加一个新的联系人

修改 activity_main.xml 添加一个按钮

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center_horizontal"

    android:orientation="vertical" >

    <Button

        android:text="添加联系人"

        android:id="@+id/btn_add"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

修改 MainActivity.java 添加一个新的联系人

public class MainActivity extends AppCompatActivity {



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);



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



        btn_add.setOnClickListener(new View.OnClickListener(){



            @Override

            public void onClick( View view )

            {



                try {

                    AddContact();

                } catch (Exception e){}

            }

        });  

    }



    private void AddContact() throws RemoteException, OperationApplicationException {



        //使用事务添加联系人

        Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");

        Uri dataUri =  Uri.parse("content://com.android.contacts/data");



        ContentResolver resolver = getContentResolver();

        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();

        ContentProviderOperation op1 = ContentProviderOperation.newInsert(uri)

                .withValue("account_name", null)

                .build();

        operations.add(op1);



        //依次是姓名,号码,邮编

        ContentProviderOperation op2 = ContentProviderOperation.newInsert(dataUri)

                .withValueBackReference("raw_contact_id", 0)

                .withValue("mimetype", "vnd.android.cursor.item/name")

                .withValue("data2", "中国联通")

                .build();

        operations.add(op2);



        ContentProviderOperation op3 = ContentProviderOperation.newInsert(dataUri)

                .withValueBackReference("raw_contact_id", 0)

                .withValue("mimetype", "vnd.android.cursor.item/phone_v2")

                .withValue("data1", "100010")

                .withValue("data2", "2")

                .build();

        operations.add(op3);



        ContentProviderOperation op4 = ContentProviderOperation.newInsert(dataUri)

                .withValueBackReference("raw_contact_id", 0)

                .withValue("mimetype", "vnd.android.cursor.item/email_v2")

                .withValue("data1", "kf@10010.com")

                .withValue("data2", "2")

                .build();

        operations.add(op4);

        //将上述内容添加到手机联系人中~

        resolver.applyBatch("com.android.contacts", operations);

        Toast.makeText(getApplicationContext(), "添加成功", Toast.LENGTH_SHORT).show();

    }}

修改 AndroidManifest.xml 加入读写联系人信息权限

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

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

5.4自定义 ContentProvider

一般情况下,很少需要自己来定义 ContentProvider ,因为很多时候我们并不希望自己应用的数据暴露给其它应用

但即使这样,学习如何 ContentProvider 还是有必要的,多一种数据传输的方式

自定义 ContentProvider 的流程一般如下

①在分享数据的 APP 中创建一个类,继承 ContentProvider

②按需实现对应的方法,不需要的直接空实现

方法

说明

onCreate()

只执行一次,用于初始化 Provider

insert()

插入

delete()

删除

update()

更新

query()

查询

getType()

获得 ContentProvider 数据的 MIME 类型

③在 AndroidManifest.xml 中注册自定义的 ContentProvider

<provider

    <!-- 全限定类名 -->

    android:name = "cn.twle.android.bean.NameContentProvider"

    <!-- 用于匹配的 URI -->

    android:authorities = "cn.twle.android.providers.msprovider"

    <!-- 是否共享数据 -->

    android:exported="true"></provider>

④使用 UriMatcher 完成 Uri 的匹配

初始化 UriMatcher 对象

private static UriMatcher matcher = new UriMatcher (UriMatcher.NO_MATCH);

使用静态代码块,通过 addURI() 方法将 uri 添加到 matcher 中

static {

matcher.addURI("cn.twle.android.providers.msprovider","test","1");

}

前两个参数构成 URI, 第三个参数:匹配后返回的标识码,如果不匹配返回 -1

在需要匹配 Uri 的地方使用 match() 方法

switch( matcher.match(uri)) {

    case 1:

        break;

    case 2:

        break;

    default:

        break;

}

当然还可以使用通配符,比如 test/* 或 test/# * 代表所有字符, # 代表数字

使用 ContentUris 类为 Uri 追加 id, 或者解析 Uri 中的 id

withAppendedId(uri,id) 为路径添加 id 部分

Uri nameUri = ContentUris.withAppendedId(uri,rowId);

parseId(uri) 解析 uri 中的 id 值

long nameId = ContentUris.parseId(uri);

⑤然后在另一个工程中,调用 getContentResolver() 方法获得 Resolver 对象,再调用相应的操作方法,比如插入操作

ContentValues values = new ContentValues();

values.put("name","测试");

Uri uri = Uri.parse("cn.twle.android.providers.msprovider/test");

resolver.insert(uri,values);

完整示例

在 MainActivity.java 同一目录下添加一个数据库创建类 DBOpenHelper.java

public class DBOpenHelper extends SQLiteOpenHelper {

    final String CREATE_SQL = "CREATE TABLE test(_id INTEGER PRIMARY KEY AUTOINCREMENT,name)";

    public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,

            int version) {

        super(context, name, null, 1);

    }



    @Override

    public void onCreate(SQLiteDatabase db) {

        db.execSQL(CREATE_SQL);

    }



    @Override

    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        // TODO Auto-generated method stub



    }

}

在 MainActivity.java 同一目录下添加一个自定义 ContentProvider 类,实现 onCreate(),getType()

public class NameContentProvider extends ContentProvider {



    //初始化一些常量

     private static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);        

     private DBOpenHelper dbOpenHelper;



    //为了方便直接使用UriMatcher,这里addURI,下面再调用Matcher进行匹配



     static{  

         matcher.addURI("cn.twle.android.providers.msprovider", "test", 1);

     }



    @Override

    public boolean onCreate() {

        dbOpenHelper = new DBOpenHelper(this.getContext(), "test.db", null, 1);

        return true;

    }



    @Override

    public Cursor query(Uri uri, String[] projection, String selection,

            String[] selectionArgs, String sortOrder) {

        return null;

    }



    @Override

    public String getType(Uri uri) {

        return null;

    }



    @Override

    public Uri insert(Uri uri, ContentValues values) {



        switch(matcher.match(uri))

        {

        //把数据库打开放到里面是想证明uri匹配完成

        case 1:

            SQLiteDatabase db = dbOpenHelper.getReadableDatabase();

            long rowId = db.insert("test", null, values);

            if(rowId > 0)

            {

                //在前面已有的Uri后面追加ID

                Uri nameUri = ContentUris.withAppendedId(uri, rowId);

                //通知数据已经发生改变

                getContext().getContentResolver().notifyChange(nameUri, null);

                return nameUri;

            }

        }

        return null;

    }



    @Override

    public int delete(Uri uri, String selection, String[] selectionArgs) {

        return 0;

    }



    @Override

    public int update(Uri uri, ContentValues values, String selection,

            String[] selectionArgs) {

        return 0;

    }

}

修改 AndroidManifest.xml 中为 ContentProvider 进行注册

<provider

    android:name="cn.twle.android.customprovider.NameContentProvider"

    android:authorities="cn.twle.android.providers.msprovider"

    android:exported="true" />

修改 activity_main.xml

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

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:gravity="center_horizontal"

    android:orientation="vertical" >

    <Button

        android:text="插入数据"

        android:id="@+id/btn_insert"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content" />

</LinearLayout>

修改 MainActivity.java 实现 ContentResolver 的部分,点击按钮插入一条数据


public class MainActivity extends AppCompatActivity {



    private Button btn_insert;



    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);



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



        //读取contentprovider 数据  

        final ContentResolver resolver = this.getContentResolver();



        btn_insert.setOnClickListener(new View.OnClickListener() {



            @Override

            public void onClick(View v) {

                 ContentValues values = new ContentValues();

                 values.put("name", "测试");

                 Uri uri = Uri.parse("content://cn.twle.android.providers.msprovider/test");

                resolver.insert(uri, values);

                Toast.makeText(getApplicationContext(), "数据插入成功", Toast.LENGTH_SHORT).show();

            }

        });

    }}

5.5 Android 更多 Provider

除了 ContentProvider 或者自定义 ContentProvider 外,Android 还提供了其它几个 Provider

①Calendar Provider

https://developer.android.google.cn/guide/topics/providers/calendar-provider?hl=zh-cn

日历提供者,就是针对针对日历相关事件的一个资源库,通过它提供的 API,我们可以对日历,时间,会议,提醒等内容做一些增删改查

②Contacts Provider

https://developer.android.google.cn/guide/topics/providers/contacts-provider?hl=zh-cn

联系人提供者,这个就不用说了,这个用得最多

③Storage Access Framework(SAF)

存储访问框架

https://developer.android.google.cn/guide/topics/providers/document-provider?hl=zh-cn

为用户浏览手机中的存储内容提供了便利,可供访问的内容不仅包括:文档,图片,视频,音频,下载,而且包含所有由由特定 ContentProvider(须具有约定的API)提供的内容

不管这些内容来自于哪里,不管是哪个应用调用浏览系统文件内容的命令,系统都会用一个统一的界面让你去浏览。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值