本文背景:前两天遇到一个问题,在原生的图库中应该都有这个想象(可以在谷歌儿子中试一下)。
在桌面添加图库的widget,会提示选择图片,这时你随便选择一张。然后在重复添加一个widget,并且选择同一张图片。这时你点击前面添加的widget,你会发现点击没有反应,进入不到图库。第二个就可以进入。
经过一天的研究代码和查找,终于找到了问题所在。下面就给大家简单说一下图库的widget生成。另外不写下来,自己可能以后就忘了,所以按照我自己的思路就写一下。
在图库的代码中专门有一个文件夹存放widget相关的java文件。com.android.gallery3d.gadget包下。
LocalPhotoSource.java
MediaSetSource.java
PhotoAppWidgetProvider.java
WidgetClickHandler.java
WidgetConfigure.java
WidgetDatabaseHelper.java
WidgetService.java
WidgetSource.java
WidgetTypeChooser.java
WidgetUtils.java
共有上面这几个文件。
一、1,用PhotoAppWidgetProvider继承了widget的必备类AppWidgetProvider。
public class PhotoAppWidgetProvider extends AppWidgetProvider{}
2,咱们看一下Gallery的AndroidManifest.xml文件。它是这样声明PhotoAppWidgetProvider。
<receiver android:name="com.android.gallery3d.gadget.PhotoAppWidgetProvider"
android:label="@string/appwidget_title">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data android:name="android.appwidget.provider"
android:resource="@xml/widget_info" />
</receiver>
这个声明也是生成widget的必备声明,只有一个update action,所以在类中只重写了onUpdate()方法。咱们看一下怎么重写的。
@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager, int[] appWidgetIds) {
if (ApiHelper.HAS_REMOTE_VIEWS_SERVICE) {
// migrate gallery widgets from pre-JB releases to JB due to bucket ID change
GalleryWidgetMigrator.migrateGalleryWidgets(context);
}
WidgetDatabaseHelper helper = new WidgetDatabaseHelper(context);
try {
for (int id : appWidgetIds) {
Entry entry = helper.getEntry(id);
if (entry != null) {
RemoteViews views = buildWidget(context, id, entry);
appWidgetManager.updateAppWidget(id, views);
} else {
Log.e(TAG, "cannot load widget: " + id);
}
}
} finally {
helper.close();
}
super.onUpdate(context, appWidgetManager, appWidgetIds);
} static RemoteViews buildWidget(Context context, int id, Entry entry) {
switch (entry.type) {
case WidgetDatabaseHelper.TYPE_ALBUM:
case WidgetDatabaseHelper.TYPE_SHUFFLE:
return buildStackWidget(context, id, entry);
case WidgetDatabaseHelper.TYPE_SINGLE_PHOTO:
return buildFrameWidget(context, id, entry);
}
throw new RuntimeException("invalid type - " + entry.type);
}
大家可以看到上面还用到了数据库WidgetDatabaseHelper类,它的作用是对widget的内容的记录,记住widget展示的图片的uri,相册的uri,和相册类型。看一下它的数据表。
private static final String FIELD_APPWIDGET_ID = "appWidgetId";
private static final String FIELD_IMAGE_URI = "imageUri";
private static final String FIELD_PHOTO_BLOB = "photoBlob";
private static final String FIELD_WIDGET_TYPE = "widgetType";
private static final String FIELD_ALBUM_PATH = "albumPath";
private static final String FIELD_RELATIVE_PATH = "relativePath";
这类变量就是表中的列名,从名字上可以看出各列的意义。上面的onUpdate的主要作用是更新widget的信息。
而从开始创建widget时,先走的并不是这个方法,而是另外的一条路。
3,上面的manifest文件咱们看到android:resource="@xml/widget_info"属性。看一下这个xml文件。也是生成widget的必需品。
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="@dimen/appwidget_width"
android:minHeight="@dimen/appwidget_height"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/preview"
android:initialLayout="@layout/appwidget_main"
android:configure="com.android.gallery3d.gadget.WidgetConfigure"/>
这个文件的作用是widget的属性。minWidth:widget的宽度,minHeight:高度,updatePeriodMillis:更新的时间周期,previewImage:widget的图标,initialLayout:初始widget的布局,configure: 如果需要在启动前先启动一个Activity进行设置,在这里给出Activity的完整类名。
所以在创建时,会先走WidgetConfigure这个activity。看一下他的代码(稍后我会把代码附加到文章中)。
public class WidgetConfigure extends Activity {},在oncreate它有调用一个选择的activity,进行选择widget类型,然后设置widget。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != RESULT_OK) {
setResult(resultCode, new Intent().putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
finish();
return;
}
if (requestCode == REQUEST_WIDGET_TYPE) {
setWidgetType(data);
} else if (requestCode == REQUEST_CHOOSE_ALBUM) {
setChoosenAlbum(data);
} else if (requestCode == REQUEST_GET_PHOTO) {
setChoosenPhoto(data);
} else if (requestCode == REQUEST_CROP_IMAGE) {
setPhotoWidget(data);
} else {
throw new AssertionError("unknown request: " + requestCode);
}
}
选择单个图片时,会走setPhotoWidget(data);这个方法:
private void setPhotoWidget(Intent data) {
// Store the cropped photo in our database
Bitmap bitmap = (Bitmap) data.getParcelableExtra("data");
WidgetDatabaseHelper helper = new WidgetDatabaseHelper(this);
try {
helper.setPhoto(mAppWidgetId, mPickedItem, bitmap);
updateWidgetAndFinish(helper.getEntry(mAppWidgetId));
} finally {
helper.close();
}
}
这个方法里,进行了数据的保存和调用设置widget。
private void updateWidgetAndFinish(WidgetDatabaseHelper.Entry entry) {
AppWidgetManager manager = AppWidgetManager.getInstance(this);
RemoteViews views = PhotoAppWidgetProvider.buildWidget(this, mAppWidgetId, entry);
manager.updateAppWidget(mAppWidgetId, views);
setResult(RESULT_OK, new Intent().putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId));
finish();
}
这个方法里,就调用了上面的PhotoAppWidgetProvider中的方法。创建RemoteViews方法。最终会调用buildFrameWidget(context, id, entry);这个方法。
static RemoteViews buildFrameWidget(Context context, int appWidgetId, Entry entry) {
RemoteViews views = new RemoteViews(
context.getPackageName(), R.layout.photo_frame);
try {
byte[] data = entry.imageData;
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
views.setImageViewBitmap(R.id.photo, bitmap);
} catch (Throwable t) {
Log.w(TAG, "cannot load widget image: " + appWidgetId, t);
}
if (entry.imageUri != null) {
try {
Uri uri = Uri.parse(entry.imageUri);
Intent clickIntent = new Intent(context, WidgetClickHandler.class)
.setData(uri);
PendingIntent pendingClickIntent = PendingIntent.getActivity(context, 0,
clickIntent, PendingIntent.FLAG_CANCEL_CURRENT);
views.setOnClickPendingIntent(R.id.photo, pendingClickIntent);
} catch (Throwable t) {
Log.w(TAG, "cannot load widget uri: " + appWidgetId, t);
}
}
return views;
}
这个方法就是设置的Remoteview的方法。文章开头的问题就是出现在这个方法里面。设置view的PendingIntent时的不同标记。看红色的字体,我给大家看一下他的另外几个标记:
public final class PendingIntent implements Parcelable {
/**
* Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
* {@link #getService}: this
* PendingIntent can only be used once. If set, after
* {@link #send()} is called on it, it will be automatically
* canceled for you and any future attempt to send through it will fail.
*/
public static final int FLAG_ONE_SHOT = 1<<30; //这个标记是此设置的PendingIntent只能点击一次。也就是点击一次后,就会失去作用
/**
* Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
* {@link #getService}: if the described PendingIntent does not already
* exist, then simply return null instead of creating it.
*/
public static final int FLAG_NO_CREATE = 1<<29;//这个标记是此设置的PendingIntent一次都不能点击,不会创建intent。
/**
* Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
* {@link #getService}: if the described PendingIntent already exists,
* the current one is canceled before generating a new one. You can use
* this to retrieve a new PendingIntent when you are only changing the
* extra data in the Intent; by canceling the previous pending intent,
* this ensures that only entities given the new data will be able to
* launch it. If this assurance is not an issue, consider
* {@link #FLAG_UPDATE_CURRENT}.
*/
public static final int FLAG_CANCEL_CURRENT = 1<<28;//这个标记是此设置的PendingIntent,如果生成的相同Intent,那么最后一个管用,前面的都会被cancel掉。
/**
* Flag for use with {@link #getActivity}, {@link #getBroadcast}, and
* {@link #getService}: if the described PendingIntent already exists,
* then keep it but its replace its extra data with what is in this new
* Intent. This can be used if you are creating intents where only the
* extras change, and don't care that any entities that received your
* previous PendingIntent will be able to launch it with your new
* extras even if they are not explicitly given to it.
*/
public static final int FLAG_UPDATE_CURRENT = 1<<27;//这个标记是此设置的PendingIntent,生成的intent全部管用。
}
所以最开始遇到的问题,就是这个标记搞得鬼。只要把红色标记的部分改为FLAG_UPDATE_CURRENT这个就可以解决掉次问题。
此文章全部是自己的观念和理解。有什么不对的地方,请大家多多指正。代码全部都是谷歌的gallery2的原生代码。