Подтвердить что ты не робот

Как создать виджет приложения с активностью конфигурации и обновить его в первый раз?

Это сводит меня с ума. Я не знаю, как обновить виджет приложения из активности конфигурации, даже с рекомендованной практикой. Почему метод обновления не вызывается при создании виджета приложения, я не понимаю.

Что мне нужно: виджет приложения, содержащий коллекцию (со списком) элементов. Но пользователю нужно что-то выбрать, поэтому мне нужна конфигурация.

Активность конфигурации: 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 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();
    }
}

Как вы можете видеть, я вызываю статический метод у поставщика виджета приложений. Я получил эту идею из официального документа.

Посмотрим на моего провайдера:

@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_width="match_parent"
    android:layout_height="match_parent">

    <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_width="match_parent"
        android:layout_height="match_parent" />

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

</LinearLayout>

Соответствующий раздел моего XML файла манифеста Android:

<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 элементов, которые автоматически добавляются, как в примере.

Я не могу получить эту проклятую вещь после завершения настройки конфигурации. Это предложение, взятое из документа, находится вне меня: "Метод onUpdate() не будет вызываться при создании виджета приложения [...] - он пропускается только в первый раз".

Мой вопрос:

  • Почему в мире разработчики Android решили не вызывать обновление при создании виджета при первом запуске?
  • Как обновить виджет приложения до завершения процесса настройки?

Еще одна вещь, которую я не понимаю, - это поток действий:

  • Установите приложение с последним скомпилированным кодом, подготовьте пространство на панели запуска, откройте меню "Виджеты" из панели запуска
  • Выберите мой виджет и поместите его в нужную область.
  • В этот момент провайдер виджета приложений получает 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 снова, что не имеет эффекта.

То, что я подразумеваю под пустым: макет виджета приложения раздувается, но список не завышен, а пустой вид НЕ отображается.

4b9b3361

Ответ 1

Недостаток выполнения обновления через AppWidgetManager заключается в том, что вы должны предоставить RemoteViews, которые - с точки зрения дизайна - не имеют смысла, поскольку логика, связанная с RemoteViews, должна быть инкапсулирована в AppWidgetProvider (или в вашем в RemoteViewsService.RemoteViewsFactory).

Подход ScicitGuy для выявления логики 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() для создания RemoteView для виджета.

Ответ 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.

Для разработки функции конфигурации в приложении необходимо создать действия по настройке и пользовательский интерфейс, который позволяет пользователю настраивать виджет. Параметр конфигурации виджета может отображаться при создании экземпляра виджета или при каждом нажатии на виджет. Каждый раз, когда изменяются настройки виджета, изменения должны применяться к экземплярам виджета.