您的位置:首页 > 移动开发 > Android开发

Android桌面小部件与RemoteViews

2016-11-19 00:11 232 查看

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, MyWi
4000
dgetProvider.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<Action>();
}
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);
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息