web-dev-qa-db-fra.com

Icône animée pour ActionItem

J'ai cherché partout une solution appropriée à mon problème et je n'arrive pas à en trouver une pour le moment. J'ai un 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 aucun texte. Cependant, lorsqu'un utilisateur clique sur l'ActionItem, je souhaite que l'icône commence à animer, plus précisément, à la rotation sur place. L'icône en question est une icône d'actualisation.

Je me rends compte que ActionBar prend en charge l’utilisation de vues personnalisées ( Ajouter une vue Action ), mais cette vue personnalisée est étendue à l’ensemble de la zone de la barre d’action et bloque tout, sauf l’icône de l’application, qui dans mon cas n’est Je cherchais. 

Ainsi, ma prochaine tentative a été d'essayer d'utiliser AnimationDrawable et de définir mon animation image par image, de définir l'icône dessinable comme icône de l'élément de menu, puis d'obtenir l'icône onOptionsItemSelected(MenuItem item) et de commencer à l'animer à l'aide de ((AnimationDrawable)item.getIcon()).start(). Cela n'a cependant pas abouti. Est-ce que quelqu'un connaît un moyen d'accomplir cet effet?

90
Alex Fu

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

Ils définissent d'abord une animation XML:

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

Définissez maintenant une présentation 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 avons à faire est d’activer cette vue chaque fois que l’élément est cliqué:

 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:

  • Cachez l'inflation de la mise en page de la vue d'action et celle de l'animation. Ils sont lents, donc vous ne voulez les faire qu’une fois.
  • Ajouter des contrôles null dans completeRefresh()

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

171
Jake Wharton

J'ai travaillé un peu sur la solution en utilisant ActionBarSherlock, je suis venu avec 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 parente 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éant des é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 pourrait être trouvé dans http://developer.Android.com/design/downloads/index.html#action-bar-icon-pack

16
Marek Sebera

En plus de ce que Jake Wharton a dit, vous devriez effectuer les opérations suivantes pour vous assurer que l'animation s'arrête en douceur et ne saute pas dès que le chargement est terminé.

Commencez par créer un nouveau booléen (pour toute la classe):

private boolean isCurrentlyLoading;

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

isCurrentlyLoading = true;

Recherchez la méthode qui est lancée lorsque votre chargement est terminé. Au lieu d'effacer l'animation, définissez votre booléen 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 effectue une rotation, vérifiez l'état de chargement et, si vous ne le chargez plus, l'animation s'arrête.

@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 elle ne se charge plus.

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

6
Lesik2008

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

    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);
1
Alen Siljak

Avec la bibliothèque de support, nous pouvons animer des icônes 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 directement Drawable, utilisez donc DrawableWrapper (à partir de Android.support.v7 pour 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 à partir d'ici: https://stackoverflow.com/a/39108111/5541688

0
Anrimian

c'est ma solution très simple (par exemple, besoin d'un refactor) fonctionne avec MenuItem standard, vous pouvez l'utiliser avec n'importe quel nombre d'états, icônes, animations, logiques, etc.

en classe d'activité: 

private enum RefreshMode {update, actual, outdated} 

auditeur standart:

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

dans refreshData () faire quelque chose comme ça:

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

méthode pour définir la couleur ou l'animation pour 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 dispositions 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>

standart rotation d'animation 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"
/>
0
Андрей К