Why ListView expand/collapse animation appears muc

2020-03-06 03:41发布

问题:

Recently, I find out the ListView expand/collapse animation appears much slower in DialogFragment than in Activity.

In DialogFragment, the animation FPS is 10 fps. Each animation takes 100ms.

In Activity, the animation FPS is 50 fps. Each animation takes 20ms.

I took this wonderful open source library as benchmark example : https://github.com/nhaarman/ListViewAnimations

I made modification on https://github.com/nhaarman/ListViewAnimations/blob/master/example/src/com/haarman/listviewanimations/itemmanipulationexamples/ItemManipulationsExamplesActivity.java#L51, to test between performance in DialogFragment and Activity.

    public void onExpandListItemAdapterClicked(View view) {
        // For Activity testing.
        Intent intent = new Intent(this, ExpandableListItemActivity.class);
        startActivity(intent);

        // For DialogFragment testing.
        //FragmentManager fm = this.getSupportFragmentManager();
        //DemoDialogFragment demoDialogFragment = new DemoDialogFragment();
        //demoDialogFragment.show(fm, "DemoDialogFragment");        
    }

I made a minor modification on https://github.com/nhaarman/ListViewAnimations/blob/master/example/src/com/haarman/listviewanimations/itemmanipulationexamples/ExpandableListItemActivity.java

public class ExpandableListItemActivity extends MyListActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        MyExpandableListItemAdapter myExpandableListItemAdapter = new MyExpandableListItemAdapter(this, getItems());
        //AlphaInAnimationAdapter alphaInAnimationAdapter = new AlphaInAnimationAdapter(myExpandableListItemAdapter);
        //alphaInAnimationAdapter.setAbsListView(getListView());
        getListView().setAdapter(myExpandableListItemAdapter);

        Toast.makeText(this, R.string.explainexpand, Toast.LENGTH_LONG).show();
    }

I created my own DialogFragment class for testing. Based entirely from ExpandableListItemActivity

package com.haarman.listviewanimations.itemmanipulationexamples;

import java.util.ArrayList;
import java.util.List;

import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.util.LruCache;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.haarman.listviewanimations.ArrayAdapter;
import com.haarman.listviewanimations.R;
import com.haarman.listviewanimations.itemmanipulation.ExpandableListItemAdapter;

public class DemoDialogFragment extends DialogFragment {

    private ListView mListView;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mListView = new ListView(getActivity());
        mListView.setDivider(null);

        MyExpandableListItemAdapter myExpandableListItemAdapter = new MyExpandableListItemAdapter(getActivity(), getItems());
        //AlphaInAnimationAdapter alphaInAnimationAdapter = new AlphaInAnimationAdapter(myExpandableListItemAdapter);
        //alphaInAnimationAdapter.setAbsListView(getListView());
        getListView().setAdapter(myExpandableListItemAdapter);

        Toast.makeText(getActivity(), R.string.explainexpand, Toast.LENGTH_LONG).show();

        final AlertDialog dialog = new AlertDialog.Builder(this.getActivity())            
        .setView(mListView)
        .create();

        return dialog;
    }

    public ListView getListView() {
        return mListView;
    }

    protected ArrayAdapter<Integer> createListAdapter() {
        return new MyListAdapter(getActivity(), getItems());
    }

    public static ArrayList<Integer> getItems() {
        ArrayList<Integer> items = new ArrayList<Integer>();
        for (int i = 0; i < 1000; i++) {
            items.add(i);
        }
        return items;
    }

    private static class MyListAdapter extends ArrayAdapter<Integer> {

        private Context mContext;

        public MyListAdapter(Context context, ArrayList<Integer> items) {
            super(items);
            mContext = context;
        }

        @Override
        public long getItemId(int position) {
            return getItem(position).hashCode();
        }

        @Override
        public boolean hasStableIds() {
            return true;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            TextView tv = (TextView) convertView;
            if (tv == null) {
                tv = (TextView) LayoutInflater.from(mContext).inflate(R.layout.list_row, parent, false);
            }
            tv.setText("This is row number " + getItem(position));
            return tv;
        }
    }

    private static class MyExpandableListItemAdapter extends ExpandableListItemAdapter<Integer> {

        private Context mContext;
        private LruCache<Integer, Bitmap> mMemoryCache;

        /**
         * Creates a new ExpandableListItemAdapter with the specified list, or an empty list if
         * items == null.
         */
        private MyExpandableListItemAdapter(Context context, List<Integer> items) {
            super(context, R.layout.activity_expandablelistitem_card, R.id.activity_expandablelistitem_card_title, R.id.activity_expandablelistitem_card_content, items);
            mContext = context;

            final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

            // Use 1/8th of the available memory for this memory cache.
            final int cacheSize = maxMemory;
            mMemoryCache = new LruCache<Integer, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(Integer key, Bitmap bitmap) {
                    // The cache size will be measured in kilobytes rather than
                    // number of items.
                    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                }
            };
        }

        @Override
        public View getTitleView(int position, View convertView, ViewGroup parent) {
            TextView tv = (TextView) convertView;
            if (tv == null) {
                tv = new TextView(mContext);
            }
            tv.setText(mContext.getString(R.string.expandorcollapsecard, getItem(position)));
            return tv;
        }

        @Override
        public View getContentView(int position, View convertView, ViewGroup parent) {
            ImageView imageView = (ImageView) convertView;
            if (imageView == null) {
                imageView = new ImageView(mContext);
                imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
            }

            int imageResId;
            switch (getItem(position) % 5) {
            case 0:
                imageResId = R.drawable.img_nature1;
                break;
            case 1:
                imageResId = R.drawable.img_nature2;
                break;
            case 2:
                imageResId = R.drawable.img_nature3;
                break;
            case 3:
                imageResId = R.drawable.img_nature4;
                break;
            default:
                imageResId = R.drawable.img_nature5;
            }

            Bitmap bitmap = getBitmapFromMemCache(imageResId);
            if (bitmap == null) {
                bitmap = BitmapFactory.decodeResource(mContext.getResources(), imageResId);
                addBitmapToMemoryCache(imageResId, bitmap);
            }
            imageView.setImageBitmap(bitmap);

            return imageView;
        }

        private void addBitmapToMemoryCache(int key, Bitmap bitmap) {
            if (getBitmapFromMemCache(key) == null) {
                mMemoryCache.put(key, bitmap);
            }
        }

        private Bitmap getBitmapFromMemCache(int key) {
            return mMemoryCache.get(key);
        }
    }

}

Then, I place my benchmark code in this file : https://github.com/nhaarman/ListViewAnimations/blob/master/library/src/com/haarman/listviewanimations/itemmanipulation/ExpandableListItemAdapter.java#L277

    private ValueAnimator createHeightAnimator(int start, int end) {
        Log.i("CHEOK", start + " -> " + end);
        ValueAnimator animator = ValueAnimator.ofInt(start, end);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                int value = (Integer) valueAnimator.getAnimatedValue();

                ViewGroup.LayoutParams layoutParams = mContentParent.getLayoutParams();
                layoutParams.height = value;
                Log.i("CHEOK", "height = " + value);
                mContentParent.setLayoutParams(layoutParams);
            }
        });
        return animator;
    }

Here is the result I'm getting

// Using DialogFragment
10-12 03:36:05.695: I/CHEOK(29407): height = 0
10-12 03:36:06.258: I/CHEOK(29407): height = 0
10-12 03:36:06.347: I/CHEOK(29407): height = 74
10-12 03:36:06.547: I/CHEOK(29407): height = 358
10-12 03:36:06.621: I/CHEOK(29407): height = 360



// Using Activity
10-12 03:37:02.375: I/CHEOK(29956): height = 0
10-12 03:37:02.387: I/CHEOK(29956): height = 0
10-12 03:37:02.406: I/CHEOK(29956): height = 3
10-12 03:37:02.461: I/CHEOK(29956): height = 51
10-12 03:37:02.476: I/CHEOK(29956): height = 72
10-12 03:37:02.492: I/CHEOK(29956): height = 98
10-12 03:37:02.512: I/CHEOK(29956): height = 131
10-12 03:37:02.531: I/CHEOK(29956): height = 164
10-12 03:37:02.547: I/CHEOK(29956): height = 196
10-12 03:37:02.562: I/CHEOK(29956): height = 228
10-12 03:37:02.582: I/CHEOK(29956): height = 260
10-12 03:37:02.601: I/CHEOK(29956): height = 290
10-12 03:37:02.617: I/CHEOK(29956): height = 312
10-12 03:37:02.633: I/CHEOK(29956): height = 331
10-12 03:37:02.652: I/CHEOK(29956): height = 348
10-12 03:37:02.672: I/CHEOK(29956): height = 357
10-12 03:37:02.687: I/CHEOK(29956): height = 360

Is there any idea, why this happens? How we can improve the performance in DialogFragment?

I place the entire project files, just in case you're interested to test out.

https://www.dropbox.com/s/5q6ttn11o92g39n/ListViewAnimations-master.zip

Update

By tracing using TraceView, I notice that when using DialogFragment to expand ListView item, the ArrayAdapter will be called frequent. I'm not exactly sure why it is so. I do another independent implementation without using the open source library. I'm still facing the same problem.

Perform animation on DialogFragment, the ArrayAdapter's getView will be triggered frequently while animation is in progress.

For Activity, when animation is in progress, ArrayAdapter's getView will NOT be triggered.

TraceView while using DialogFragment

TraceView while using Activity

回答1:

I find out the key solution to this problem is, fix the dialog size always. Once we fix the size, no resource will be spent to perform size computation during animation.

    final ViewTreeObserver vto = view.getViewTreeObserver();
    vto.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {

        @SuppressLint("NewApi")
        @SuppressWarnings("deprecation")
        @Override
        public void onGlobalLayout() {
            // Key area to perform optimization. Fix the dialog size!
            // During ListView's rows animation, dialog will never need to 
            // perform computation again.
            int width = dialog.getWindow().getDecorView().getWidth();
            int height = dialog.getWindow().getDecorView().getHeight();
            dialog.getWindow().setLayout(width, height);

            ViewTreeObserver obs = view.getViewTreeObserver();
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
                obs.removeOnGlobalLayoutListener(this);
            } else {
                obs.removeGlobalOnLayoutListener(this);
            }
        }
    });