1. widget的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/tv_list_widget_title"
android:layout_width="match_parent"
android:layout_height="45dp"
android:gravity="center"
android:textAppearance="?android:attr/textAppearanceMedium"
/>
<!-- 通过代码控制,在ListView为空时,可以显示TextView -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
></ListView>
<TextView
android:id="@+id/list_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="No Items Available"
android:textSize="22sp"
/>
</FrameLayout>
</LinearLayout>
在widget中会使用一个ListView。所以,还需要提供Listview的Item布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:orientation="vertical"
android:id="@+id/ll_widget_item"
android:paddingLeft="10dp"
android:gravity="center_vertical"
>
<TextView
android:id="@+id/line1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/line2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</LinearLayout>
需要注意的是,虽然item的布局文件与android.R.layout.simple_list_item_2很相似,但是如果ListView是用在widget中,最好不要使用系统提供的那些布局文件,因为在形成widget的视图时要先将其包装为RemoteViews,而RemoteViews中支持的视图组件是非常有限的,不能保证系统写的布局文件中用到的视图组件都可以被支持。
2. 提供一个元数据文件,里面声明该widget会需要一个配置界面(Configure Activity)
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="110dp"
android:updatePeriodMillis="86400000"
android:initialLayout="@layout/list_widget_layout"
android:configure="com.example.widgettest.ListWidgetConfigureActivity"
android:resizeMode="horizontal|vertical"
/>
configure属性声明了widget需要一个ConfigureActivity。在把widget拖放到桌面松手时的一刹那,会优先显示作为configure activity的ListWidgetConfigureActivity,当ListWidgetConfigureActivity以setResult(result_code,data)返回时,widget才会出现在界面上。
3. 修改AndroidManifest文件,声明为widget服务的AppWidgetProvider,声明作为widget的configure activity的ListWidgetConfigureActivity。同时,为widget中的ListView提供数据时,是不能使用Adapter的,需要利用RemoteViewsService,并提供一个RemoteViewsFactory实例来填充ListView的数据。
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.widgettest.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ListWidgetConfigureActivity"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
</activity>
<receiver
android:name=".ListAppWidget"
>
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/list_appwidget"
/>
</receiver>
<service
android:name=".ListWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
></service>
<service
android:name=".MediaService"
></service>
</application>
这里特别需要注意的是,凡是为widget提供帮助的receiver,activity和service,都需要添加一些额外的说明。
receiver标签中需要添加<intent-filter>和<meta-data>标签。名字是ListAppWidget的AppWidgetProvider类为该widget(s)服务。一个widget可以被反复拖拽到界面上,每拖拽一个在界面上显示,就意味着AppWidgetProvider需要多一个widget进行管理和通知。因此,一般情况下,AppWidgetProvider内的代码都需要以widget数组的形式来设计代码。
作为configure activity的activity中,必须要设置
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
</intent-filter>
否则configure activity不会显示,app widget也不能创建
作为RemoteViewsService,service标签里面必须要声明 BIND_REMOTEVIEWS许可
另外,例子中还声明了一个<service>,这个service是用来读取手机媒体库中的图片和视频的
4. 新建ListWidgetConfigureActivity
首先是该Activity的布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/tv_configure_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:text="Select Media Type:"
/>
<RadioGroup
android:id="@+id/rg_configure_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_configure_title"
android:orientation="vertical"
>
<RadioButton
android:id="@+id/rb_mode_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="IMAGE"
/>
<RadioButton
android:id="@+id/rb_mode_media"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="VIDEOS"
/>
</RadioGroup>
<Button
android:id="@+id/btn_configure"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Add Widget"
android:layout_alignParentBottom="true"
/>
</RelativeLayout>
然后是Activity的代码:
public class ListWidgetConfigureActivity extends Activity {
private int mAppWidgetId;
@ViewInject(R.id.rg_configure_mode)
private RadioGroup mModeGroup;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.configure);
ViewUtils.inject(this);
//该Activity是被Intent启动的,
//通过该Intent可以获得当前正要被创建的widget的Id
mAppWidgetId = getIntent().getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
//如果用户在这个节目没有进行任何选择就直接按back退出该Activity的话
//通过setResult(RESULT_CANCELED),widget就不会被创建
setResult(RESULT_CANCELED);
}
@OnClick({ R.id.btn_configure })
public void onAddClick(View v) {
//通过当前widget的Id
//为当前要被创建的widget设定一个Preference文件保存设置(显示图片还是视频)
SharedPreferences sp = getSharedPreferences(
String.valueOf(mAppWidgetId), MODE_PRIVATE);
Editor editor = sp.edit();
//将widget包装成一个RemoteViews
RemoteViews rv = new RemoteViews(getPackageName(),
R.layout.list_widget_layout);
switch (mModeGroup.getCheckedRadioButtonId()) {
case R.id.rb_mode_image:
editor.putString(ListWidgetService.KEY_MODE,
ListWidgetService.MODE_IMAGE);
editor.commit();
rv.setTextViewText(R.id.tv_configure_title, "Image Collection");
break;
case R.id.rb_mode_media:
editor.putString(ListWidgetService.KEY_MODE,
ListWidgetService.MODE_MEDIA);
editor.commit();
rv.setTextViewText(R.id.tv_configure_title, "Media Collection");
break;
default:
Toast.makeText(this, "please select a Media Type", 1).show();
return;
}
Intent intent=new Intent(this, ListWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
//这里就是为widget中的ListView设置Adapter
//需要提供被绑定的widget,该widget中需要被填充的ListView和为这个ListView服务的
//可以启动RemoteViewsService的Intent
rv.setRemoteAdapter(mAppWidgetId, R.id.list, intent);
//如果ListView的数据为空,用什么视图组件来代替显示
rv.setEmptyView(R.id.list, R.id.list_empty);
//设置一个PendingItent,在widget的ListView中进行点击的时候触发
Intent viewIntent=new Intent(Intent.ACTION_VIEW);
PendingIntent pi=PendingIntent.getActivity(this, 0, viewIntent, 0);
//setPendingIntentTemplate与setOnItemClickListener是类似的
//这里使用setPendingIntentTemplate直接为ListView中的每一个Item都设置上了PendingIntent
rv.setPendingIntentTemplate(R.id.list, pi);
//利用AppWidgetManager来更新RemoteViews
AppWidgetManager manager=AppWidgetManager.getInstance(this);
manager.updateAppWidget(mAppWidgetId, rv);
//一切都设置完毕后,调用setResult(RESULT_OK,data);并把自己关闭
//widget随即会出现在桌面上
Intent data=new Intent();
data.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
setResult(RESULT_OK,data);
finish();
}
}
5. AppWidgetProvider用来接收widget的相关广播
public class ListAppWidget extends AppWidgetProvider {
/**
* widget中的内容需要更新的时候,会回调这个方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
AppWidgetManager manager = AppWidgetManager.getInstance(context);
//因为同一个widget可以被反复拖拽到页面,每拖拽一次就生成一个新的widget,会有一个新的id
//所以,AppWidgetProvider大多数情况下都应该是面向数组进行操作
for (int i = 0; i < appWidgetIds.length; i++) {
Intent intent = new Intent(context, ListWidgetService.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetIds[i]);
intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
RemoteViews rv = new RemoteViews(context.getPackageName(),
R.layout.list_widget_layout);
//获取以widget的Id为名称的SharedPreferences文件
SharedPreferences sp = context.getSharedPreferences(
String.valueOf(appWidgetIds[i]), context.MODE_PRIVATE);
String mode = sp.getString(ListWidgetService.KEY_MODE,
ListWidgetService.MODE_IMAGE);
if (ListWidgetService.MODE_MEDIA.equals(mode)) {
rv.setTextViewText(R.id.tv_list_widget_title, "视频列表");
} else {
rv.setTextViewText(R.id.tv_list_widget_title, "图片列表");
}
//每一个widget都可以从ListWidgetService中获得数据
rv.setRemoteAdapter(appWidgetIds[i], R.id.list, intent);
//如果ListView中没有数据填充,那么就显示TextView
rv.setEmptyView(R.id.list, R.id.list_empty);
Intent viewIntent = new Intent(Intent.ACTION_VIEW);
PendingIntent pi = PendingIntent.getActivity(context, 0,
viewIntent, 0);
rv.setPendingIntentTemplate(R.id.list, pi);
manager.updateAppWidget(appWidgetIds[i], rv);
}
}
/**
* 与该AppWidgetProvider绑定的若干个widget中,只要有一个widget被删除了
* AppWidgetProvider的 onDeleted会被回调
*/
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
for(int i=0;i<appWidgetIds.length;i++){
SharedPreferences sp=context.getSharedPreferences(String.valueOf(appWidgetIds[i]), context.MODE_PRIVATE);
Editor editor=sp.edit();
editor.clear();
editor.commit();
}
}
/**
* 第一个与该AppWidgetProvider绑定的widget被创建的时候,该方法会被回调
*/
@Override
public void onEnabled(Context context) {
//启动用来读取媒体库中读取图片和视频的服务
context.startService(new Intent(context, MediaService.class));
}
/**
* 最后一个与该AppWidgetProvider绑定的widget被删除的时候,该方法会被回调
*/
@Override
public void onDisabled(Context context) {
//停止用来读取媒体库中读取图片和视频的服务
context.stopService(new Intent(context, MediaService.class));
}
}
6. 为widget中的ListView提供数据填充的RemoteViewsService
public class ListWidgetService extends RemoteViewsService{
public static final String KEY_MODE="mode";
public static final String MODE_IMAGE="image";
public static final String MODE_MEDIA="media";
//实现RemoteViewService时要实现的抽象方法,获得一个RemoteViewsFactory实例
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new ListRemoteViewFactory(this,intent);
}
/**
* 内部类,实现RemoteViewsFatory接口
* 这个RemoteViewsFactory就相当于BaseAdatper
*/
private class ListRemoteViewFactory implements RemoteViewsFactory{
private Context mContext;
private int mAppWidgetId;
private Cursor mDataCursor;
public ListRemoteViewFactory(Context context,Intent intent) {
mContext=context;
mAppWidgetId=intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
}
@Override
public void onCreate() {
//获得一个widget对应的SharedPerferences文件
SharedPreferences sp=mContext.getSharedPreferences(String.valueOf(mAppWidgetId), Context.MODE_PRIVATE);
//获得该widget的Mode类型
String mode=sp.getString(KEY_MODE, MODE_IMAGE);
//如果mode是视频类型,则去query系统的视频数据表获得对应的cursor
if(MODE_MEDIA.equals(mode)){
String[] projection={
MediaStore.Video.Media.TITLE,
MediaStore.Video.Media.DATE_TAKEN,
MediaStore.Video.Media.DATA
};
mDataCursor = MediaStore.Images.Media.query(getContentResolver(), MediaStore.Video.Media.EXTERNAL_CONTENT_URI, projection);
}else{
//如果mode是图片类型,则去query系统的图片数据表获得对应的cursor
String[] projection={
MediaStore.Images.Media.TITLE,
MediaStore.Images.Media.DATE_TAKEN,
MediaStore.Images.Media.DATA
};
mDataCursor = MediaStore.Images.Media.query(getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection);
}
}
@Override
public void onDataSetChanged() {
mDataCursor.requery();
}
@Override
public void onDestroy() {
mDataCursor.close();
mDataCursor=null;
}
@Override
public int getCount() {
return mDataCursor.getCount();
}
/**
* 与BaseAdapter中的getView方法作用是一样的
*/
@Override
public RemoteViews getViewAt(int position) {
mDataCursor.moveToPosition(position);
//widget中的ListView中的Item也要包装为RemoteView
//该代码与BaseAdapter的getView中获取Item布局的代码类似
RemoteViews rv=new RemoteViews(getPackageName(),R.layout.list_widget_item_layout);
rv.setTextViewText(R.id.line1, mDataCursor.getString(0));
//DateFormat是一个安卓提供的工具类,可以省去用SimpleDateFormat去parse的步骤了
rv.setTextViewText(R.id.line2, DateFormat.format("MM/dd/yyyy", mDataCursor.getLong(1)));
SharedPreferences sp=mContext.getSharedPreferences(String.valueOf(mAppWidgetId), MODE_PRIVATE);
String mode=sp.getString(KEY_MODE, MODE_IMAGE);
String type;
if(MODE_MEDIA.equals(mode)){
type="video/*";
}else{
type="image/*";
}
Uri data=Uri.fromFile(new File(mDataCursor.getString(2)));
Intent intent=new Intent();
intent.setDataAndType(data, type);
//setOnClickFillInIntent与 setPendingIntentTemplate可以联合使用
//当在widgets中使用集合(比如说ListView, StackView等等),为一个个单独的Item设置PendingIntents是非常麻烦的,
//通过设置PendingIntentsTemplate可以一次性给集合里面的Item设置相同的点击时动作,
//如果希望某一个Item点击后的动作有所不同,就为这个Item单独使用fillInIntent来设置它点击后执行的Intent。
rv.setOnClickFillInIntent(R.id.ll_widget_item, intent);
return rv;
}
/**
* 在getView方法执行获得View的过程中,该方法的返回值会作为等待加载画面一直显示
* 当getView方法返回时,等待加载画面会自动消失
*/
@Override
public RemoteViews getLoadingView() {
return null;
}
/**
* 该方法与BaseAdapter中的getViewTypeCount的意思是一样的
*/
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public boolean hasStableIds() {
return false;
}
}
}
7. 最后是一个服务类型的service MediaService。它的启动和中止是在AppWidgetProvider的onEnable和onDisable中进行的。这个服务的作用就是当你添加或者删除了图片或视频的时候,这个MediaService上有一个ContentObserver,会即时把这个变化反应到widget的Listview上。及可以实时保持widget的ListView中的内容与手机媒体库中的内容保持一致
public class MediaService extends Service{
private ContentObserver mMediaStoreObserver;
@Override
public void onCreate() {
super.onCreate();
mMediaStoreObserver=new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
Context _context=MediaService.this;
AppWidgetManager manager=AppWidgetManager.getInstance(_context);
ComponentName provider=new ComponentName(_context, ListAppWidget.class);
//获得,所有利用AppWidgetProvider作为广播接收器的那些widget的id
int[] ids=manager.getAppWidgetIds(provider);
//这样当有数据发生变化时,这种变化会反映到所有桌面上的widget的ListView列表中
manager.notifyAppWidgetViewDataChanged(ids, R.id.list);
}
};
getContentResolver().registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
true, mMediaStoreObserver);
getContentResolver().registerContentObserver(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
true, mMediaStoreObserver);
}
@Override
public void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(mMediaStoreObserver);
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
所有的代码和配置文件都书写完毕。