这是我在学习过程中总结的知识
目的是希望日后回来看或者需要用的时候可以 一目了然 # 的回顾、巩固、查缺补漏
不追求详细相当于书本的精简版或者说是导读(想看详细的直接对应翻书),但会尽力保证读者都能快速理解和快速使用(随理解加深会总结的更加精简),但必要时会附上一些较详细解释的链接
脚注是空白的:表示还没弄懂的知识,了解后会添加
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_2.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
notification.setLatestEventInfo(this, "chapter_5", "this is notification.", pendingIntent);
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);
上述代码弹出默认样式的通知,下面自定义通知
首先提供一个自定义布局,然后通过RemoteViews来加载
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
intent.putExtra("sid", "" + sId);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
System.out.println(pendingIntent);
//创建对象只需要当前应用的包名和布局文件资源id,其中的layout就是通知栏的标题、图片、描述的布局
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_notification);
//标题,这就是一种更新远程View的方法,TextView的id和要设置的文本
remoteViews.setTextViewText(R.id.msg, "chapter_5: " + sId);
//图片
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
manager.notify(sId, notification);
为什么更新RemoteViews如此复杂呢?因为没有和View类似的findViewById方法,当然还有其他原因见5.2
AppWidgetProvider继承于广播类BroadcastReceiver,下面简单介绍桌面小部件的开发步骤
1. 定义小部件界面
这就是小部件长什么样子
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon1" />
</LinearLayout>
2. 定义小部件配置信息
在res/xml/appwidget_provider_info.xml
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
//小工具所使用的初始化布局
android:initialLayout="@layout/widget"
//最小尺寸
android:minHeight="84dp"
android:minWidth="84dp"
//自动更新周期
android:updatePeriodMillis="86400000" >
</appwidget-provider>
3. 定义小部件的类
这个类有点复杂,我们跟着注释走
public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MyAppWidgetProvider";
//判断小部件动作的
public static final String CLICK_ACTION = "com.ryg.chapter_5.action.CLICK";
public MyAppWidgetProvider() {
super();
}
/**
这个方法是处理接收到的动作的方法
*/
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
Log.i(TAG, "onReceive : action = " + intent.getAction());
// 这里判断是自己的action,做自己的事情,比如小工具被点击了要干啥,这里是做一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(
context.getResources(), R.drawable.icon1);
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
for (int i = 0; i < 37; i++) {
//旋转360度
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context
.getPackageName(), R.layout.widget);
//旋转一次
remoteViews.setImageViewBitmap(R.id.imageView1,
rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent
.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(
context, MyAppWidgetProvider.class),remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
}
/**
* 每次窗口小部件被点击更新都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
final int counter = appWidgetIds.length;
Log.i(TAG, "counter = " + counter);
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
/**
* 窗口小部件更新
*
* @param context
* @param appWidgeManger
* @param appWidgetId
*/
private void onWidgetUpdate(Context context,
AppWidgetManager appWidgeManger, int appWidgetId) {
Log.i(TAG, "appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.widget);
// "窗口小部件"点击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
}
private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0,
srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}
4. 在AndroidManifest.xml中声明小部件
注册广播
<receiver android:name=".MyAppWidgetProvider" >
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info" >
</meta-data>
<intent-filter>
<action android:name="com.ryg.chapter_5.action.CLICK" />
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
其中有两个Action,其中第一个用于识别小部件的单击行为、第二个作为小部件的标识必须存在,否则无法添加这个小部件
AppWidgetProvider还有其他的几个方法,当广播来到后会自动根据广播的Action通过onReceive方法来自动分发广播也就是调用如下方法:
· onEnable:小部件第一次添加到桌面时调用该方法
· onUpdate:小部件被添加或者更新时调用,周期自动更新时机由updatePeriodMillis来指定
· onDelete:删除一次调用一次
· onDisable:最后一个该类型的桌面小部件被删除后调用
具体onReceive方法可以查看源码,以上步骤是通用的开发小部件的步骤,可以发现,小部件初始化还是更新都需要用到RemoteViews
· PendingIntent是在某个不确定的时间发生了,不同于Intent立即发生
· 因为RemoteViews运行在远程进程中,不能像View通过setOnClickListener来设置单击事件,所以必须使用PendingIntent
· PendingIntent通过send和cancel方法来发送和取消特定Intent
PendingIntent支持三种意图:启动Activity、启动Service、发送广播,对应如下三种接口方法
下面讲解4个参数:
· requestCode表示发送方的请求码,多数为0即可,另外会影响flags的效果
· 当两个PendingIntent内部的Intent和requestCode相同时,两个PendingIntent也就相同
· 当两个Intent的ComponentName和intent-filter相同时,两个Intent也就相同
下面讲flags的四个参数
FLAG_ONE_SHOT
当前PendingIntent只使用一次,然后自动被cancel.后续相同的PendingIntent的send方法就会调用失败
对于通知栏消息,同类的通知只能使用一次,后续通知单击后无法再打开
FLAG_NO_CREATE
不主动创建,不常用
FLAG_CANCEL_CURRENT
如果当前PendingIntent已存在,那么它会被cancel,然后系统再创建一个新的PendingIntent
被cancel的消息单击后无法再打开
FLAG_UPDATE_CURRENT
已经存在,那么会被更新,即内部Intent的Extras会被换成最新的
现在让我们继续分析,最后发出通知的代码是manager.notify(1,notification).
第一个参数1是一个id,是常量.那么多次调用notify都只会弹出最新的一个通知,因为id没变.
如果id不同就会弹出多个通知
如果id不同时
· 不同的PendingIntent无论哪个标记位,都不会互相干扰
· 同PendingIntent+FLAG_ONT_SHOT:多条通知,只能点其中一个,然后其他失效.清除所有通知后重新生效
· 同PendingIntent+FLAG_CANCEL_CURRENT:只有最新的通知可打开,之前的都不行
·同PendingIntent+FLAG_UPDATE_CURRENT:之前弹出的被更新到和最新的PendingIntent一致,并且都可以打开
我们还记得RemoteViews的构造方法有两个参数:当前应用的包名和布局文件id,下面来看支持哪种类型的View(不支持自定义View)
Layout
FrameLayout、LinearLayout、RelativeLayout、GridLayout
View
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub
使用RemoteViews的时候提供了一系列的set方法(ctrl+鼠标左键或者.set自动弹出提示,然后看方法名就能大致知道用法,不了解的再去百度)
其中RemoteViews还可以通过反射调用View的方法:setInt()
关于内部机制理论知识来啦
通知栏和小部件由NotificationManager和AppWidgetManager管理
以上两个Manager通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetService进行通信
所以他们的布局文件实际是在以上各自的Service中被加载的,运行在SystemServer中,是一个跨进程通信
RemoteViews是怎么做的呢?
1.RemoteViews(已经实现了Parcelable)先通过Binder传递到SystemServer进程.系统根据包名和布局id,进行常见的View加载
2.系统执行一系列View的更新任务(RemoteViews的set方法过来的)注意set方法不是立即执行,而是等待RemoteViews被加载以后才执行
系统并没有通过Binder去支持所有的View操作
· 代价太大,所以就有了Action(Parcelable接口)代表一个View操作
· 每次调用set方法,都会RemoteViews都会添加一个对应的Action,通过Manager提交我们的更新,然后在远程进程中依次进行
下面分析源码(自己打开as一步一步看··)
拿setTextViewText方法举例
源码路径:setTextViewText-setCharSequence-addAction
意思就是:我要用setText方法-我去通知兄弟(反射)加个Action-我把Action加进队列
RemoteViews的apply方法
源码路径:RemoteViews-apply
源码大致内容是,先加载布局文件,加载完后通过performApply方法去执行更新操作
performApply就遍历mActions这个列表去执行每一个Aciont的apply方法(真正操作View的方法)
Action的子类ReflectionAction(反射)的具体实现
在此之前先了解一下apply和reapply作用
· apply:首次加载布局和更新界面使用
· reapply:后续更新界面,无加载布局功能
现在查看ReflectionAction源码,其中的getMethod就是根据方法名来得到反射所需的Method对象
除了ReflectionAction,还有其他Action:TextViewSizeAction、SetOnClickPendingIntent等
关于单击事件
setOnClickPendingIntent用于给普通View设置单击事件,不能给ListView、StackView中的(集合型)View设置单击事件.应该将setPendingIntentTemplate和setOnclickFillIntent组合使用才行
这个小节给出的是一个跨进程更新界面的demo,书P239页
简单介绍一下这个demo,感兴趣再翻书看看