Анимированный значок для 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 отображается со значком и без текста, однако, когда пользователь нажимает на ActionItem, я хочу, чтобы значок начал анимацию, точнее, вращение на месте. Рассматриваемый значок - это значок обновления.

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

Итак, моя следующая попытка заключалась в том, чтобы попытаться использовать AnimationDrawable и определить мою анимацию покадрово, установить drawable как значок для пункта меню, а затем onOptionsItemSelected(MenuItem item)получить значок и начать анимацию с использованием ((AnimationDrawable)item.getIcon()).start(). Однако это было безуспешно. Кто-нибудь знает, как добиться такого эффекта?

Алекс Фу
источник

Ответы:

174

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

Джейк Уортон
источник
1
Отлично! Я следовал методу, используемому на странице документации Android: добавление ActionView, и с помощью этого метода весь AB блокируется представлением. Еще раз спасибо!
Alex Fu
2
Отличный ответ, однако getActivity () больше не доступен, вместо этого используйте getApplication ().
theAlse
8
@Alborz Это полностью специфично для вашего приложения, а не является общим правилом. Все зависит от того, где вы размещаете метод обновления.
Jake Wharton
2
Не должно быть скачка, если ваше изображение квадратное и правильного размера для элемента действия.
Джейк Уортон
14
У меня также были проблемы с прыжком кнопки в сторону при реализации этого. Это потому, что я не использовал стиль Widget.Sherlock.ActionButton. Я исправил это, добавив android:paddingLeft="12dp"и android:paddingRight="12dp"в свою тему.
Уильям Картер,
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>

рез / вытяжка / ротация_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
вытяжка-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.
Ираклис
Я тестировал это решение с несколькими своими приложениями, и все они используют один и тот же код. Так что я предполагаю, что это некоторая несогласованность в вашем коде, поскольку ABS (4.2.0) и SDK (API 14 и выше) являются общими ;-)
Марек Себера,
Вы пробовали это в Nexus 7? (не эму, а реальное устройство) Это единственное устройство, которое не отображается должным образом, отсюда и настройки tvdpi.
Ираклис
@Iraklis нет, у меня нет такого устройства. Итак, да, теперь я понимаю, что вы отладили. Отлично, не стесняйтесь добавлять его в ответ.
Марек Себера
6

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

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

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

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

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

Лесик2008
источник
2

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

    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);
Ален Сильжак
источник
Использование этого и касание onOptionsItemSelected не вызывается.
псевдозач
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();
}

Мы не можем напрямую анимировать 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

Анримиан
источник
0

мое очень простое решение (например, требуется некоторый рефакторинг) работает со стандартным 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"
/>
Андрей К
источник
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));
    }
}

Помните, что вы не можете получить доступ к "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);
}
M Kasesang
источник