Android桌面小部件与RemoteViews
Android桌面小部件与RemoteViews
标签(空格分隔): Android
一、简介
App Widget是应用程序窗口小部件(Widget)是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。
小部件通过AppWidgetProvide来实现,AppWidgetProvide本质上是一个广播,在小部件的开发过程中会用到RemoteViews,因为小部件在更新界面时无法像在Activity中直接更新View,这是因为它的界面运行在其他进程中,确切来说是在系统的SystemServer进程中。为了跨进程更新界面,RemoteViews中提供一系列的set方法,并且这些方法只是View全部方法的子集,而且它支持的View类型也是有限的。
二、使用小部件
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="wrap_content" android:orientation="vertical" > <TextView android:id="@+id/tv_widget_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击更换内容" android:textSize="16sp" > <Button android:id="@+id/btn_widget_openactivityt" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="点击打开MainActivity" android:textSize="16sp" >LinearLayout>
2.定义小部件配置信息
在res/xml/下新建一个appwidget_provide_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>
其中initialLayout-初始化布局,updatePeriodMillis-自动更新周期。
3.定义小部件的实现类
public class MyWidgetProvider extends AppWidgetProvider { public static final String TAG = "MyWidgetProvider"; public static final String CLICK_ACTION = "com.gaop.HutHelper.action_click"; @Override public void onReceive(Context context, Intent intent) { super.onReceive(context, intent);//不可去掉 if(intent.getAction().equals(CLICK_ACTION)){ RemoteViews remoteviews = new RemoteViews(context.getPackageName(), R.layout.widget); remoteviews.setTextViewText(R.id.tv_widget_content, "已点击"); //获得appwidget管理实例,用于管理appwidget以便进行更新操作 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); //相当于获得所有本程序创建的appwidget ComponentName componentName = new ComponentName(context, MyWidgetProvider.class); //更新appwidget appWidgetManager.updateAppWidget(componentName, remoteviews); } } @Override public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { final int counter = appWidgetIds.length; for (int i = 0; i < counter; i++) { int appWidgetId = appWidgetIds[i]; onWidgetUpdate(context, appWidgetManager, appWidgetId); } } @Override public void onEnabled(Context context) { // Enter relevant functionality for when the first widget is created } @Override public void onDisabled(Context context) { // Enter relevant functionality for when the last widget is disabled } private void onWidgetUpdate(Context context, AppWidgetManager appWidgetManager, int appWidgetId) { RemoteViews remoteViews = new RemoteViews(context.getPackageName(), R.layout.widget); Intent intent = new Intent(context, MyWidgetProvider.class); intent.setAction(CLICK_ACTION); PendingIntent pendingIntentpre = PendingIntent.getBroadcast(context, 0, intent, 0); //打开MainActivity Intent intent2 = new Intent(context,MainActivity.class); PendingIntent pendingIntent2 = PendingIntent.getActivity(context, 0, intent2, 0); remoteViews.setOnClickPendingIntent(R.id.btn_widget_openactivity, pendingIntent2); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); }}
上面实现了一个特别简单的小部件,点击TextView更换内容,点击Button打开MainActiviy。当小部件添加到桌面时,会通过RemoteViews来加载布局文件,点击更换效果则是通过不断更新RemoteViews来实现的。
4.在AndroidManifest.xml中声明小部件
<receiver android:name=".view.CourseWidget"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_provider_info" /> receiver>
当然这是一个特简单的例子,具体开发会复杂很多。
三、小部件常用方法
AppWidgetProvider除了最常用的onUpdate方法,还有onEnable、onDisabled、onDeleted、onReceive。它们都自动的在合适的时间调用(其实就是onReceive方法中的super()所做的:AppWidgetProvider会自动的根据广播的Action通过onReceive方法分发广播)。以下为调用时机:
- onEnable:小部件第一次添加到桌面时调用,添加多次也只会调用一次。
- onUpdate:小部件被添加到桌面或者更新时就会调用一次该方法。
- onDelete:每删除一个小部件,调用一次。
- onDisabled:当最后一个小部件被删除时调用。
- onReceive:广播的内置方法,用于分发具体事件。
以下为onReceive方法的分发过程:
public void More ...onReceive(Context context, Intent intent) { // Protect against rogue update broadcasts (not really a security issue, // just filter bad broacasts out so subclasses are less likely to crash). String action = intent.getAction(); if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); if (appWidgetIds != null && appWidgetIds.length > 0) { this.onUpdate(context, AppWidgetManager.getInstance(context), appWidgetIds); } } } else if (AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID)) { final int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); this.onDeleted(context, new int[] { appWidgetId }); } } else if (AppWidgetManager.ACTION_APPWIDGET_OPTIONS_CHANGED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_ID) && extras.containsKey(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS)) { int appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID); Bundle widgetExtras = extras.getBundle(AppWidgetManager.EXTRA_APPWIDGET_OPTIONS); this.onAppWidgetOptionsChanged(context, AppWidgetManager.getInstance(context), appWidgetId, widgetExtras); } } else if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) { this.onEnabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)) { this.onDisabled(context); } else if (AppWidgetManager.ACTION_APPWIDGET_RESTORED.equals(action)) { Bundle extras = intent.getExtras(); if (extras != null) { int[] oldIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_OLD_IDS); int[] newIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS); if (oldIds != null && oldIds.length > 0) { this.onRestored(context, oldIds, newIds); this.onUpdate(context, AppWidgetManager.getInstance(context), newIds); } } } }
四、PendingIntent
上面的例子中使用到PendingIntent,这其实是一种等待(pending)状态的intent,就是intent将在接下来的某个待定时间发生,而Intent是立即发生。
给RemoteViews添加点击事件就是一个典型的使用场景,因为RemoteViews无法直接向View那个通过setOnClickListener来设置点击事件,PendingIntent通过send和cancel来发送、取消待定的Intent。
它支持三种特定的意图:
1. getActivity(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于startActivity(Intent intent).
2. getService(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于startService(Intent intent).
3. getBroadcast(Context context,int requestCode,Intent intent,int flags) 获取一个PendingIntent,发生时效果相当于sendBroadcast(Intent intent).
其中requestCode为发送方的请求码,一般设为0,同时它也会影响flags的效果。flags常见的类型有:
1. FLAG_ONE_SHOT:当前PendingIntent只能被使用一次,然后就被cancel,如果后续有相同的PendingInent,它们的send方法都会调用失败。
2. FLAG_NO_CREATE:当前PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么get××()方法直接返回null。
3. FLAG_CANCEL_CURRENT:当前PendingIntent如果不存在,那么它们都会被cancel,然后系统会构建一个新的PendingIntent。
4. FLAG_UPDATE_CURRENT:如果当前PendingIntent存在,那么它会被更新。
然后再说下PendingIntent的匹配规则:如果两个PendingIntent的Intent相同而且requestCode也相同,那么两个PendingIntent就是相同的。其中Intent的匹配规则为:如果两个Intent的ComponentName和intent-filter都相同,则它们相同。
五、RemoteViews的内部机制
首先看一下RemoteViews的构造方法: public(String packagename,int layoutid);
其中Remoteviews并不支持全部Layout和View,只支持以下:
layout:
FrameLayout LinearLayout RelativeLayout GridLayout
View:
AnalogClock button Chronometer ImageButton ImageView ProgressBar TextView ViewFlipper ListView GridView StackView AdapterViewFilpper ViewStub
由于RemoteViews没有findViewById的方法 因为它是远程的View即使findViewById我们也不知道远程app的资源文件,它提供了一系列的set方法(例如 setTextViewText,setImageViewResource)来设置内容。
接下来要说的是RemoteViews的工作过程,在小部件中AppWidgetManager是通过Binder与在SystemServer进程中的AppWidgetService通信。可见,小部件的布局文件实际是在SystemServer中加载的。
首先RemoteViews(实现了Parcelable接口)通过Binder传递到SystemServer进程,系统根据RemoteViews中的包名等信息获取该应用的资源。然后通过LayoutInflater加载RemoteViews中的布局文件。接着系统会根据通过我们的set方法提交的内容来更新View,这样小部件就可以使用了。
下面再说一下更新View的具体实现:Android这里提供了一个Action的概念,Action代表一个View操作,同样,它也实现了Parcealable接口,在RemoteViews中有一个mActions的ArrayList对象,每调用一次set方法,RemoteViews就会创建相应的Action添加到mActions中,然后通过Binder将包含mActions的RemoteViews传递到SystemServer进程中。接着调用RemoteViews的apply方法来更新View,这个方法将遍历每个Action并调用它们的apply方法,根据Action的不同来更新不同View的内容。下面根据setTextViewText方法的源码分析下整个流程:
public void setTextViewText(int viewId, CharSequence text) { setCharSequence(viewId, "setText", text); }
public void setCharSequence(int viewId, String methodName, CharSequence value) { //ReflectionAction为一个通用Acton,通过反射调用得到具体的Method对象 addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value)); }
private void addAction(Action a) { if (hasLandscapeAndPortraitLayouts()) { throw new RuntimeException("RemoteViews specifying separate landscape and portrait" + " layouts cannot be modified. Instead, fully configure the landscape and" + " portrait layouts individually before constructing the combined layout."); } if (mActions == null) { mActions = new ArrayList(); } mActions.add(a); // update the memory usage stats a.updateMemoryUsageEstimate(mMemoryUsageCounter); }
public View apply(Context context, ViewGroup parent, OnClickHandler handler) { RemoteViews rvToApply = getRemoteViewsToApply(context); View result = inflateView(context, rvToApply, parent); loadTransitionOverride(context, handler); rvToApply.performApply(result, parent, handler); return result; }
private View inflateView(Context context, RemoteViews rv, ViewGroup parent) { // RemoteViews may be built by an application installed in another // user. So build a context that loads resources from that user but // still returns the current users userId so settings like data / time formats // are loaded without requiring cross user persmissions. final Context contextForResources = getContextForResources(context); Context inflationContext = new ContextWrapper(context) { @Override public Resources getResources() { return contextForResources.getResources(); } @Override public Resources.Theme getTheme() { return contextForResources.getTheme(); } @Override public String getPackageName() { return contextForResources.getPackageName(); } }; LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); // Clone inflater so we load resources from correct context and // we don't add a filter to the static version returned by getSystemService. inflater = inflater.cloneInContext(inflationContext); inflater.setFilter(this); return inflater.inflate(rv.getLayoutId(), parent, false); }
private void performApply(View v, ViewGroup parent, OnClickHandler handler) { if (mActions != null) { handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler; final int count = mActions.size(); for (int i = 0; i < count; i++) { Action a = mActions.get(i); a.apply(v, parent, handler); } } }
更多相关文章
- [译]提升Android应用性能的小贴士
- Android(安卓)APK应用安装原理(1)-解析AndroidManifest原理-Pack
- [置顶] android软键盘弹出,会把原来的界面挤上去的问题 处理方法
- Android(安卓)面试精华题目总结
- android 启动默认的邮件客户端,多附件的问题
- Android设置背景色为透明的两种方法
- Android多module初始化application
- 浅谈Java中Collections.sort对List排序的两种方法
- Python list sort方法的具体使用