Icona animata per ActionItem


91

Ho cercato ovunque una soluzione adeguata al mio problema e non riesco ancora a trovarne una. Ho un ActionBar (ActionBarSherlock) con un menu che viene gonfiato da un file XML e quel menu contiene un elemento e quell'elemento viene mostrato come ActionItem.

menù:

<menu xmlns:android="http://schemas.android.com/apk/res/android" >    
    <item
        android:id="@+id/menu_refresh"       
        android:icon="@drawable/ic_menu_refresh"
        android:showAsAction="ifRoom"
        android:title="Refresh"/>    
</menu>

attività:

[...]
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getSupportMenuInflater().inflate(R.menu.mymenu, menu);
    return true;
  }
[...]

ActionItem viene visualizzato con un'icona e senza testo, tuttavia quando un utente fa clic su ActionItem, voglio che l'icona inizi ad animarsi, più specificamente, ruotando sul posto. L'icona in questione è un'icona di aggiornamento.

Mi rendo conto che ActionBar ha il supporto per l'utilizzo di visualizzazioni personalizzate ( aggiunta di una visualizzazione azione ) tuttavia questa visualizzazione personalizzata è espansa per coprire l'intera area della ActionBar e in realtà blocca tutto tranne l'icona dell'app, che nel mio caso non è quello che stavo cercando .

Quindi il mio tentativo successivo è stato quello di provare a utilizzare AnimationDrawable e definire la mia animazione fotogramma per fotogramma, impostare il disegnabile come icona per la voce di menu, quindi onOptionsItemSelected(MenuItem item)ottenere l'icona e iniziare ad animare utilizzando ((AnimationDrawable)item.getIcon()).start(). Questo però non ha avuto successo. Qualcuno sa come ottenere questo effetto?

Risposte:


173

Sei sulla strada giusta. Ecco come l'app GitHub Gaug.es lo implementerà.

Per prima cosa definiscono un XML di animazione:

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromDegrees="0"
    android:toDegrees="360"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="1000"
    android:interpolator="@android:anim/linear_interpolator" />

Ora definisci un layout per la visualizzazione azioni:

<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:src="@drawable/ic_action_refresh"
    style="@style/Widget.Sherlock.ActionButton" />

Tutto quello che dobbiamo fare è abilitare questa visualizzazione ogni volta che si fa clic sull'elemento:

 public void refresh() {
     /* Attach a rotating ImageView to the refresh item as an ActionView */
     LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
     ImageView iv = (ImageView) inflater.inflate(R.layout.refresh_action_view, null);

     Animation rotation = AnimationUtils.loadAnimation(getActivity(), R.anim.clockwise_refresh);
     rotation.setRepeatCount(Animation.INFINITE);
     iv.startAnimation(rotation);

     refreshItem.setActionView(iv);

     //TODO trigger loading
 }

Quando il caricamento è terminato, interrompi semplicemente l'animazione e cancella la visualizzazione:

public void completeRefresh() {
    refreshItem.getActionView().clearAnimation();
    refreshItem.setActionView(null);
}

E hai finito!

Alcune cose aggiuntive da fare:

  • Memorizza nella cache l'inflazione del layout della visualizzazione azione e l'inflazione dell'animazione. Sono lenti, quindi vuoi farli solo una volta.
  • Aggiungi i nullcontrollicompleteRefresh()

Ecco la richiesta di pull sull'app: https://github.com/github/gauges-android/pull/13/files


2
Ottima risposta, tuttavia getActivity () non è più accessibile, usa invece getApplication ().
theAlse

8
@Alborz Questo è del tutto specifico per la tua app e non è una regola generale. Dipende tutto da dove stai posizionando il metodo di aggiornamento.
Jake Wharton

1
Funziona anche con la normale ActionBar (senza ActionBar Sherlock)? Per me l'icona salta a sinistra quando inizia l'animazione e non è più cliccabile in seguito. EDIT: Ho appena scoperto che l'impostazione di ActionView causa questo, non l'animazione stessa.
Nome visualizzato

2
Non dovrebbero esserci salti se l'immagine è quadrata e la dimensione corretta per un elemento di azione.
Jake Wharton

13
Ho anche avuto problemi con il pulsante che saltava di lato durante l'implementazione. Questo perché non stavo usando lo stile Widget.Sherlock.ActionButton. L'ho corretto aggiungendo android:paddingLeft="12dp"e android:paddingRight="12dp"al mio tema.
William Carter

16

Ho lavorato un po 'sulla soluzione usando ActionBarSherlock, ho trovato questo:

res / layout / indeterminate_progress_action.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="48dp"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:paddingRight="12dp" >

    <ProgressBar
        style="@style/Widget.Sherlock.ProgressBar"
        android:layout_width="44dp"
        android:layout_height="32dp"
        android:layout_gravity="left"
        android:layout_marginLeft="12dp"
        android:indeterminate="true"
        android:indeterminateDrawable="@drawable/rotation_refresh"
        android:paddingRight="12dp" />

</FrameLayout>

res / layout-v11 / indeterminate_progress_action.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center" >

    <ProgressBar
        style="@style/Widget.Sherlock.ProgressBar"
        android:layout_width="32dp"
        android:layout_gravity="left"
        android:layout_marginRight="12dp"
        android:layout_marginLeft="12dp"
        android:layout_height="32dp"
        android:indeterminateDrawable="@drawable/rotation_refresh"
        android:indeterminate="true" />

</FrameLayout>

res / drawable / rotation_refresh.xml

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:pivotX="50%"
    android:pivotY="50%"
    android:drawable="@drawable/ic_menu_navigation_refresh"
    android:repeatCount="infinite" >

</rotate>

Codice in attività (ce l'ho nella classe genitore ActivityWithRefresh)

// Helper methods
protected MenuItem refreshItem = null;  

protected void setRefreshItem(MenuItem item) {
    refreshItem = item;
}

protected void stopRefresh() {
    if (refreshItem != null) {
        refreshItem.setActionView(null);
    }
}

protected void runRefresh() {
    if (refreshItem != null) {
        refreshItem.setActionView(R.layout.indeterminate_progress_action);
    }
}

nell'attività di creazione di voci di menu

private static final int MENU_REFRESH = 1;
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    menu.add(Menu.NONE, MENU_REFRESH, Menu.NONE, "Refresh data")
            .setIcon(R.drawable.ic_menu_navigation_refresh)
            .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS);
    setRefreshItem(menu.findItem(MENU_REFRESH));
    refreshData();
    return super.onCreateOptionsMenu(menu);
}

private void refreshData(){
    runRefresh();
    // work with your data
    // for animation to work properly, make AsyncTask to refresh your data
    // or delegate work anyhow to another thread
    // If you'll have work at UI thread, animation might not work at all
    stopRefresh();
}

E l'icona, questa è drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi / ic_menu_navigation_refresh.png

Questo potrebbe essere trovato in http://developer.android.com/design/downloads/index.html#action-bar-icon-pack


Cordiali saluti, ho dovuto aggiungere anche layout-tvdpi-v11 / indeterminate_progress_action.xml con un android: layout_marginRight = "16dp", per visualizzare correttamente. Non so se questa incoerenza sia un bug nel mio codice, ABS o SDK.
Iraklis

Ho testato questa soluzione con poche delle mie applicazioni e tutte utilizzano lo stesso codice. Quindi immagino che questa sia una certa incoerenza nel tuo codice, poiché ABS (4.2.0) e SDK (API 14 e versioni successive) sono condivisi ;-)
Marek Sebera

L'hai provato in un Nexus 7? (non un emu, un dispositivo reale) È l'unico dispositivo che non viene visualizzato correttamente, da qui le impostazioni tvdpi.
Iraklis

@Iraklis no, non ho questo dispositivo. Quindi sì, ora capisco cosa hai eseguito il debug. Ottimo, sentiti libero di aggiungerlo per rispondere.
Marek Sebera

6

Oltre a quanto ha detto Jake Wharton, dovresti probabilmente fare quanto segue per assicurarti che l'animazione si interrompa senza intoppi e non salti del caricamento.

Innanzitutto, crea un nuovo booleano (per l'intera classe):

private boolean isCurrentlyLoading;

Trova il metodo che avvia il caricamento. Imposta il valore booleano su true quando inizia il caricamento dell'attività.

isCurrentlyLoading = true;

Trova il metodo che viene avviato al termine del caricamento. Invece di cancellare l'animazione, imposta il valore booleano su false.

isCurrentlyLoading = false;

Imposta un AnimationListener sulla tua animazione:

animationRotate.setAnimationListener(new AnimationListener() {

Quindi, ogni volta che l'animazione è stata eseguita una volta, ciò significa che quando l'icona ha fatto una rotazione, controlla lo stato di caricamento e, se non si carica più, l'animazione si fermerà.

@Override
public void onAnimationRepeat(Animation animation) {
    if(!isCurrentlyLoading) {
        refreshItem.getActionView().clearAnimation();
        refreshItem.setActionView(null);
    }
}

In questo modo, l'animazione può essere interrotta solo se è già stata ruotata fino alla fine e verrà ripetuta a breve E non si carica più.

Questo è almeno quello che ho fatto quando ho voluto implementare l'idea di Jake.


2

C'è anche un'opzione per creare la rotazione nel codice. Snip completo:

    MenuItem item = getToolbar().getMenu().findItem(Menu.FIRST);
    if (item == null) return;

    // define the animation for rotation
    Animation animation = new RotateAnimation(0.0f, 360.0f,
            Animation.RELATIVE_TO_SELF, 0.5f,
            Animation.RELATIVE_TO_SELF, 0.5f);
    animation.setDuration(1000);
    //animRotate = AnimationUtils.loadAnimation(this, R.anim.rotation);

    animation.setRepeatCount(Animation.INFINITE);

    ImageView imageView = new ImageView(this);
    imageView.setImageDrawable(UIHelper.getIcon(this, MMEXIconFont.Icon.mmx_refresh));

    imageView.startAnimation(animation);
    item.setActionView(imageView);

Usando questo e il tocco suOptionsItemSelected non viene chiamato.
pseudozach

1

Con la libreria di supporto possiamo animare l'icona senza actionView personalizzata.

private AnimationDrawableWrapper drawableWrapper;    

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    //inflate menu...

    MenuItem menuItem = menu.findItem(R.id.your_icon);
    Drawable icon = menuItem.getIcon();
    drawableWrapper = new AnimationDrawableWrapper(getResources(), icon);
    menuItem.setIcon(drawableWrapper);
    return true;
}

public void startRotateIconAnimation() {
    ValueAnimator animator = ObjectAnimator.ofInt(0, 360);
    animator.addUpdateListener(animation -> {
        int rotation = (int) animation.getAnimatedValue();
        drawableWrapper.setRotation(rotation);
    });
    animator.start();
}

Non possiamo animare direttamente Drawable, quindi usa DrawableWrapper (da android.support.v7 per API <21):

public class AnimationDrawableWrapper extends DrawableWrapper {

    private float rotation;
    private Rect bounds;

    public AnimationDrawableWrapper(Resources resources, Drawable drawable) {
        super(vectorToBitmapDrawableIfNeeded(resources, drawable));
        bounds = new Rect();
    }

    @Override
    public void draw(Canvas canvas) {
        copyBounds(bounds);
        canvas.save();
        canvas.rotate(rotation, bounds.centerX(), bounds.centerY());
        super.draw(canvas);
        canvas.restore();
    }

    public void setRotation(float degrees) {
        this.rotation = degrees % 360;
        invalidateSelf();
    }

    /**
     * Workaround for issues related to vector drawables rotation and scaling:
     * https://code.google.com/p/android/issues/detail?id=192413
     * https://code.google.com/p/android/issues/detail?id=208453
     */
    private static Drawable vectorToBitmapDrawableIfNeeded(Resources resources, Drawable drawable) {
        if (drawable instanceof VectorDrawable) {
            Bitmap b = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(b);
            drawable.setBounds(0, 0, c.getWidth(), c.getHeight());
            drawable.draw(c);
            drawable = new BitmapDrawable(resources, b);
        }
        return drawable;
    }
}

Ho preso l'idea per DrawableWrapper da qui: https://stackoverflow.com/a/39108111/5541688


0

è la mia soluzione molto semplice (ad esempio, ho bisogno di un refactor) funziona con MenuItem standard, puoi usarlo con qualsiasi numero di stati, icone, animazioni, logica ecc.

nella classe di attività:

private enum RefreshMode {update, actual, outdated} 

ascoltatore standard:

public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case R.id.menu_refresh: {
            refreshData(null);
            break;
        }
    }
}

in refreshData () fai qualcosa di simile:

setRefreshIcon(RefreshMode.update);
// update your data
setRefreshIcon(RefreshMode.actual);

metodo per definire il colore o l'animazione per l'icona:

 void setRefreshIcon(RefreshMode refreshMode) {

    LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    Animation rotation = AnimationUtils.loadAnimation(MainActivity.this, R.anim.rotation);
    FrameLayout iconView;

    switch (refreshMode) {
        case update: {
            iconView = (FrameLayout) inflater.inflate(R.layout.refresh_action_view,null);
            iconView.startAnimation(rotation);
            toolbar.getMenu().findItem(R.id.menu_refresh).setActionView(iconView);
            break;
        }
        case actual: {
            toolbar.getMenu().findItem(R.id.menu_refresh).getActionView().clearAnimation();
            iconView = (FrameLayout) inflater.inflate(R.layout.refresh_action_view_actual,null);
            toolbar.getMenu().findItem(R.id.menu_refresh).setActionView(null);
            toolbar.getMenu().findItem(R.id.menu_refresh).setIcon(R.drawable.ic_refresh_24dp_actual);
            break;
        }
        case outdated:{
            toolbar.getMenu().findItem(R.id.menu_refresh).setIcon(R.drawable.ic_refresh_24dp);
            break;
        }
        default: {
        }
    }
}

ci sono 2 layout con l'icona (R.layout.refresh_action_view (+ "_actual")):

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:gravity="center">
<ImageView
    android:src="@drawable/ic_refresh_24dp_actual" // or ="@drawable/ic_refresh_24dp"
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:layout_margin="12dp"/>
</FrameLayout>

animazione di rotazione standard in questo caso (R.anim.rotation):

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:fromDegrees="0"
android:toDegrees="360"
android:pivotX="50%"
android:pivotY="50%"
android:duration="1000"
android:repeatCount="infinite"
/>

0

il modo migliore è qui:

public class HomeActivity extends AppCompatActivity {
    public static ActionMenuItemView btsync;
    public static RotateAnimation rotateAnimation;

@Override
protected void onCreate(Bundle savedInstanceState) {
    rotateAnimation = new RotateAnimation(360, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
    rotateAnimation.setDuration((long) 2*500);
    rotateAnimation.setRepeatCount(Animation.INFINITE);

e poi:

private void sync() {
    btsync = this.findViewById(R.id.action_sync); //remember that u cant access this view at onCreate() or onStart() or onResume() or onPostResume() or onPostCreate() or onCreateOptionsMenu() or onPrepareOptionsMenu()
    if (isSyncServiceRunning(HomeActivity.this)) {
        showConfirmStopDialog();
    } else {
        if (btsync != null) {
            btsync.startAnimation(rotateAnimation);
        }
        Context context = getApplicationContext();
        context.startService(new Intent(context, SyncService.class));
    }
}

Ricorda che non puoi accedere a "btsync = this.findViewById (R.id.action_sync);" at onCreate () o onStart () o onResume () o onPostResume () o onPostCreate () o onCreateOptionsMenu () o onPrepareOptionsMenu () se vuoi ottenerlo subito dopo l'inizio dell'attività mettilo in un postdelayed:

public static void refreshSync(Activity context) {
    Handler handler = new Handler(Looper.getMainLooper());
    handler.postDelayed(new Runnable() {
        public void run() {
            btsync = context.findViewById(R.id.action_sync);
            if (btsync != null && isSyncServiceRunning(context)) {
                btsync.startAnimation(rotateAnimation);
            } else if (btsync != null) {
                btsync.clearAnimation();
            }
        }
    }, 1000);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.