Ícono animado para ActionItem

91

He estado buscando en todas partes una solución adecuada a mi problema y parece que todavía no puedo encontrar una. Tengo una ActionBar (ActionBarSherlock) con un menú que se infla desde un archivo XML y ese menú contiene un elemento y ese elemento se muestra como ActionItem.

menú:

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

actividad:

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

ActionItem se muestra con un icono y sin texto; sin embargo, cuando un usuario hace clic en ActionItem, quiero que el icono comience a animarse, más específicamente, a rotar en su lugar. El icono en cuestión es un icono de actualización.

Me doy cuenta de que ActionBar tiene soporte para usar vistas personalizadas ( Agregar una vista de acción ); sin embargo, esta vista personalizada se expande para cubrir toda el área de ActionBar y en realidad bloquea todo excepto el ícono de la aplicación, que en mi caso no es lo que estaba buscando. .

Así que mi siguiente intento fue intentar usar AnimationDrawable y definir mi animación cuadro por cuadro, establecer el elemento de dibujo como el ícono del elemento del menú, y luego onOptionsItemSelected(MenuItem item)obtener el ícono y comenzar a animar usando ((AnimationDrawable)item.getIcon()).start(). Sin embargo, esto no tuvo éxito. ¿Alguien conoce alguna forma de lograr este efecto?

Alex Fu
fuente

Respuestas:

173

Estás en el camino correcto. Así es como la aplicación GitHub Gaug.es lo implementará.

Primero definen un XML de animación:

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

Ahora defina un diseño para la vista de acción:

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

Todo lo que tenemos que hacer es habilitar esta vista cada vez que se haga clic en el elemento:

 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
 }

Cuando finalice la carga, simplemente detenga la animación y borre la vista:

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

¡Y tu estas listo!

Algunas cosas adicionales para hacer:

  • Almacene en caché la inflación del diseño de la vista de acción y la inflación de la animación. Son lentos, por lo que solo querrás hacerlos una vez.
  • Agregar nullcheques encompleteRefresh()

Aquí está la solicitud de extracción en la aplicación: https://github.com/github/gauges-android/pull/13/files

Jake Wharton
fuente
2
Gran respuesta, sin embargo, getActivity () ya no es accesible, use getApplication () en su lugar.
theAlse
8
@Alborz Eso es completamente específico para su aplicación y no una regla general. Todo depende de dónde coloque el método de actualización.
Jake Wharton
1
¿Funciona esto también con ActionBar normal (sin ActionBar Sherlock)? Para mí, el icono salta a la izquierda cuando comienza la animación y ya no se puede hacer clic en él. EDITAR: Acabo de descubrir que configurar ActionView causa esto, no la animación en sí.
Nombre para mostrar el
2
No debería haber ningún salto si su imagen es cuadrada y tiene el tamaño correcto para un elemento de acción.
Jake Wharton
13
También tuve problemas con el botón saltando hacia un lado al implementar esto. Esto se debe a que no estaba usando el estilo Widget.Sherlock.ActionButton. Corregí esto agregando android:paddingLeft="12dp"y android:paddingRight="12dp"a mi propio tema.
William Carter
16

Trabajé un poco en la solución usando ActionBarSherlock, se me ocurrió esto:

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

Código en actividad (lo tengo en la clase principal 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);
    }
}

en actividad creando elementos de menú

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

Y el icono, este es drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi / ic_menu_navigation_refresh.png

Esto se puede encontrar en http://developer.android.com/design/downloads/index.html#action-bar-icon-pack

Marek Sebera
fuente
Para su información, también tuve que agregar el layout-tvdpi-v11 / indeterminate_progress_action.xml con un android: layout_marginRight = "16dp", para que se muestre correctamente. No sé si esta inconsistencia es un error en mi código, ABS o el SDK.
Iraklis
He probado esta solución con algunas de mis aplicaciones y todas usan el mismo código. Así que supongo que esto es una inconsistencia en su código, ya que ABS (4.2.0) y SDK (API 14 y superior) son compartidos ;-)
Marek Sebera
¿Lo has probado en un Nexus 7? (no un emu, un dispositivo real) Es el único dispositivo que no se muestra correctamente, de ahí la configuración de tvdpi.
Iraklis
@Iraklis no, no tengo ese dispositivo. Así que sí, ahora veo lo que has depurado. Genial, siéntete libre de agregarlo para responder.
Marek Sebera
6

Además de lo que dijo Jake Wharton, probablemente debería hacer lo siguiente para asegurarse de que la animación se detenga sin problemas y no salte tan pronto como finalice la carga.

Primero, cree un nuevo booleano (para toda la clase):

private boolean isCurrentlyLoading;

Encuentra el método que inicia tu carga. Establezca su booleano en verdadero cuando la actividad comience a cargarse.

isCurrentlyLoading = true;

Busque el método que se inicia cuando finaliza la carga. En lugar de borrar la animación, establezca su booleano en falso.

isCurrentlyLoading = false;

Establezca un AnimationListener en su animación:

animationRotate.setAnimationListener(new AnimationListener() {

Luego, cada vez que se ejecutó la animación una vez, eso significa que cuando su ícono hizo una rotación, verifique el estado de carga y, si no se carga más, la animación se detendrá.

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

De esta manera, la animación solo se puede detener si ya giró hasta el final y se repetirá en breve Y ya no se está cargando.

Esto es al menos lo que hice cuando quise implementar la idea de Jake.

Lesik2008
fuente
2

También hay una opción para crear la rotación en código. Recorte completo:

    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
fuente
No se llama al usar esto y el toque enOptionsItemSelected.
pseudozach
1

Con la biblioteca de soporte podemos animar el icono sin actionView personalizado.

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

No podemos animar dibujable directamente, así que use DrawableWrapper (de android.support.v7 para 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;
    }
}

Tomé la idea de DrawableWrapper desde aquí: https://stackoverflow.com/a/39108111/5541688

Anrimian
fuente
0

Es mi solución muy simple (por ejemplo, necesito una refactorización) funciona con MenuItem estándar, puede usarlo con cualquier número de estados, íconos, animaciones, lógica, etc.

en clase de actividad:

private enum RefreshMode {update, actual, outdated} 

oyente estándar:

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

en refreshData () haga algo como esto:

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

método para definir el color o la animación del icono:

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

hay 2 diseños con el icono (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 en este caso (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"
/>
Андрей К
fuente
0

la mejor manera es aquí:

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

y entonces:

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

Recuerde que no puede acceder a "btsync = this.findViewById (R.id.action_sync);" en onCreate () o onStart () o onResume () o onPostResume () o onPostCreate () o onCreateOptionsMenu () o onPrepareOptionsMenu () si desea obtenerlo justo después de que comience la actividad, póngalo en un postratamiento:

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
fuente