2012-12-04 9 views
18

Questo mi sta facendo impazzire. Non so come aggiornare il widget dell'app dall'attività di configurazione, anche con le pratiche consigliate. Perché il metodo di aggiornamento non è chiamato sulla creazione del widget dell'app è fuori dalla mia comprensione.Come creare un widget dell'app con un'attività di configurazione e aggiornarlo per la prima volta?

Cosa mi piacerebbe: un widget di app contenente una raccolta (con una lista) di elementi. Ma l'utente deve selezionare qualcosa, quindi ho bisogno di un'attività di configurazione.

L'attività di configurazione è un 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(); 
    } 
} 

Come potete vedere sto chiamando un metodo statico dal mio fornitore di widget di app. Ho avuto questa idea da the official doc.

Diamo uno sguardo al mio fornitore:

@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); 
    } 
} 

Questo è fondamentalmente un copia/incolla dal documento ufficiale. Possiamo vedere il mio metodo statico qui. Facciamo finta che in realtà usi lo rowId per ora.

Possiamo anche vedere un altro tentativo fallito (vedi sotto) di aggiornare il widget dell'app quando ricevo le opzioni modificate broadcast (onAppWidgetOptionsChanged).

Il Service richiesto per un widget di un'applicazione basata su collezioni è quasi un'esatta copia/incolla del documento:

@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. 
    } 
} 

E alla fine, il mio widget di impaginazione:

<?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> 

La sezione pertinente del mio file XML manifest 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" /> 

Allora, che cosa c'è che non va? Bene, quando creo il widget è vuoto. Intendo vuoto. Vuoto. Niente. Non ho la vista vuota definita nel mio layout! Che diavolo?

Se si reinstalla l'app o si riavvia il dispositivo (o si chiude l'app di avvio), il widget dell'app viene effettivamente aggiornato e contiene i 10 elementi aggiunti automaticamente come nell'esempio.

Non riesco a ottenere la dannata cosa da aggiornare al termine dell'attività di configurazione. Questa frase, tratta dal documento, è oltre me: "Il metodo onUpdate() non verrà chiamato quando viene creato il widget dell'app [...] - viene solo saltato la prima volta.".

La mia domanda è:

  • Perché nel mondo ha fatto il team di sviluppo di Android ha scelto di non chiamare aggiornamento per la prima volta si crea il widget?
  • Come posso aggiornare il widget dell'app prima che l'attività di configurazione termini?

Un'altra cosa che non capisco è il flusso di azione:

  1. installare l'applicazione con l'ultimo codice compilato, preparare lo spazio nel programma di avvio, aprire il "widget" menù dal lanciatore
  2. scegliere i miei widget e posizionarlo nella zona desiderata
  3. In quel momento, la mia app fornitore widget di riceve android.appwidget.action.APPWIDGET_ENABLED e poi android.appwidget.action.APPWIDGET_UPDATE
  4. poi la mia app fornitore widget di prende ilMetodochiamato. Mi aspettavo che succedesse DOPO che l'attività di configurazione termina ...
  5. La mia attività di configurazione inizia. Ma il widget dell'app sembra già creato E aggiornato, che non capisco.
  6. scelgo l'elemento dalla mia attività di configurazione: onListItemClick viene chiamato
  7. Il statica updateAppWidget dal mio fornitore è chiamato, cercando disperatamente di aggiornare il widget.
  8. L'attività di configurazione imposta il suo risultato e termina.
  9. Il provider riceve android.appwidget.action.APPWIDGET_UPDATE_OPTIONS: beh, questo ha molto senso ricevere un aggiornamento di dimensioni quando viene creato. Ecco dove chiamo disperatamente updateAppWidget
  10. onUpdate dal mio provider NON è stato chiamato. Perché??!!

Alla fine: il widget è vuoto. Non listview-empty o @android: id/empty-empty, veramente EMPTY. Nessuna visualizzazione visualizzata. Niente.
Se si installa nuovamente l'app, il widget dell'app viene popolato con visualizzazioni all'interno del listview, come previsto.
Il ridimensionamento del widget non ha alcun effetto. Chiama di nuovo onAppWidgetOptionsChanged, che non ha alcun effetto.

Cosa intendo per vuoto: il layout del widget dell'app è gonfiato, ma il listview NON è gonfiato e la vista vuota NON è visualizzata.

+0

Non vedo la configurazione del widget xml e AndroidManifest.xml, puoi fornire anche loro? –

+0

Ho aggiunto la sezione pertinente 'AndroidManifest.xml', aggiungerò presto il xml di configurazione del widget (non è ancora nel repository di origine, quindi non avrò accesso all'origine fino a poche ore da ora). –

risposta

28

Lo svantaggio di eseguire l'aggiornamento tramite AppWidgetManager è che è necessario fornire RemoteViews che - dal punto di vista del progetto - non ha senso in quanto la logica relativa a RemoteViews deve essere incapsulata all'interno di AppWidgetProvider (o in il tuo caso in RemoteViewsService.RemoteViewsFactory).

approccio di SciencyGuy per esporre la logica RemoteViews tramite un metodo statico è un modo di trattare con questo, ma c'è una soluzione più elegante l'invio di una trasmissione direttamente al widget:

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

Come conseguenza del AppWidgetProvider onUpdate () verrà chiamato il metodo per creare RemoteViews per il widget.

+1

Questa dovrebbe essere la risposta giusta !! mi ha salvato la giornata;) – Mirko

+0

Per favore, PROVA questa risposta prima di quella accettata, che ha introdotto la mia app su alcuni bug a causa di una logica un po 'diversa. Questo è il metodo che uso dopo l'attività di configurazione e funziona benissimo. –

+1

Per la risposta "accettata" @HenriqueSousa si riferisce al momento in cui ha scritto la risposta di Jonas. Ho appena segnato Emanuel come accettato oggi. –

2

Non ho visto il tuo appwidgetprovider.xml e AndroidManifest.xml, ma la mia ipotesi è che non hai impostato correttamente l'attività di configurazione.

Ecco come si fa:

  1. aggiungere il seguente attributo al appwidgetprovider.xml:

    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" 
        ... 
        android:configure="com.full.package.name.ChecksWidgetConfigureActivity" 
        ... /> 
    
  2. La vostra attività di configurazione dovrebbe avere un adeguato intent-filter:

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

Se l'attività di configurazione è configurata correttamente, onUpdate() viene attivato solo al termine.

+0

Se non è stato configurato correttamente, la mia attività di configurazione non verrebbe visualizzata, giusto? Ho aggiornato la domanda con le mie voci manifest. –

15

È corretto che il metodo onUpdate non venga attivato al termine dell'attività di configurazione. Spetta alla tua attività di configurazione eseguire l'aggiornamento iniziale. Quindi è necessario costruire la vista iniziale.

Questa è l'essenza di ciò che si dovrebbe fare alla fine della configurazione:

// 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(); 

È già impostato correttamente il risultato. E chiami ChecksWidgetProvider.updateAppWidget(), tuttavia updateAppWidget() non restituisce il risultato corretto.

updateAppWidget() al corrente restituisce un oggetto RemoteViews vuoto. Questo spiega perché il tuo widget è completamente vuoto all'inizio. Non hai riempito la vista di nulla. Suggerisco che si sposta il codice onUpdate ad un buildRemoteViews metodo statico(), che è possibile chiamare da entrambi onUpdate e 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); 
    } 
} 

che dovrebbe prendersi cura di l'inizializzazione del widget.

L'ultimo passaggio prima di chiamare finish() nel mio codice di esempio sta aggiornando la vista di raccolta. Come dice il commento, non è necessario la prima volta. Tuttavia, lo includo solo nel caso in cui intendi consentire la riconfigurazione di un widget dopo che è stato aggiunto. In tal caso, è necessario aggiornare manualmente la vista di raccolta per assicurarsi che vengano caricate le viste e i dati appropriati.

+0

E 'stato FANTASTICO! Brillante! Immagino di essermi perso un po 'con il doc. Grazie ancora. –

Problemi correlati