本文背景:前两天遇到一个问题,在原生的图库中应该都有这个想象(可以在谷歌儿子中试一下)。

             在桌面添加图库的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的原生代码。