Анимированный значок для 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(). Однако это было безуспешно. Кто-нибудь знает, как добиться такого эффекта?


person Alex Fu    schedule 16.03.2012    source источник


Ответы (7)


Вы на правильном пути. Вот как это будет реализовывать приложение 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);
}

И вы сделали!

Еще кое-что, что нужно сделать:

  • Кэшируйте инфляцию макета вида действия и инфляцию анимации. Они медленные, поэтому вам нужно сделать их только один раз.
  • Добавить null чеков в completeRefresh()

Вот запрос на перенос в приложении: https://github.com/github/gauges-android/pull/13/files

person Jake Wharton    schedule 16.03.2012
comment
Идеально! Я следовал методу, используемому на странице документов Android: Добавление ActionView и с помощью этого метода весь AB блокируется представлением. Еще раз спасибо! - person Alex Fu; 16.03.2012
comment
Отличный ответ, однако getActivity () больше не доступен, вместо этого используйте getApplication (). - person theAlse; 16.06.2012
comment
@Alborz Это относится исключительно к вашему приложению, а не к общему правилу. Все зависит от того, где вы размещаете метод обновления. - person Jake Wharton; 16.06.2012
comment
Работает ли это и с обычным ActionBar (без ActionBar Sherlock)? Для меня значок скачет влево, когда начинается анимация, и после этого больше не активируется. РЕДАКТИРОВАТЬ: только что выяснилось, что это вызывает установка ActionView, а не сама анимация. - person Display name; 17.08.2012
comment
Не должно быть скачка, если ваше изображение квадратное и правильного размера для элемента действия. - person Jake Wharton; 17.08.2012
comment
Это решение работает, но я бы предпочел такой метод, как MenuItem.setAnimation (), который можно установить / очистить в Activity.onPrepareOptionsMenu (). Теперь мне нужно отслеживать два места, где я определяю значок для своего элемента ActionBar. :( - person Diederik; 15.10.2012
comment
@JakeWharton, ваше решение отлично работает, но если я попытаюсь заменить ImageView на ImageButton, я вижу, что выделенный квадрат imageButton вращается вместе с изображением, и он не смог обнаружить, когда я нажимаю на кнопку поворота - person user1026605; 05.11.2012
comment
@Seppl Не могли бы вы решить эту проблему? Я использую ActionBarSherlock, но когда я нажимаю пункт меню, происходит то же, что и вы, - person Archie.bpgc; 24.11.2012
comment
Я получаю эту ошибку при добавлении анимации на Android 2.3 nopaste.info/612f9e08e5_nl.html (причина в эта строка: style=@style/Widget.Sherlock.ActionButton) Вы знаете, почему я получаю эту ошибку? - person patrick; 04.12.2012
comment
У меня также были проблемы с прыжком кнопки в сторону при реализации этого. Это потому, что я не использовал стиль Widget.Sherlock.ActionButton. Я исправил это, добавив android:paddingLeft="12dp" и android:paddingRight="12dp" в свою тему. - person William Carter; 26.02.2013
comment
Кто-нибудь знает решение для Android 2.2? setActionView () доступен только с уровнем API 11 или выше. Я использую android-support-v7-appcompat. - person samo; 28.07.2013
comment
@samo: используйте Menu из com.actionbarsherlock.view вместо значения по умолчанию. - person Ε Г И І И О; 29.07.2013
comment
@Seppl Для нормальной панели действий это решение позволяет избежать скачков: stackoverflow.com/questions/19139775/ - person Bart Friederichs; 07.10.2013
comment
@samo вы должны использовать android.support.v4.view.MenuItemCompat: refreshItem.setActionView (iv); станет MenuItemCompat.setActionView (refreshItem, iv); - person gorodezkiy; 12.12.2013
comment
@WilliamCarter Для решения этой проблемы не нужно использовать отступы, просто измените стиль, чтобы не использовать стиль Шерлока. Я использовал style="@style/Widget.AppCompat.ActionButton", чтобы исправить эту проблему. - person Yoann Hercouet; 26.09.2016

Я немного поработал над решением, используя 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

person Marek Sebera    schedule 16.01.2013
comment
К вашему сведению, мне также пришлось добавить layout-tvdpi-v11 / indeterminate_progress_action.xml с android: layout_marginRight = 16dp для правильного отображения. Я не знаю, является ли это несоответствие ошибкой в ​​моем коде, ABS или SDK. - person Iraklis; 19.02.2013
comment
Я тестировал это решение с несколькими своими приложениями, и все они используют один и тот же код. Я предполагаю, что это некоторая несогласованность в вашем коде, так как ABS (4.2.0) и SDK (API 14 и выше) являются общими ;-) - person Marek Sebera; 19.02.2013
comment
Вы пробовали это в Nexus 7? (не эму, а настоящее устройство) Это единственное устройство, которое не отображается должным образом, отсюда и настройки tvdpi. - person Iraklis; 19.02.2013
comment
@Iraklis нет, у меня нет такого устройства. Итак, да, теперь я понимаю, что вы отладили. Отлично, не стесняйтесь добавлять его в ответ. - person Marek Sebera; 19.02.2013

В дополнение к тому, что сказал Джейк Уортон, вам, вероятно, следует сделать следующее, чтобы анимация останавливалась плавно и не перескакивала после завершения загрузки.

Сначала создайте новое логическое значение (для всего класса):

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

Таким образом, анимация может быть остановлена ​​только в том случае, если она уже повернута до конца, и вскоре будет повторяться, И она больше не загружается.

По крайней мере, это то, что я сделал, когда хотел реализовать идею Джейка.

person Lesik2008    schedule 19.02.2014

Также есть возможность создать ротацию в коде. Полный фрагмент:

    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);
person Alen Siljak    schedule 27.08.2016
comment
Использование этого и касание onOptionsItemSelected не вызывается. - person pseudozach; 22.04.2020
comment
Я не удивлюсь, если с 2016 года в Android произошла масса изменений, когда на этот вопрос был дан ответ. Обратите внимание, что это код реального приложения с открытым исходным кодом, и это работало в то время. - person Alen Siljak; 14.04.2021

С помощью библиотеки поддержки мы можем анимировать значок без настраиваемого 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();
}

Мы не можем напрямую анимировать drawable, поэтому используйте DrawableWrapper (из android.support.v7 для 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;
    }
}

Я взял идею DrawableWrapper отсюда: https://stackoverflow.com/a/39108111/5541688

person Anrimian    schedule 08.12.2017

мое очень простое решение (например, требуется некоторый рефакторинг) работает со стандартным MenuItem, вы можете использовать его с любым количеством состояний, значков, анимаций, логики и т. д.

в классе активности:

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

есть 2 макета со значком (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"
/>
person Andrei K    schedule 20.12.2018

лучший способ здесь:

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

Помните, что вы не можете получить доступ к "btsync = this.findViewById (R.id.action_sync);" в onCreate () или onStart () или onResume () или onPostResume () или onPostCreate () или onCreateOptionsMenu () или onPrepareOptionsMenu (), если вы хотите получить его сразу после начала активности, поместите его в 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);
}
person M Kasesang    schedule 21.12.2019