ActionItem 的动画图标

我一直在到处寻找解决问题的方法,但似乎还没有找到。我有一个 ActionBar (ActionBarSherlock) ,它的菜单是从一个 XML 文件膨胀起来的,该菜单包含一个项目,其中一个项目显示为 ActionItem。

菜单:

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

活动:

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

当用户单击 ActionItem 时,ActionItem 将显示一个图标,但不显示文本,我希望图标开始动画,更具体地说,就地旋转。该图标是一个刷新图标。

我意识到 ActionBar 支持使用自定义视图(添加操作视图) ,但是这个自定义视图扩展到了 ActionBar 的整个区域,实际上阻止了除应用程序图标之外的所有东西,在我的例子中,这不是我想要的。

因此,我的下一个尝试是尝试使用 AnimationDrawable 并逐帧定义我的动画,设置绘图为菜单项的图标,然后在 onOptionsItemSelected(MenuItem item)中获取图标并使用 ((AnimationDrawable)item.getIcon()).start()开始动画。然而,这是不成功的。有人知道如何达到这个效果吗?

67227 次浏览

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

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

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.

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

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

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

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