Android--桌面组件开发
一直想写自己的博客,可是纠结了好久都没写,一是因为文笔不好,二是人品不好,所以拖拖拉拉了一段时间,最近想对工作中的一些事情做些总结,所以就借这个契机来了这里;好的,闲话多说,接下来进入主题.
桌面组件:Appwidget,相关类有:
1)AppWidgetService是框架的的核心类,是系统 service之一,它负责widgets的管理工作。加载,删除,定时事件等都需要AppWidgetService的处理。开机自启动的。
2)AppWidgetManager 负责widget视图的实际更新以及相关管理。
3) AppWidgetHost AppWidgetHost 是实际控制widget的地方,大家注意,widget不是一个单独的用户界面程序,他必须寄生在某个程序(activity)中,这样如果程序要支持 widget寄生就要实现AppWidgetHost,桌面程序(Launcher)就实现了这个接口。
4) AppWidgetHostView是真正的View,但它只是一个容器,用来容纳实际的AppWidget的View。这个AppWidget的View是根据RemoteViews的描述来创建。
5) AppWidgetProvider是AppWidget提供者需要实现的接口,它实际上是一个BroadcastReceiver。只不过子类要实现的不再是onReceive,而是转换成了几个新的函数:
1. publicvoid onUpdate(Context context, AppWidgetManagerappWidgetManager, int[] appWidgetIds)
2. public void onDeleted(Context context, int[] appWidgetIds)
3. public void onEnabled(Context context)
4. public void onDisabled(Context context)
这几个函数用来响应AppWidgetService发出的相应的广播消息。
6) RemoteViews并不是一个真正的View,它没有实现View的接口,而只是一个用于描述View的实体。比如:创建View需要的资源ID和各个控件的事件响应方法。RemoteViews会通过进程间通信机制传递给AppWidgetHost。
以上类是相关类,笔者也没有进行仔细研究,有心思深入挖掘者可以仔细研究官方源码.
接下来讲一下创建一个桌面组件的流程,以sdk下例子为例:
1.AppWidgetProviderInfo
描述一个桌面组件的信息,如布局、更新频率、预览图片、最小尺寸等。这都是以一个xml文件定义的;
在res/xml中新建一个xml文件,在此文件中,AppWidgetProviderInfo是以<appwidget-provider>便签存在的,以下是sdk下的例子,appwidget_provider.xml:
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="60dp"
android:minHeight="30dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/appwidget_provider"
android:configure="com.example.android.apis.appwidget.ExampleAppWidgetConfigure"
android:resizeMode="horizontal"
>
</appwidget-provider>
在xml中,第一二项声明最小宽高,这里要说明一下:此项是相对桌面4x4布局而定义的,其每格的宽度用70*N-30换算。 如2x3 则是130 x 180;
第三项声明更新频率,单位为秒,从SDK1.5后,最小更新频率为30分钟,假如设置更新时间小于30分钟则会默认为30分钟,若为0则不自动更新;
第四项指向桌面组件的布局文件
第五项是一个跳转Activity的链接,当你的桌面组件在添加到桌面需要一些设置时(如设置颜色,个性选择等),可使用此项。
第六项为组件拉伸模式,有"horizontal","vertical", "none",对应水平拉伸、垂直拉伸、不可拉伸等。
2 AppWidgetProvider
AppWidgetProvider是BroadcastReceiver的子类,这个类处理App Widget广播。AppWidgetProvider只接收于App Widget有关系的广播,比如App Widget在updated, deleted, enabled, and disabled。当这些广播发生的时候,AppWidgetProvider会调用一下回调方法:
onUpdate(Context, AppWidgetManager, int[])
间隔调用此方法去更新App Widget,间隔时间的设置是在AppWidgetProviderInfo下的updatePeriodMillis属性,同样当用户添加App Widget的时候也被调用。如果你已经声明了一个configuration Activity,用户添加App Widget的时候就不会调用onUpdate,但是在随后的更新中依然会被调用。
onDeleted(Context, int[])
当App Widget 从App Widget host 中删除的时候调用。
onEnabled(Context)
当App Widget第一次创建的时候调用。比如,当用户增加两个同样的App Widget时候,这个方法只在第一次去调用。如果你需要打开一个新的数据库或者其他的设置,而这在所有的App Widgets只需要设置一次的情况下,这个是最好的地方去实现它们。
onDisabled(Context)
当App Widget的最后一个实例从App Widget host中被删除的时候调用。这里可以做一些在onEnabled(Context)中相反的操作,比如删除临时数据库。
onReceive(Context, Intent)
每一个广播的产生都会调用此方法,而且是在上面方法之前被调用。通常不需要实现此方法。
在AppWidgetProvider中最重要的callback就是onUpdated(),如果你的App Widget接收用户交互事件,就需要在这个callback里面进行处理。
以下为sdk下的例子:
publicclass ExampleAppWidgetProvider extendsAppWidgetProvider {
// log tag
privatestaticfinal String TAG = "ExampleAppWidgetProvider";
@Override
publicvoid onUpdate(Context context, AppWidgetManager appWidgetManager, int[]appWidgetIds) {
Log.d(TAG, "onUpdate");
// For each widget that needs anupdate, get the text that we should display:
// - Create a RemoteViews object for it
// - Set the text in the RemoteViews object
// - Tell the AppWidgetManager to show thatviews object for the widget.
finalint N = appWidgetIds.length;
for (int i=0; i<N; i++) {
int appWidgetId = appWidgetIds[i];
String titlePrefix =ExampleAppWidgetConfigure.loadTitlePref(context, appWidgetId);
updateAppWidget(context,appWidgetManager, appWidgetId, titlePrefix);
}
}
@Override
publicvoid onDeleted(Context context, int[] appWidgetIds) {
Log.d(TAG, "onDeleted");
// When the user deletes thewidget, delete the preference associated with it.
finalint N = appWidgetIds.length;
for (int i=0; i<N; i++) {
ExampleAppWidgetConfigure.deleteTitlePref(context,appWidgetIds[i]);
}
}
@Override
publicvoid onEnabled(Context context) {
Log.d(TAG, "onEnabled");
// When the first widget iscreated, register for the TIMEZONE_CHANGED and TIME_CHANGED
// broadcasts. We don't want to be listening for these ifnobody has our widget active.
// This setting is sticky acrossreboots, but that doesn't matter, because this will
// be called after boot if thereis a widget instance for this provider.
PackageManager pm =context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.example.android.apis", ".appwidget.ExampleBroadcastReceiver"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
@Override
publicvoid onDisabled(Context context) {
// When the first widget iscreated, stop listening for the TIMEZONE_CHANGED and
// TIME_CHANGED broadcasts.
Log.d(TAG, "onDisabled");
PackageManager pm =context.getPackageManager();
pm.setComponentEnabledSetting(
new ComponentName("com.example.android.apis", ".appwidget.ExampleBroadcastReceiver"),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
staticvoid updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId, String titlePrefix) {
Log.d(TAG, "updateAppWidgetappWidgetId=" + appWidgetId + " titlePrefix=" + titlePrefix);
// Getting the string this wayallows the string to be localized. Theformat
// string is filled in usingjava.util.Formatter-style format strings.
CharSequence text = context.getString(R.string.appwidget_text_format,
ExampleAppWidgetConfigure.loadTitlePref(context,appWidgetId),
"0x" + Long.toHexString(SystemClock.elapsedRealtime()));
// Construct the RemoteViewsobject. It takes the package name (inour case, it's our
// package, but it needs thisbecause on the other side it's the widget host inflating
// the layout from our package).
RemoteViews views = new RemoteViews(context.getPackageName(),R.layout.appwidget_provider);
views.setTextViewText(R.id.appwidget_text,text);
// Tell the widget manager
appWidgetManager.updateAppWidget(appWidgetId, views);
}
}
3.layout 文件
这里说明:
目前桌面组件支持的布局和控件为:FrameLayout、LinearLayout、RelativeLayout
AnalogClock、Button、Chronmeter、ImageButton、ImageView、ProgressBar、TextView ;若对安卓源码又仔细研究的攻城狮会发现,这些类在类的前面都会有一个标签描述,如TextView:
@RemoteView
public class TextView extends View
若想自身支持一些复杂的View,也可将自定义的View加入framework下,并在类名前声明标签@RemotView,即可在桌面布局中引用,如需此步操作,建议在framework/base/core/android/widget下添加,如非特殊需要,最好添加自己的类,而不是直接改变Google定义的类。
以下为sdk下的例子,res/layout/appwideget_provicer.xml:
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/appwidget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#ffff00ff"
android:textColor="#ff000000"
/>
4.AppWidgetconfigure Activity
这个Activity将通过App Widget自动启动,用户可以给App Widget设置有用的参数,比如App Widget的颜色、大小、更新时间或者其他的属性。
在AndroidManifes.xml中定义这个Activity和一般定义Activity基本没有区别,App Widget host启动这个Activity需要一个Action,所以:
<activity android:name=".ExampleAppWidgetConfigure">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
同样这个Activity必须在AppWidgetProviderInfo XML文件中定义android:configure。
值得注意的是AppWidget host调用configurationActivity,configurationActivity必须要返回一个结果(必须包含AppWidget ID)saved in theIntent extras as EXTRA_APPWIDGET_ID。
sdk例子,ExampleAppWidgetConfigure.java文件:
publicclass ExampleAppWidgetConfigure extends Activity {
staticfinal String TAG = "ExampleAppWidgetConfigure";
privatestaticfinal String PREFS_NAME
= "com.example.android.apis.appwidget.ExampleAppWidgetProvider";
privatestaticfinal String PREF_PREFIX_KEY = "prefix_";
intmAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
EditText mAppWidgetPrefix;
public ExampleAppWidgetConfigure() {
super();
}
@Override
publicvoid onCreate(Bundle icicle) {
super.onCreate(icicle);
// Set the result to CANCELED. This will cause the widget host to cancel
// out of the widget placement if theypress the back button.
setResult(RESULT_CANCELED);
// Set the view layout resource to use.
setContentView(R.layout.appwidget_configure);
// Find the EditText
mAppWidgetPrefix = (EditText)findViewById(R.id.appwidget_prefix);
// Bind the action for the save button.
findViewById(R.id.save_button).setOnClickListener(mOnClickListener);
// Find the widget id from the intent.
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras != null) {
mAppWidgetId = extras.getInt(
AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
}
// If they gave us an intent without thewidget id, just bail.
if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish();
}
mAppWidgetPrefix.setText(loadTitlePref(ExampleAppWidgetConfigure.this, mAppWidgetId));
}
View.OnClickListener mOnClickListener = new View.OnClickListener() {
publicvoid onClick(View v) {
final Context context = ExampleAppWidgetConfigure.this;
// When the button is clicked, save thestring in our prefs and return that they
// clicked OK.
String titlePrefix = mAppWidgetPrefix.getText().toString();
saveTitlePref(context, mAppWidgetId, titlePrefix);
// Push widget update to surface with newlyset prefix
AppWidgetManager appWidgetManager =AppWidgetManager.getInstance(context);
ExampleAppWidgetProvider.updateAppWidget(context,appWidgetManager,
mAppWidgetId, titlePrefix);
// Make sure we pass back the originalappWidgetId
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK, resultValue);
finish();
}
};
// Write the prefix to the SharedPreferences object forthis widget
staticvoid saveTitlePref(Context context, int appWidgetId, String text) {
SharedPreferences.Editor prefs =context.getSharedPreferences(PREFS_NAME,0).edit();
prefs.putString(PREF_PREFIX_KEY + appWidgetId, text);
prefs.commit();
}
// Read the prefix from the SharedPreferences object forthis widget.
// If there is no preference saved, get the default froma resource
static String loadTitlePref(Context context, int appWidgetId) {
SharedPreferences prefs =context.getSharedPreferences(PREFS_NAME, 0);
String prefix = prefs.getString(PREF_PREFIX_KEY + appWidgetId, null);
if (prefix != null) {
return prefix;
} else {
return context.getString(R.string.appwidget_prefix_default);
}
}
staticvoid deleteTitlePref(Context context, int appWidgetId) {
}
staticvoid loadAllTitlePrefs(Context context, ArrayList<Integer> appWidgetIds,
ArrayList<String> texts) {
}
}
5.给桌面组件添加点击事件
桌面组件的点击事件使用的是RemoteView为载体,且仅仅可以使用PendingIntent作为点击反应的意图。
例子:RemoteViews views = new RemoteViews(context.getPackageName(),
R.layout.appwidget);
views.setOnClickPendingIntent(R.id.button,
PendingIntent.getActivity(context,0,
new Intent(context, xxx.class), 0));
int[] appWidgetIds = intent.getIntArrayExtra(
AppWidgetManager.EXTRA_APPWIDGET_IDS);
AppWidgetManager gm = AppWidgetManager.getInstance(context);
gm.updateAppWidget(appWidgetIds,views);
第一,需使用RemoteViews作为载体;
第二,完毕必须调用AppWidgetManager的updateAppWidget使之生效;
这里需注意的是,上面例子是针对跳转路径统一且不需返回每个组件对应特殊值的时候应用的,假如需要对每个组件,或者组件内有多个部位需要不同的点击事件响应,因对pendingintent进行不同的赋值或标志位以区分,各位可仔细研究PendingIntent。
点击事件还需要注意的是,在你对桌面组件进行更新后,一定要在此对pendingintent进行上述操作,否则可能会失去点击响应。
6.RemoteViews函数的引用
各位开发过桌面组件的同事都会知道,RemoteViews对象可以引用的函数是非常有限的,如setTextViewText,setImageViewResource等(只述说针对view操作的函数),其实,RemoteViews的函数扩张性是非常好的,因为他拥有如下函数setxxx(int,String,xxx);
其中,第一个参数是此函数要作用的View的id,第二个参数是这个View对象所拥有的函数,第三个参数是这个函数所带的参数。
如一个TextView要引用setText函数,设置它显示"Hello",它的ID为textView.
则如此应用:RemoteViews views = newRemoteViews(context.getPackageName(),
R.layout.appwidget);
views.setCharSequence (R.id.textView,"setText","Hello");
int appWidgetId = intent.getIntArrayExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID);
AppWidgetManager gm =AppWidgetManager.getInstance(context);
gm.updateAppWidget(appWidgetId,views);
此外,RemoteViews可如此引用的函数还有:
setBitmap(intviewId, String methodName, Bitmap value)
setBundle(intviewId, String methodName, Bundle value)
setIntent(intviewId, String methodName, Intent value)
setDouble(intviewId, String methodName, double value)...太多了,就不一一列举了。
但是我们往往会发现许多函数是无法如此应用的,如一个TextView的setEnabled(boolean enabled)函数,若应用Remoteviews的setBoolean(R.id.textView,"setEnabled",false)就会报错而上述应用views.set CharSequence (R.id.textView,"setText","Hello")就可以呢?
让我们一起去源码看一看setText和setEnable有什么区别:
@android.view.RemotableViewMethod
public final void setText(CharSequence text) {
setText(text, mBufferType);
}
@Override
public void setEnabled(boolean enabled) {
if (enabled == isEnabled()) {
return;
}
if (!enabled) {
// Hide the soft input if the currently active TextView is disabled
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null && imm.isActive(this)) {
imm.hideSoftInputFromWindow(getWindowToken(), 0);
}
}
super.setEnabled(enabled);
prepareCursorControllers();
if (enabled) {
// Make sure IME is updated with current editor info.
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
}
}
怎样,是不是一看就明白了,没错就是因为方法前声明@android.view.RemoteableViewMethod,就可以由Remoteviews引用啦。
好了,写到这里,大家有什么不明白的可以来找我,我们一起探讨一下(本人也是菜鸟,欢迎提问)。
本文借鉴:http://blog.csdn.net/sadamdiyi/article/details/8245818
p.s 第一次写微博,若有错误,比为巧合,不要打我