Animiertes Symbol für ActionItem

91

Ich habe überall nach einer geeigneten Lösung für mein Problem gesucht und kann noch keine finden. Ich habe eine ActionBar (ActionBarSherlock) mit einem Menü, das aus einer XML-Datei aufgeblasen wird. Dieses Menü enthält ein Element und dieses Element wird als ActionItem angezeigt.

Speisekarte:

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

Aktivität:

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

Das ActionItem wird mit einem Symbol und ohne Text angezeigt. Wenn ein Benutzer jedoch auf das ActionItem klickt, soll das Symbol animiert werden, genauer gesagt, an Ort und Stelle gedreht werden. Das betreffende Symbol ist ein Aktualisierungssymbol.

Mir ist klar, dass ActionBar die Verwendung von benutzerdefinierten Ansichten unterstützt ( Hinzufügen einer Aktionsansicht ). Diese benutzerdefinierte Ansicht wird jedoch erweitert, um den gesamten Bereich der ActionBar abzudecken, und blockiert tatsächlich alles außer dem App-Symbol, das in meinem Fall nicht das ist, wonach ich gesucht habe .

Mein nächster Versuch war also, AnimationDrawable zu verwenden und meine Animation Frame für Frame zu definieren, das Drawable als Symbol für den Menüpunkt festzulegen und dann onOptionsItemSelected(MenuItem item)das Symbol abzurufen und mit der Animation zu beginnen ((AnimationDrawable)item.getIcon()).start(). Dies war jedoch erfolglos. Kennt jemand einen Weg, um diesen Effekt zu erzielen?

Alex Fu
quelle

Antworten:

173

Du bist auf dem richtigen Weg. So wird es von der GitHub Gaug.es-App implementiert.

Zuerst definieren sie eine Animations-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" />

Definieren Sie nun ein Layout für die Aktionsansicht:

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

Alles, was wir tun müssen, ist diese Ansicht zu aktivieren, wenn auf das Element geklickt wird:

 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
 }

Wenn der Ladevorgang abgeschlossen ist, stoppen Sie einfach die Animation und löschen Sie die Ansicht:

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

Und du bist fertig!

Einige zusätzliche Dinge zu tun:

  • Cache das Aufblasen des Aktionsansichtslayouts und das Aufblasen der Animation. Sie sind langsam, sodass Sie sie nur einmal ausführen möchten.
  • In nullKontrollen incompleteRefresh()

Hier ist die Pull-Anfrage für die App: https://github.com/github/gauges-android/pull/13/files

Jake Wharton
quelle
2
Gute Antwort, aber auf getActivity () kann nicht mehr zugegriffen werden. Verwenden Sie stattdessen getApplication ().
theAlse
8
@Alborz Das ist ganz spezifisch für Ihre App und keine allgemeine Regel. Es hängt alles davon ab, wo Sie die Aktualisierungsmethode platzieren.
Jake Wharton
1
Funktioniert dies auch mit der normalen ActionBar (ohne ActionBar Sherlock)? Für mich springt das Symbol zu Beginn der Animation nach links und kann danach nicht mehr angeklickt werden. BEARBEITEN: Ich habe gerade herausgefunden, dass das Einstellen der ActionView dies verursacht, nicht die Animation selbst.
Anzeigename
2
Es sollte keinen Sprung geben, wenn Ihr Bild quadratisch ist und die richtige Größe für ein Aktionselement hat.
Jake Wharton
13
Ich hatte auch Probleme damit, dass der Knopf zur Seite sprang, als ich dies implementierte. Dies liegt daran, dass ich den Widget.Sherlock.ActionButton-Stil nicht verwendet habe. Ich habe dies korrigiert, indem ich android:paddingLeft="12dp"und android:paddingRight="12dp"zu meinem eigenen Thema hinzugefügt habe.
William Carter
16

Ich habe ein bisschen an der Lösung mit ActionBarSherlock gearbeitet und mir Folgendes ausgedacht:

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 Aktivität (ich habe ihn in der übergeordneten Klasse 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);
    }
}

in Aktivität Erstellen von Menüelementen

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

Und das Symbol ist das drawable-xhdpi/ic_menu_navigation_refresh.png
drawable-xhdpi / ic_menu_navigation_refresh.png

Dies kann unter http://developer.android.com/design/downloads/index.html#action-bar-icon-pack gefunden werden

Marek Sebera
quelle
Zu Ihrer Information, ich musste auch die layout-tvdpi-v11 / indeterminate_progress_action.xml mit einem Android hinzufügen: layout_marginRight = "16dp", um richtig anzuzeigen. Ich weiß nicht, ob diese Inkonsistenz ein Fehler in meinem Code, ABS oder dem SDK ist.
Iraklis
Ich habe diese Lösung mit einigen meiner Anwendungen getestet und alle verwenden denselben Code. Ich denke, dies ist eine Inkonsistenz in Ihrem Code, da ABS (4.2.0) und SDK (API 14 und höher) gemeinsam genutzt werden ;-)
Marek Sebera
Haben Sie es in einem Nexus 7 versucht? (kein Emu, ein echtes Gerät) Es ist das einzige Gerät, das nicht richtig angezeigt wird, daher die tvdpi-Einstellungen.
Iraklis
@Iraklis nein, ich habe kein solches Gerät. Also ja, jetzt verstehe ich, was Sie debuggt haben. Großartig, zögern Sie nicht, es hinzuzufügen, um zu antworten.
Marek Sebera
6

Zusätzlich zu den Aussagen von Jake Wharton sollten Sie Folgendes tun, um sicherzustellen, dass die Animation reibungslos stoppt und nicht herumspringt, sobald das Laden beendet ist.

Erstellen Sie zunächst einen neuen Booleschen Wert (für die gesamte Klasse):

private boolean isCurrentlyLoading;

Suchen Sie die Methode, mit der das Laden gestartet wird. Setzen Sie Ihren Booleschen Wert auf true, wenn die Aktivität geladen wird.

isCurrentlyLoading = true;

Suchen Sie die Methode, die gestartet wird, wenn das Laden abgeschlossen ist. Anstatt die Animation zu löschen, setzen Sie Ihren Booleschen Wert auf false.

isCurrentlyLoading = false;

Legen Sie einen AnimationListener für Ihre Animation fest:

animationRotate.setAnimationListener(new AnimationListener() {

Jedes Mal, wenn die Animation einmal ausgeführt wurde, dh wenn Ihr Symbol eine Umdrehung ausgeführt hat, überprüfen Sie den Ladezustand. Wenn die Animation nicht mehr geladen wird, wird sie gestoppt.

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

Auf diese Weise kann die Animation nur gestoppt werden, wenn sie sich bereits bis zum Ende gedreht hat und in Kürze wiederholt wird UND nicht mehr geladen wird.

Dies ist zumindest das, was ich getan habe, als ich Jakes Idee umsetzen wollte.

Lesik2008
quelle
2

Es gibt auch eine Option zum Erstellen der Rotation im Code. Voller 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);
Alen Siljak
quelle
Wenn Sie dies und das Tippen auf onOptionsItemSelected verwenden, wird dies nicht aufgerufen.
Pseudozach
1

Mit der Support-Bibliothek können wir Symbole ohne benutzerdefinierte actionView animieren.

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

Wir können Drawable nicht direkt animieren. Verwenden Sie daher DrawableWrapper (von android.support.v7 für 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;
    }
}

Ich habe die Idee für DrawableWrapper von hier übernommen: https://stackoverflow.com/a/39108111/5541688

Anrimian
quelle
0

Es ist meine sehr einfache Lösung (zum Beispiel, brauche einen Refactor) funktioniert mit Standard MenuItem, du kannst es mit einer beliebigen Anzahl von Zuständen, Symbolen, Animationen, Logik usw. verwenden.

in der Aktivitätsklasse:

private enum RefreshMode {update, actual, outdated} 

Standard Hörer:

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

in refreshData () mache so etwas:

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

Methode zum Definieren von Farbe oder Animation für Symbol:

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

Es gibt 2 Layouts mit Symbol (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>

Standard-Rotationsanimation in diesem Fall (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"
/>
Андрей К
quelle
0

Der beste Weg ist hier:

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

und dann:

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

Denken Sie daran, dass Sie nicht auf "btsync = this.findViewById (R.id.action_sync)" zugreifen können. at onCreate () oder onStart () oder onResume () oder onPostResume () oder onPostCreate () oder onCreateOptionsMenu () oder onPrepareOptionsMenu ()

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
quelle