如何创建具有配置活动的应用小部件,并首次对其进行更新?

Posted

技术标签:

【中文标题】如何创建具有配置活动的应用小部件,并首次对其进行更新?【英文标题】:How to create an app widget with a configuration activity, and update it for the first time? 【发布时间】:2012-11-21 13:32:36 【问题描述】:

这快把我逼疯了。即使有推荐的做法,我也不知道如何从配置活动中更新应用小部件。为什么在创建应用小部件时不调用 update 方法超出了我的理解。

我想要的:一个包含项目集合(带有列表视图)的应用小部件。但是用户需要选择一些东西,所以我需要一个配置活动。

配置活动是ListActivity:

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ChecksWidgetConfigureActivity extends SherlockListActivity 
    private List<Long> mRowIDs;
    int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
    private BaseAdapter mAdapter;

    @Override
    protected void onCreate(final Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        setResult(RESULT_CANCELED);
        setContentView(R.layout.checks_widget_configure);

        final Intent intent = getIntent();
        final 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 the widget id, just bail.
        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) 
            finish();
        

        mRowIDs = new ArrayList<Long>(); // it's actually loaded from an ASyncTask, don't worry about that — it works.
        mAdapter = new MyListAdapter((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE));
        getListView().setAdapter(mAdapter);
    

    private class MyListAdapter extends BaseAdapter 
        // not relevant...
    

    @Override
    protected void onListItemClick(final ListView l, final View v, final int position, final long id) 
        if (position < mRowIDs.size()) 
            // Set widget result
            final Intent resultValue = new Intent();
            resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
            resultValue.putExtra("rowId", mRowIDs.get(position));
            setResult(RESULT_OK, resultValue);

            // Request widget update
            final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
            ChecksWidgetProvider.updateAppWidget(this, appWidgetManager, mAppWidgetId, mRowIDs);
        

        finish();
    

如您所见,我正在从我的应用小部件提供程序调用静态方法。我从the official doc 得到这个想法。

让我们看看我的提供者:

@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class ChecksWidgetProvider extends AppWidgetProvider 
    public static final String TOAST_ACTION = "com.example.android.stackwidget.TOAST_ACTION";
    public static final String EXTRA_ITEM = "com.example.android.stackwidget.EXTRA_ITEM";

    @Override
    public void onUpdate(final Context context, final AppWidgetManager appWidgetManager, final int[] appWidgetIds) 
        super.onUpdate(context, appWidgetManager, appWidgetIds);
        final int N = appWidgetIds.length;

        // Perform this loop procedure for each App Widget that belongs to this provider
        for (int i = 0; i < N; i++) 
            // Here we setup the intent which points to the StackViewService which will
            // provide the views for this collection.
            final Intent intent = new Intent(context, ChecksWidgetService.class);
            intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            // When intents are compared, the extras are ignored, so we need to embed the extras
            // into the data so that the extras will not be ignored.
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.checks_widget);
            rv.setRemoteAdapter(android.R.id.list, intent);

            // The empty view is displayed when the collection has no items. It should be a sibling
            // of the collection view.
            rv.setEmptyView(android.R.id.list, android.R.id.empty);

            // Here we setup the a pending intent template. Individuals items of a collection
            // cannot setup their own pending intents, instead, the collection as a whole can
            // setup a pending intent template, and the individual items can set a fillInIntent
            // to create unique before on an item to item basis.
            final Intent toastIntent = new Intent(context, ChecksWidgetProvider.class);
            toastIntent.setAction(ChecksWidgetProvider.TOAST_ACTION);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]);
            toastIntent.setData(Uri.parse(toastIntent.toUri(Intent.URI_INTENT_SCHEME)));
            final PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            rv.setPendingIntentTemplate(android.R.id.list, toastPendingIntent);

            appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
        
    

    @Override
    public void onReceive(final Context context, final Intent intent) 
        final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
        if (intent.getAction().equals(TOAST_ACTION)) 
            final int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
            final long rowId = intent.getLongExtra("rowId", 0);
            final int viewIndex = intent.getIntExtra(EXTRA_ITEM, 0);
            Toast.makeText(context, "Touched view " + viewIndex + " (rowId: " + rowId + ")", Toast.LENGTH_SHORT).show();
        
        super.onReceive(context, intent);
    

    @Override
    public void onAppWidgetOptionsChanged(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId, final Bundle newOptions) 
        updateAppWidget(context, appWidgetManager, appWidgetId, newOptions.getLong("rowId"));
    

    public static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId, final long rowId) 
        final RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.checks_widget);
        appWidgetManager.updateAppWidget(appWidgetId, views);
    

这基本上是官方文档的复制/粘贴。我们可以在这里看到我的静态方法。让我们暂时假设它实际上使用了rowId

当我收到选项更改广播 (onAppWidgetOptionsChanged) 时,我们还可以看到另一个失败的(见下文)尝试更新应用小部件。

基于集合的应用小部件所需的Service 几乎是文档的精确复制/粘贴:

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public class ChecksWidgetService extends RemoteViewsService 
    @Override
    public RemoteViewsFactory onGetViewFactory(final Intent intent) 
        return new StackRemoteViewsFactory(this.getApplicationContext(), intent);
    


class StackRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory 
    private static final int mCount = 10;
    private final List<WidgetItem> mWidgetItems = new ArrayList<WidgetItem>();
    private final Context mContext;
    private final int mAppWidgetId;
    private final long mRowId;

    public StackRemoteViewsFactory(final Context context, final Intent intent) 
        mContext = context;
        mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        mRowId = intent.getLongExtra("rowId", 0);
    

    @Override
    public void onCreate() 
        // In onCreate() you setup any connections / cursors to your data source. Heavy lifting,
        // for example downloading or creating content etc, should be deferred to onDataSetChanged()
        // or getViewAt(). Taking more than 20 seconds in this call will result in an ANR.
        for (int i = 0; i < mCount; i++) 
            mWidgetItems.add(new WidgetItem(i + " (rowId: " + mRowId + ") !"));
        

        // We sleep for 3 seconds here to show how the empty view appears in the interim.
        // The empty view is set in the StackWidgetProvider and should be a sibling of the
        // collection view.
        try 
            Thread.sleep(3000);
         catch (final InterruptedException e) 
            e.printStackTrace();
        
    

    @Override
    public void onDestroy() 
        // In onDestroy() you should tear down anything that was setup for your data source,
        // eg. cursors, connections, etc.
        mWidgetItems.clear();
    

    @Override
    public int getCount() 
        return mCount;
    

    @Override
    public RemoteViews getViewAt(final int position) 
        // position will always range from 0 to getCount() - 1.

        // We construct a remote views item based on our widget item xml file, and set the
        // text based on the position.
        final RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.widget_item);
        rv.setTextViewText(R.id.widget_item, mWidgetItems.get(position).text);

        // Next, we set a fill-intent which will be used to fill-in the pending intent template
        // which is set on the collection view in StackWidgetProvider.
        final Bundle extras = new Bundle();
        extras.putInt(ChecksWidgetProvider.EXTRA_ITEM, position);
        final Intent fillInIntent = new Intent();
        fillInIntent.putExtras(extras);
        rv.setOnClickFillInIntent(R.id.widget_item, fillInIntent);

        // You can do heaving lifting in here, synchronously. For example, if you need to
        // process an image, fetch something from the network, etc., it is ok to do it here,
        // synchronously. A loading view will show up in lieu of the actual contents in the
        // interim.
        try 
            L.d("Loading view " + position);
            Thread.sleep(500);
         catch (final InterruptedException e) 
            e.printStackTrace();
        

        // Return the remote views object.
        return rv;
    

    @Override
    public RemoteViews getLoadingView() 
        // You can create a custom loading view (for instance when getViewAt() is slow.) If you
        // return null here, you will get the default loading view.
        return null;
    

    @Override
    public int getViewTypeCount() 
        return 1;
    

    @Override
    public long getItemId(final int position) 
        return position;
    

    @Override
    public boolean hasStableIds() 
        return true;
    

    @Override
    public void onDataSetChanged() 
        // This is triggered when you call AppWidgetManager notifyAppWidgetViewDataChanged
        // on the collection view corresponding to this factory. You can do heaving lifting in
        // here, synchronously. For example, if you need to process an image, fetch something
        // from the network, etc., it is ok to do it here, synchronously. The widget will remain
        // in its current state while work is being done here, so you don't need to worry about
        // locking up the widget.
    

最后,我的小部件布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/widgetLayout"
    android:orientation="vertical"
    android:padding="@dimen/widget_margin"
    android:layout_
    android:layout_>

    <TextView
        android:id="@+id/resizeable_widget_title"
        style="@style/show_subTitle"
        android:padding="2dp"
        android:paddingLeft="5dp"
        android:textColor="#FFFFFFFF"
        android:background="@drawable/background_pink_striked_transparent"
        android:text="@string/show_title_key_dates" />

    <ListView
        android:id="@android:id/list"
        android:layout_marginRight="5dp"
        android:layout_marginLeft="5dp"
        android:background="@color/timeline_month_dark"
        android:layout_
        android:layout_ />

    <TextView
        android:id="@android:id/empty"
        android:layout_
        android:layout_
        android:gravity="center"
        android:textColor="#ffffff"
        android:textStyle="bold"
        android:text="@string/empty_view_text"
        android:textSize="20sp" />

</LinearLayout>

我的 android manifest XML 文件的相关部分:

<receiver android:name="com.my.full.pkg.ChecksWidgetProvider">
    <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>

    <meta-data
            android:name="android.appwidget.provider"
            android:resource="@xml/checks_widget_info" />
</receiver>
<activity android:name="com.my.full.pkg.ChecksWidgetConfigureActivity">
    <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
    </intent-filter>
</activity>
<service
    android:name="com.my.full.pkg.ChecksWidgetService"
    android:permission="android.permission.BIND_REMOTEVIEWS" />

xml/checks_widget_info.xml:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="146dp"
    android:minHeight="146dp"
    android:updatePeriodMillis="86400000"
    android:initialLayout="@layout/checks_widget"
    android:configure="com.my.full.pkg.ChecksWidgetConfigureActivity"
    android:resizeMode="horizontal|vertical"
    android:previewImage="@drawable/resizeable_widget_preview" />

那么,怎么了?好吧,当我创建小部件时,它是空的。我的意思是空虚。空的。没有。我的布局中没有定义空视图!什么鬼?

如果我重新安装应用程序或重新启动设备(或终止启动器应用程序),应用程序小部件实际上会更新并包含自动添加的 10 个项目,如示例中所示。

配置活动完成后,我无法更新该死的东西。摘自文档的这句话超出了我的理解:“在创建 App Widget 时不会调用 onUpdate() 方法 [...]——它只是第一次被跳过。” .

我的问题是:

为什么 Android 开发团队在第一次创建小部件时选择不调用 update? 如何在配置活动完成之前更新我的应用小部件?

我不明白的另一件事是动作流程:

    使用最后编译的代码安装应用程序,在启动器上准备空间,从启动器打开“小部件”菜单 选择我的小部件并将其放置到所需区域 在那一刻,我的应用小部件提供程序收到android.appwidget.action.APPWIDGET_ENABLED,然后收到android.appwidget.action.APPWIDGET_UPDATE 然后我的应用程序小部件提供程序会调用其onUpdate 方法。 我预计这会在配置活动完成后发生... 我的配置活动开始了。但应用小部件似乎已经创建和更新,我不明白。 我从配置活动中选择项目:onListItemClick 被调用 调用了来自我的提供商的静态 updateAppWidget,拼命尝试更新小部件。 配置活动设置其结果并完成。 提供者收到android.appwidget.action.APPWIDGET_UPDATE_OPTIONS:嗯,在创建时接收大小更新确实很有意义。这就是我拼命打电话的地方updateAppWidget 来自我的提供商的onUpdate 未被调用。为什么??!!

最后:小部件是空的。不是 listview-empty 或 @android:id/empty-empty,真的是 EMPTY。没有显示视图。什么都没有。 如果我再次安装应用程序,应用程序小部件将按预期填充列表视图中的视图。 调整小部件的大小没有效果。它只是再次调用onAppWidgetOptionsChanged,没有任何效果。

我所说的空是什么意思:应用小部件布局已膨胀,但列表视图未膨胀且空视图未显示。

【问题讨论】:

我没有看到您的小部件配置xml和AndroidManifest.xml,您也可以提供它们吗? 我已经添加了 AndroidManifest.xml 相关部分,我将很快添加小部件配置 xml(它还没有在源存储库中,所以我要几个小时才能访问源从现在开始)。 【参考方案1】:

通过 AppWidgetManager 进行更新的缺点是您必须提供 RemoteViews - 从设计的角度来看 - 这是没有意义的,因为与 RemoteViews 相关的逻辑应该封装在 AppWidgetProvider 中(或您的RemoteViewsService.RemoteViewsFactory 中的情况)。

SciencyGuy 通过静态方法公开 RemoteViews 逻辑的方法是处理该问题的一种方法,但还有一种更优雅的解决方案,将广播直接发送到小部件:

Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE, null, this, ChecksWidgetProvider.class);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] mAppWidgetId);
sendBroadcast(intent);

因此,AppWidgetProvider 的 onUpdate() 方法将被调用来为小部件创建 RemoteViews。

【讨论】:

请在接受的答案之前尝试这个答案,由于逻辑有所不同,这给我的应用程序引入了一些错误。这是我在Configuration Activity之后使用的方法,效果很好。 仅供参考,@HenriqueSousa 所指的“已接受”答案是在他撰写乔纳斯的答案时。我今天刚刚将 Emanuel 标记为已接受。 小心使用这种方法 - 查看我在使用它时遇到的问题here,以及解决方案。总而言之,文档具有误导性 - 配置活动完成后,只要活动返回 RESULT_OK 和包含小部件 ID 的意图,就会调用 onUpdate()。 @rothloup 您遇到问题的原因不同,但不是因为这种方法是错误的。它只是用更优雅的解决方案替换了developer.android0com/guide/topics/appwidgets/… 中的 create RemoteView 部分。另请参阅我对您引用的问题的回答。 非常感谢,official documentation 太不完整了。对于任何未来的读者,EXTRA_APPWIDGET_ID 不起作用,它必须是 EXTRA_APPWIDGET_IDS【参考方案2】:

配置活动完成后未触发 onUpdate 方法是正确的。初始更新取决于您的配置活动。所以你需要构建初始视图。

这是在配置结束时应该做什么的要点:

// First set result OK with appropriate widgetId
Intent resultValue = new Intent();
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
setResult(RESULT_OK, resultValue);

// Build/Update widget
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());

// This is equivalent to your ChecksWidgetProvider.updateAppWidget()    
appWidgetManager.updateAppWidget(appWidgetId,
                                 ChecksWidgetProvider.buildRemoteViews(getApplicationContext(),
                                                                       appWidgetId));

// Updates the collection view, not necessary the first time
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.notes_list);

// Destroy activity
finish();

您已经正确设置了结果。并且您调用 ChecksWidgetProvider.updateAppWidget(),但是 updateAppWidget() 不会返回正确的结果。

updateAppWidget() 当前返回一个空的 RemoteViews 对象。这就解释了为什么你的小部件一开始是完全空的。你没有用任何东西填充视图。我建议您将代码从 onUpdate 移动到静态 buildRemoteViews() 方法,您可以从 onUpdate 和 updateAppWidget() 调用该方法:

public static RemoteViews buildRemoteViews(final Context context, final int appWidgetId) 
        final RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.checks_widget);
        rv.setRemoteAdapter(android.R.id.list, intent);

        // The empty view is displayed when the collection has no items. It should be a sibling
        // of the collection view.
        rv.setEmptyView(android.R.id.list, android.R.id.empty);

        // Here we setup the a pending intent template. Individuals items of a collection
        // cannot setup their own pending intents, instead, the collection as a whole can
        // setup a pending intent template, and the individual items can set a fillInIntent
        // to create unique before on an item to item basis.
        final Intent toastIntent = new Intent(context, ChecksWidgetProvider.class);
        toastIntent.setAction(ChecksWidgetProvider.TOAST_ACTION);
        toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        toastIntent.setData(Uri.parse(toastIntent.toUri(Intent.URI_INTENT_SCHEME)));
        final PendingIntent toastPendingIntent = PendingIntent.getBroadcast(context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        rv.setPendingIntentTemplate(android.R.id.list, toastPendingIntent);

        return rv;


public static void updateAppWidget(final Context context, final AppWidgetManager appWidgetManager, final int appWidgetId) 
    final RemoteViews views = buildRemoteViews(context, appWidgetId);
    appWidgetManager.updateAppWidget(appWidgetId, views);


@Override
public void onUpdate(final Context context, final AppWidgetManager appWidgetManager, final int[] appWidgetIds) 
    super.onUpdate(context, appWidgetManager, appWidgetIds);

    // Perform this loop procedure for each App Widget that belongs to this provider
    for (int appWidgetId: appWidgetIds) 
        RemoteViews rv = buildRemoteViews(context, appWidgetId);
        appWidgetManager.updateAppWidget(appWidgetIds[i], rv);
    

这应该负责小部件的初始化。

在我的示例代码中调用 finish() 之前的最后一步是更新集合视图。正如评论所说,这不是第一次。但是,我包含它以防万一您打算允许在添加小部件后对其进行重新配置。在这种情况下,必须手动更新集合视图以确保加载适当的视图和数据。

【讨论】:

太棒了!杰出的!我想我对医生有点迷失了。再次感谢。【参考方案3】:

我没有看到你的 appwidgetprovider.xml 和 AndroidManifest.xml,但我猜你没有正确设置你的配置活动。

这是怎么做的:

    将以下属性添加到您的 appwidgetprovider.xml:

    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
        ...
        android:configure="com.full.package.name.ChecksWidgetConfigureActivity" 
        ... />
    

    您的配置活动应该有一个适当的intent-filter

    <activity android:name=".ChecksWidgetConfigureActivity">
        <intent-filter>
            <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
        </intent-filter>
    </activity>
    

如果配置活动配置正确,onUpdate() 只会在完成后触发。

【讨论】:

如果配置不正确,我的配置活动就不会显示,对吧?我已经用我的清单条目更新了这个问题。【参考方案4】:

对于正在寻找解释如何使用配置或选项或设置功能创建小部件的最新示例的开发人员,请参阅http://www.zoftino.com/android-widget-example。

为了开发配置功能,需要在应用程序中创建允许用户配置小部件的配置活动和 UI。 可以在创建小部件实例或每次单击小部件时显示小部件配置选项。每次更改小部件设置时,都需要将更改应用到小部件实例。

【讨论】:

虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接的答案可能会失效。

以上是关于如何创建具有配置活动的应用小部件,并首次对其进行更新?的主要内容,如果未能解决你的问题,请参考以下文章

如何在活动和小部件之间共享数据?

配置更改后小部件不刷新

无法将小部件 ID 传递给配置活动

运行应用小部件 - 无法识别启动活动

我们总结了 3 大使用建议,并首次公开 Nacos3.0 规划图

多种小部件尺寸