91

I have been searching everywhere for a proper solution to my problem and I can't seem to find one yet. I have an ActionBar (ActionBarSherlock) with a menu that is inflated from an XML file and that menu contains one item and that one item is shown as an 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>

activity:

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

The ActionItem is displayed with an icon and no text however when a user clicks on the ActionItem, I want the icon to begin animating, more specifically, rotating in place. The icon in question is a refresh icon.

I realize that ActionBar has support for using custom views (Adding an Action View) however this custom view is expanded to cover the entire area of the ActionBar and actually blocks everything except the app icon, which in my case is not what I was looking for.

So my next attempt was to try to use AnimationDrawable and define my animation frame-by-frame, set the drawable as the icon for the menu item, and then in onOptionsItemSelected(MenuItem item) get the icon and begin animating using ((AnimationDrawable)item.getIcon()).start(). This however was unsuccessful. Does anyone know of any way to accomplish this effect?

Alex Fu
  • 5,509
  • 3
  • 31
  • 40

7 Answers7

176

You're on the right track. Here is how the GitHub Gaug.es app will be implementing it.

First they define an 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" />

Now define a layout for the action view:

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

All we need to do is enable this view whenever the item is clicked:

 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
 }

When the loading is done, simply stop the animation and clear the view:

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

And you're done!

Some additional things to do:

  • Cache the action view layout inflation and animation inflation. They are slow so you only want to do them once.
  • Add null checks in completeRefresh()

Here's the pull request on the app: https://github.com/github/gauges-android/pull/13/files

Ahmad Kayyali
  • 8,233
  • 13
  • 49
  • 83
Jake Wharton
  • 75,598
  • 23
  • 223
  • 230
  • 1
    Perfect! I had been following the method used on the Android docs page: [Adding an ActionView](http://developer.android.com/guide/topics/ui/actionbar.html#ActionView) and with that method the whole AB gets blocked by the view. Thank you again! – Alex Fu Mar 16 '12 at 05:38
  • 2
    Great answer, however getActivity() is not accessible any more, use getApplication() instead. – theAlse Jun 16 '12 at 09:42
  • 8
    @Alborz That is entirely specific to your app and not a general rule. It's all dependent on where you are placing the refresh method. – Jake Wharton Jun 16 '12 at 09:45
  • 1
    Does this work with the normal ActionBar as well (without ActionBar Sherlock) ? For me the icon jumpes to the left when the animation starts and isn't clickable anymore afterwards. EDIT: Just found out that setting the ActionView causes this, not the animation itself. – Display name Aug 17 '12 at 08:07
  • 2
    There should be no jump if your image is square and the correct size for an action item. – Jake Wharton Aug 17 '12 at 14:24
  • This solution works, but I would have preferred a method like MenuItem.setAnimation() that can be set/cleared in Activity.onPrepareOptionsMenu(). Now I have to keep track of the two places where I define the icon for my ActionBar item. :( – Diederik Oct 15 '12 at 12:48
  • @JakeWharton your solution works great, but if I try to replace the ImageView with an ImageButton, I can see the imageButton highlighted square rotating with the image and it failed to detect when I press on the rotating button – user1026605 Nov 05 '12 at 18:40
  • @Seppl Could you solve that issue? I am using ActionBarSherlock, still when i click the Menu Item same as you stated happens – Archie.bpgc Nov 24 '12 at 07:29
  • I get this error when adding the animation on Android 2.3 http://nopaste.info/612f9e08e5_nl.html (the cause is this line: style="@style/Widget.Sherlock.ActionButton") Do you know why I get this error? – patrick Dec 04 '12 at 00:37
  • 14
    I also had problems with the button jumping to the side when implementing this. This is because I was not using the Widget.Sherlock.ActionButton style. I corrected this by adding `android:paddingLeft="12dp"` and `android:paddingRight="12dp"` to my own theme. – William Carter Feb 26 '13 at 17:14
  • Does someone know a solution for Android 2.2? setActionView() is only available with API level 11 or higher. I'm using android-support-v7-appcompat. – samo Jul 28 '13 at 18:17
  • @samo: Use `Menu` from `com.actionbarsherlock.view` instead of the default one. – Ε Г И І И О Jul 29 '13 at 16:56
  • @Seppl For a "normal" actionbar, this solution works to avoid the "jumping": http://stackoverflow.com/questions/19139775/animated-menu-item-jumps-when-animation-starts/19225651#19225651 – Bart Friederichs Oct 07 '13 at 13:03
  • @samo you should use *android.support.v4.view.MenuItemCompat*: **refreshItem.setActionView(iv);** will become **MenuItemCompat.setActionView(refreshItem,iv);** – gorodezkiy Dec 11 '13 at 20:04
  • @WilliamCarter You don't need to use a padding to fix this problem, just change the style to avoid using the Sherlock one. I used `style="@style/Widget.AppCompat.ActionButton"` to fix this problem. – Yoann Hercouet Sep 26 '16 at 10:42
16

I've worked a bit on solution using ActionBarSherlock, I've came up with this:

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 in activity (I have it in ActivityWithRefresh parent class)

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

in activity creating menu items

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

And the icon, this is drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi/ic_menu_navigation_refresh.png

This could be found in http://developer.android.com/design/downloads/index.html#action-bar-icon-pack

Marek Sebera
  • 39,650
  • 37
  • 158
  • 244
  • FYI I had to also add the layout-tvdpi-v11/indeterminate_progress_action.xml with a android:layout_marginRight="16dp", in order to display properly. I don't know if this inconsistency isi a bug in my code, ABS, or the SDK. – Iraklis Feb 19 '13 at 12:34
  • I have tested this solution with few of my applications, and all of them use the same code. So I guess this is some inconsistency in your code, as ABS (4.2.0) and SDK (API 14 and higher) are shared ;-) – Marek Sebera Feb 19 '13 at 13:23
  • Have you tried it in a Nexus 7? (not an emu, a real device) Its the only device its not displaying properly , hence the tvdpi settings. – Iraklis Feb 19 '13 at 15:32
  • @Iraklis nope, I don't have such device. So yes, now I see, what you have debugged. Great, feel free to add it to answer. – Marek Sebera Feb 19 '13 at 15:36
7

In addition to what Jake Wharton said, you should propably do the following to ensure that the animation stops smoothly and does not jump around as soon as the loading finished.

First, create a new boolean (for the whole class):

private boolean isCurrentlyLoading;

Find the method that starts your loading. Set your boolean to true when the activity starts loading.

isCurrentlyLoading = true;

Find the method that is started when your loading is finished. Instead of clearing the animation, set your boolean to false.

isCurrentlyLoading = false;

Set an AnimationListener on your animation:

animationRotate.setAnimationListener(new AnimationListener() {

Then, each time the animation was executed one time, that means when your icon made one rotation, check the loading state, and if not loading anymore, the animation will stop.

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

This way, the animation can only be stopped if it already rotated till the end and will be repeated shortly AND it is not loading anymore.

This is at least what I did when I wanted to implement Jake's idea.

Lesik2008
  • 487
  • 1
  • 8
  • 16
1

There is also an option to create the rotation in code. Full 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);
Alen Siljak
  • 2,482
  • 2
  • 24
  • 29
  • Using this and the tap onOptionsItemSelected doesn't get called. – pseudozach Apr 22 '20 at 04:08
  • I would not be surprised if there were tons of changes in Android since 2016, when this was answered. Note that this is the code from an actual Open Source app and this **has** been working at the time. – Alen Siljak Apr 14 '21 at 18:13
1

With support library we can animate icon without custom actionView.

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

We can't animate drawable directly, so use DrawableWrapper(from android.support.v7 for 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;
    }
}

I took idea for DrawableWrapper from here: https://stackoverflow.com/a/39108111/5541688

Anrimian
  • 4,257
  • 4
  • 22
  • 30
0

its my very simple solution (for example, need some refactor) works with standart MenuItem, you can use it with any number of states, icons, animations, logic etc.

in Activity class:

private enum RefreshMode {update, actual, outdated} 

standart listener:

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

into refreshData() do something like this:

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

method for define color or animation for icon:

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

there is 2 layouts with icon (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 rotate animation in this case (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"
/>
Andrei K
  • 141
  • 2
  • 9
0

the best way is here:

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

and then:

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

Remember that u cant access "btsync = this.findViewById(R.id.action_sync);" at onCreate() or onStart() or onResume() or onPostResume() or onPostCreate() or onCreateOptionsMenu() or onPrepareOptionsMenu() if u want get it just after activity start put it in a 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);
}
M Kasesang
  • 49
  • 4