标签(空格分隔): Android
App Widget是应用程序窗口小部件(Widget)是微型的应用程序视图,它可以被嵌入到其它应用程序中(比如桌面)并接收周期性的更新。你可以通过一个App Widget Provider来发布一个Widget。
小部件通过AppWidgetProvide来实现,AppWidgetProvide本质上是一个广播,在小部件的开发过程中会用到RemoteViews,因为小部件在更新界面时无法像在Activity中直接更新View,这是因为它的界面运行在其他进程中,确切来说是在系统的SystemServer进程中。为了跨进程更新界面,RemoteViews中提供一系列的set方法,并且这些方法只是View全部方法的子集,而且它支持的View类型也是有限的。
<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>
在res/xml/下新建一个appwidget_provide_info.xml
<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-自动更新周期。
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来实现的。
<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方法分发广播)。以下为调用时机:
以下为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,这其实是一种等待(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的构造方法: 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);
}
}
}