动画图标用于ActionItem

91

我已经到处搜索如何解决我的问题,但似乎还找不到一个合适的解决方案。我有一个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时,该项将显示一个图标但不显示文本。我希望在这时图标开始动画,具体来说是旋转。所使用的图标是一个刷新图标。

我了解ActionBar支持使用自定义视图(添加操作视图),但此自定义视图会扩展以覆盖整个ActionBar区域,并且实际上会阻止除应用程序图标之外的所有内容,而在我的情况下,这并不是我想要的。

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

7个回答

176

你正在正确的道路上。这是GitHub Gaug.es应用程序将如何实现它。

首先,他们定义了一个动画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" />

现在为操作视图定义一个布局:

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

我们所需做的就是在项目被点击时启用此视图:

 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
 }

当加载完成时,只需停止动画并清除视图:

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

完成了!

还有一些额外的事情要做:

  • 缓存操作视图布局膨胀和动画膨胀。它们很慢,所以您只需要执行一次。
  • completeRefresh() 中添加 null 检查

这是应用程序上的拉取请求:https://github.com/github/gauges-android/pull/13/files


2
很好的答案,不过现在无法访问 getActivity(),请改用 getApplication()。 - theAlse
8
这完全取决于你的应用程序,而不是一般规则。这完全取决于你放置刷新方法的位置。 - Jake Wharton
1
这个能够在正常的ActionBar上工作吗(不使用ActionBar Sherlock)?对我来说,当动画开始时图标会跳到左边,之后就无法再点击了。编辑:刚刚发现是设置ActionView导致的,而不是动画本身。 - Display name
2
如果您的图像是正方形且大小适合操作项,则不应跳转。 - Jake Wharton
14
在实施这个功能时,我也遇到了按钮跳转到侧边的问题。原因是我没有使用Widget.Sherlock.ActionButton样式。为了解决这个问题,我在自己的主题中添加了android:paddingLeft="12dp"android:paddingRight="12dp" - William Carter
显示剩余10条评论

16

我使用ActionBarSherlock尝试解决了一些问题,我得到了以下代码:

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>

在活动中的代码(我将其放在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);
    }
}

在创建活动时创建菜单项

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();
}
而这个图标,是 drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi/ic_menu_navigation_refresh.png

你可以在 http://developer.android.com/design/downloads/index.html#action-bar-icon-pack 找到它。


顺便提一下,我不得不添加layout-tvdpi-v11 / indeterminate_progress_action.xml,并设置android:layout_marginRight =“16dp”,以使其正确显示。我不知道这个不一致性是我的代码,ABS还是SDK中的错误。 - Iraklis
我已经测试了我的几个应用程序,它们都使用相同的代码。因此,我猜测这是您的代码存在一些不一致性,因为 ABS(4.2.0)和 SDK(API 14及更高版本)是共享的;-) - Marek Sebera
你在Nexus 7上试过了吗?(不是模拟器,是真实设备)这是唯一一个显示不正确的设备,因此需要使用tvdpi设置。 - Iraklis
@Iraklis 不,我没有这样的设备。所以现在我明白你调试了什么。很好,随意将其添加到答案中。 - Marek Sebera

7
除了Jake Wharton所说的内容,为确保动画平滑停止且在加载完成后不会跳动,您应该执行以下操作:
第一步,创建一个新布尔变量(针对整个类):
private boolean isCurrentlyLoading;

找到启动加载的方法。当活动开始加载时,将布尔值设置为true。

isCurrentlyLoading = true;

找到在加载完成时启动的方法。不要清除动画,将布尔值设置为false。
isCurrentlyLoading = false;

在你的动画上设置一个AnimationListener:

animationRotate.setAnimationListener(new AnimationListener() {

然后,每次动画执行一次,也就是当您的图标旋转一次时,检查加载状态,如果不再加载,则停止动画。

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

这样做的话,动画只有在已经旋转到末尾并且很快就要重复播放,并且不再加载时才能停止。

至少这是我想实现Jake的想法时所做的事情。


1
使用支持库,我们可以在不使用自定义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();
}

我们无法直接对可绘制对象进行动画处理,因此请使用DrawableWrapper(适用于API<21的android.support.v7):

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

我从这里得到了DrawableWrapper的想法:https://dev59.com/sm865IYBdhLWcg3wceRU#39108111

1

还有一种选项是在代码中创建旋转。完整代码片段:

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

使用这个后,tap onOptionsItemSelected 不会被调用。 - pseudozach
自从2016年回答这个问题以来,如果Android发生了大量变化,我不会感到惊讶。请注意,这是一个实际开源应用程序的代码,并且在当时已经可以工作。 - Alen Siljak

0

这是我非常简单的解决方案(例如,需要一些重构)适用于标准 MenuItem,您可以使用它来处理任意数量的状态、图标、动画、逻辑等。

在 Activity 类中:

private enum RefreshMode {update, actual, outdated} 

标准监听器:

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

在refreshData()函数中,做如下操作:
setRefreshIcon(RefreshMode.update);
// update your data
setRefreshIcon(RefreshMode.actual);

定义图标颜色或动画的方法:

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

有两个带图标的布局(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>

在这种情况下,标准旋转动画(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

最好的方法在这里:

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

然后:

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

请记住,在 onCreate() 或 onStart() 或 onResume() 或 onPostResume() 或 onPostCreate() 或 onCreateOptionsMenu() 或 onPrepareOptionsMenu() 中无法访问 "btsync = this.findViewById(R.id.action_sync);"。如果您想在活动开始后立即获取它,请将其放入 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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接