Icône animée pour ActionItem

91

J'ai cherché partout une solution appropriée à mon problème et je n'arrive pas encore à en trouver une. J'ai une ActionBar (ActionBarSherlock) avec un menu qui est gonflé à partir d'un fichier XML et ce menu contient un élément et cet élément est affiché comme un ActionItem.

menu:

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

activité:

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

L'ActionItem est affiché avec une icône et pas de texte, mais lorsqu'un utilisateur clique sur l'ActionItem, je veux que l'icône commence à s'animer, plus précisément, à tourner sur place. L'icône en question est une icône de rafraîchissement.

Je me rends compte qu'ActionBar prend en charge l'utilisation de vues personnalisées ( Ajout d'une vue d'action ), mais cette vue personnalisée est étendue pour couvrir toute la zone de l'ActionBar et bloque en fait tout sauf l'icône de l'application, qui dans mon cas n'est pas ce que je recherchais .

Ma prochaine tentative a donc été d'essayer d'utiliser AnimationDrawable et de définir mon animation image par image, de définir le dessinable comme icône de l'élément de menu, puis d' onOptionsItemSelected(MenuItem item)obtenir l'icône et de commencer à animer en utilisant ((AnimationDrawable)item.getIcon()).start(). Cela a cependant échoué. Quelqu'un connaît-il un moyen d'accomplir cet effet?

Alex Fu
la source

Réponses:

173

Vous êtes sur la bonne voie. Voici comment l'application GitHub Gaug.es l'implémentera.

Ils définissent d'abord un XML d'animation:

<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" />

Maintenant, définissez une disposition pour la vue d'action:

<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" />

Tout ce que nous devons faire est d'activer cette vue chaque fois que vous cliquez sur l'élément:

 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
 }

Lorsque le chargement est terminé, arrêtez simplement l'animation et effacez la vue:

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

Et tu as fini!

Quelques choses supplémentaires à faire:

  • Mettez en cache l'inflation de la disposition de la vue d'action et l'inflation de l'animation. Ils sont lents, vous ne voulez donc les faire qu'une seule fois.
  • Ajouter des nullenregistrementscompleteRefresh()

Voici la demande de tirage sur l'application: https://github.com/github/gauges-android/pull/13/files

Jake Wharton
la source
2
Bonne réponse, cependant getActivity () n'est plus accessible, utilisez plutôt getApplication ().
theAlse
8
@Alborz C'est entièrement spécifique à votre application et non une règle générale. Tout dépend de l'endroit où vous placez la méthode d'actualisation.
Jake Wharton
1
Cela fonctionne-t-il également avec l'ActionBar normal (sans ActionBar Sherlock)? Pour moi, l'icône saute vers la gauche lorsque l'animation démarre et n'est plus cliquable par la suite. EDIT: Je viens de découvrir que la configuration de l'ActionView provoque cela, pas l'animation elle-même.
Afficher le nom
2
Il ne devrait y avoir aucun saut si votre image est carrée et de la bonne taille pour un élément d'action.
Jake Wharton
13
J'ai également eu des problèmes avec le bouton sautant sur le côté lors de la mise en œuvre de cela. C'est parce que je n'utilisais pas le style Widget.Sherlock.ActionButton. J'ai corrigé cela en ajoutant android:paddingLeft="12dp"et android:paddingRight="12dp"à mon propre thème.
William Carter
16

J'ai travaillé un peu sur la solution en utilisant ActionBarSherlock, j'ai trouvé ceci:

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>

Code en activité (je l'ai dans la classe parent 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);
    }
}

dans l'activité création d'éléments de 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();
}

Et l'icône, c'est drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi / ic_menu_navigation_refresh.png

Cela peut être trouvé dans http://developer.android.com/design/downloads/index.html#action-bar-icon-pack

Marek Sebera
la source
Pour info, j'ai dû également ajouter le layout-tvdpi-v11 / indeterminate_progress_action.xml avec un android: layout_marginRight = "16dp", afin d'afficher correctement. Je ne sais pas si cette incohérence est un bogue dans mon code, ABS ou le SDK.
Iraklis
J'ai testé cette solution avec quelques-unes de mes applications, et toutes utilisent le même code. Donc, je suppose que c'est une incohérence dans votre code, car ABS (4.2.0) et SDK (API 14 et supérieur) sont partagés ;-)
Marek Sebera
L'avez-vous essayé dans un Nexus 7? (pas un emu, un vrai appareil) C'est le seul appareil qui ne s'affiche pas correctement, d'où les paramètres tvdpi.
Iraklis
@Iraklis non, je n'ai pas un tel appareil. Alors oui, maintenant je vois, ce que vous avez débogué. Super, n'hésitez pas à l'ajouter pour répondre.
Marek Sebera
6

En plus de ce que Jake Wharton a dit, vous devriez probablement faire ce qui suit pour vous assurer que l'animation s'arrête en douceur et ne saute pas. le chargement terminé.

Tout d'abord, créez un nouveau booléen (pour toute la classe):

private boolean isCurrentlyLoading;

Trouvez la méthode qui démarre votre chargement. Définissez votre valeur booléenne sur true lorsque l'activité commence à se charger.

isCurrentlyLoading = true;

Trouvez la méthode qui démarre lorsque votre chargement est terminé. Au lieu d'effacer l'animation, définissez votre valeur booléenne sur false.

isCurrentlyLoading = false;

Définissez un AnimationListener sur votre animation:

animationRotate.setAnimationListener(new AnimationListener() {

Ensuite, chaque fois que l'animation a été exécutée une fois, cela signifie que lorsque votre icône a effectué une rotation, vérifiez l'état de chargement, et si ce n'est plus le cas, l'animation s'arrêtera.

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

De cette façon, l'animation ne peut être arrêtée que si elle a déjà tourné jusqu'à la fin et sera répétée sous peu ET qu'elle ne se charge plus.

C'est du moins ce que j'ai fait quand j'ai voulu mettre en œuvre l'idée de Jake.

Lesik2008
la source
2

Il existe également une option pour créer la rotation dans le code. Snip complet:

    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);
Alen Siljak
la source
En utilisant ceci et le robinet surOptionsItemSelected ne sont pas appelés.
pseudozach le
1

Avec la bibliothèque de support, nous pouvons animer l'icône sans actionView personnalisée.

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

Nous ne pouvons pas animer dessiné directement, utilisez donc DrawableWrapper (à partir de android.support.v7 pour l'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;
    }
}

J'ai pris l'idée de DrawableWrapper d'ici: https://stackoverflow.com/a/39108111/5541688

Anrimien
la source
0

sa solution très simple (par exemple, besoin d'un refactor) fonctionne avec MenuItem standart, vous pouvez l'utiliser avec n'importe quel nombre d'états, d'icônes, d'animations, de logique, etc.

en classe d'activité:

private enum RefreshMode {update, actual, outdated} 

auditeur standard:

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

dans refreshData () faites quelque chose comme ceci:

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

méthode pour définir la couleur ou l'animation de l'icône:

 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: {
        }
    }
}

il y a 2 mises en page avec l'icône (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>

animation de rotation standard dans ce cas (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"
/>
Андрей К
la source
0

le meilleur moyen est ici:

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

puis:

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

N'oubliez pas que vous ne pouvez pas accéder à "btsync = this.findViewById (R.id.action_sync);" à onCreate () ou onStart () ou onResume () ou onPostResume () ou onPostCreate () ou onCreateOptionsMenu () ou onPrepareOptionsMenu () si vous voulez l'obtenir juste après le début de l'activité, mettez-le dans un postdélayé:

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);
}
M Kasesang
la source