ไอคอนเคลื่อนไหวสำหรับ 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
ไม่ควรกระโดดหากรูปภาพของคุณเป็นสี่เหลี่ยมจัตุรัสและมีขนาดที่ถูกต้องสำหรับรายการการทำงาน
Jake Wharton

14
ฉันยังมีปัญหากับปุ่มที่กระโดดไปด้านข้างเมื่อใช้สิ่งนี้ นี่เป็นเพราะฉันไม่ได้ใช้สไตล์ Widget.Sherlock.ActionButton ฉันแก้ไขสิ่งนี้โดยการเพิ่มandroid:paddingLeft="12dp"และandroid:paddingRight="12dp"ในธีมของฉันเอง
William Carter

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

6

นอกจากสิ่งที่ Jake Wharton กล่าวแล้วคุณควรทำสิ่งต่อไปนี้เพื่อให้แน่ใจว่าภาพเคลื่อนไหวจะหยุดอย่างราบรื่นและไม่กระโดดไปมาทันทีที่การโหลดเสร็จ

ขั้นแรกสร้างบูลีนใหม่ (สำหรับทั้งคลาส):

private boolean isCurrentlyLoading;

ค้นหาวิธีที่เริ่มการโหลดของคุณ ตั้งค่าบูลีนของคุณเป็นจริงเมื่อกิจกรรมเริ่มโหลด

isCurrentlyLoading = true;

ค้นหาวิธีการที่เริ่มต้นเมื่อการโหลดของคุณเสร็จสิ้น แทนที่จะล้างภาพเคลื่อนไหวให้ตั้งค่าบูลีนเป็นเท็จ

isCurrentlyLoading = false;

ตั้งค่า AnimationListener ในภาพเคลื่อนไหวของคุณ:

animationRotate.setAnimationListener(new AnimationListener() {

จากนั้นทุกครั้งที่มีการดำเนินการภาพเคลื่อนไหวหนึ่งครั้งนั่นหมายความว่าเมื่อไอคอนของคุณทำการหมุนหนึ่งครั้งให้ตรวจสอบสถานะการโหลดและหากไม่โหลดอีกต่อไปภาพเคลื่อนไหวจะหยุดลง

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

วิธีนี้จะหยุดภาพเคลื่อนไหวได้ก็ต่อเมื่อหมุนไปแล้วจนจบและจะฉายซ้ำในไม่ช้าและจะไม่โหลดอีกต่อไป

อย่างน้อยก็เป็นสิ่งที่ฉันทำเมื่อต้องการนำความคิดของเจคไปใช้


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 จะไม่ถูกเรียก
pseudozach

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 (จาก 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

มันเป็นทางออกที่ง่ายมากของฉัน (ตัวอย่างเช่นต้องการ refactor) ทำงานร่วมกับ 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 () หากคุณต้องการรับหลังจากเริ่มกิจกรรมแล้วให้วางไว้ในภายหลัง:

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);
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.