OutOfMemoryError in SharedTransitionElement Animation

189 views Asked by At

I'm using sharedElementTransition in my app and i've used a custom class for sharedElementCallback to update the views at run time. It was causing OutOfMemory errors at sometimes, so i searched about it and from this solution i used code of LeakFreeSupportSharedElementCallback to avoid crashes but i still get following crash logs very often.

Fatal Exception: java.lang.OutOfMemoryError: Failed to allocate a 8357052 byte allocation with 953048 free bytes and 930KB until OOM
       at dalvik.system.VMRuntime.newNonMovableArray(VMRuntime.java)
       at android.graphics.Bitmap.nativeCopy(Bitmap.java)
       at android.graphics.Bitmap.copy(Bitmap.java:684)
       at com.fayvo.ui.main.home.post.PostDetailSharedElementCallback.onCreateSnapshotView(PostDetailSharedElementCallback.java:279)
       at android.support.v4.app.ActivityCompat$SharedElementCallback21Impl.onCreateSnapshotView(ActivityCompat.java:609)
       at android.app.ActivityTransitionCoordinator.createSnapshots(ActivityTransitionCoordinator.java:666)
       at android.app.EnterTransitionCoordinator.startSharedElementTransition(EnterTransitionCoordinator.java:400)
       at android.app.EnterTransitionCoordinator.-wrap4(EnterTransitionCoordinator.java)
       at android.app.EnterTransitionCoordinator$5$1$1.run(EnterTransitionCoordinator.java:475)
       at android.app.ActivityTransitionCoordinator.startTransition(ActivityTransitionCoordinator.java:836)
       at android.app.EnterTransitionCoordinator$5$1.onPreDraw(EnterTransitionCoordinator.java:472)
       at android.view.ViewTreeObserver.dispatchOnPreDraw(ViewTreeObserver.java:1013)
       at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2513)
       at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1522)
       at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7098)
       at android.view.Choreographer$CallbackRecord.run(Choreographer.java:927)
       at android.view.Choreographer.doCallbacks(Choreographer.java:702)
       at android.view.Choreographer.doFrame(Choreographer.java:638)
       at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:913)
       at android.os.Handler.handleCallback(Handler.java:751)
       at android.os.Handler.dispatchMessage(Handler.java:95)
       at android.os.Looper.loop(Looper.java:154)
       at android.app.ActivityThread.main(ActivityThread.java:6682)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1520)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1410)

CustomCallback class:

public class CustomSharedElementCallback extends SharedElementCallback {


    static final String BUNDLE_SNAPSHOT_BITMAP = "BUNDLE_SNAPSHOT_BITMAP";
    static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "BUNDLE_SNAPSHOT_IMAGE_SCALETYPE";
    static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "BUNDLE_SNAPSHOT_IMAGE_MATRIX";

    static final String BUNDLE_SNAPSHOT_TYPE = "BUNDLE_SNAPSHOT_TYPE";
    static final String BUNDLE_SNAPSHOT_TYPE_IMAGE_VIEW = "BUNDLE_SNAPSHOT_TYPE";
    private static int MAX_IMAGE_SIZE = (1024 * 1024);
    private List<View> mSharedElements;
    private int currentPosition = 0;
    private boolean enableChangeImageTransform;
    private SparseArray<Matrix> tempMatrixes;

    public PostDetailSharedElementCallback() {
        mSharedElements = new ArrayList<>();
        tempMatrixes = new SparseArray<>();
    }


    @Override
    public void onSharedElementStart(List<String> sharedElementNames,
                                     List<View> sharedElements,
                                     List<View> sharedElementSnapshots) {

        /*AppLogger.d("usm_shared_callback_0.1", "onSharedElementStart: " + sharedElementNames.get(0)
                + " ,enableChangeImageTransform= " + enableChangeImageTransform
        );*/
        boolean allowTransform = enableChangeImageTransform && sharedElements != null && sharedElements.size() > 0;

        if (allowTransform && sharedElements.get(0) instanceof CustomPhotoView) {
            ((CustomPhotoView) sharedElements.get(0)).setScaleType(ImageView.ScaleType.CENTER_CROP);
        } else if (allowTransform && sharedElements.get(0) instanceof ImageView) {
            ((ImageView) sharedElements.get(0)).setScaleType(ImageView.ScaleType.FIT_CENTER);
        }
    }

    @Override
    public void onSharedElementEnd(List<String> sharedElementNames, List<View> sharedElements, List<View> sharedElementSnapshots) {
       /*AppLogger.d("usm_shared_callback_0.2", "onSharedElementEnd: " + sharedElementNames.get(0)
                + " ,enableChangeImageTransform= " + enableChangeImageTransform
        );*/
        boolean allowTransform = enableChangeImageTransform && sharedElements != null && sharedElements.size() > 0;

        if (allowTransform && sharedElements.get(0) instanceof CustomPhotoView) {
            ((CustomPhotoView) sharedElements.get(0)).setScaleType(ImageView.ScaleType.FIT_CENTER);

            // updateCloudChipBoxTagsView(sharedElementNames, sharedElements);
            updateBoxIconView(sharedElementNames, sharedElements);
            updateUserNameView(sharedElementNames, sharedElements);
        } else if (allowTransform && sharedElements.get(0) instanceof ImageView) {
            ((ImageView) sharedElements.get(0)).setScaleType(ImageView.ScaleType.CENTER_CROP);
        }

    }

    private void updateCloudChipBoxTagsView(List<String> sharedElementNames, List<View> sharedElements) {
        int i = 0;
        for (String sharedName : sharedElementNames) {

            if (sharedName.contains(PostTags.TRANSITION_NAME_TAGS)) {
                if (sharedElements.get(i) instanceof ChipCloud) {
                    ChipCloud chipCloud = ((ChipCloud) sharedElements.get(i));

                    int textColor = ContextCompat.getColor(chipCloud.getContext(), R.color.fayvo_color);
                    // adding modifyChips method to make animation transition experience a bit better
                    chipCloud.modifyChips(chipCloud.getContext().getResources().getDimension(R.dimen.hint_text_size), textColor);
                    break;
                }
            }
            i++;
        }
    }

    private void updateBoxIconView(List<String> sharedElementNames, List<View> sharedElements) {
        int i = 0;
        for (String sharedName : sharedElementNames) {

            if (sharedName.contains(PostTags.TRANSITION_NAME_BOX_ICON)) {
                if (sharedElements.get(i) instanceof ImageView) {
                    ImageView ivBoxIcon = (ImageView) sharedElements.get(i);
                    int color = ContextCompat.getColor(ivBoxIcon.getContext(), R.color.fayvo_color);
                    ivBoxIcon.setColorFilter(color);
                    break;
                }
            }
            i++;
        }
    }

    private void updateUserNameView(List<String> sharedElementNames, List<View> sharedElements) {
        int i = 0;
        for (String sharedName : sharedElementNames) {

            if (sharedName.contains(PostTags.TRANSITION_NAME_USERNAME)) {
                if (sharedElements.get(i) instanceof TextView) {
                    TextView textView = (TextView) sharedElements.get(i);
                    int color = ContextCompat.getColor(textView.getContext(), R.color.black);
                    float textSizePx = textView.getContext().getResources().getDimension(R.dimen.et_text_size);
                    textView.setTextColor(color);
                    textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSizePx);
                    break;
                }
            }
            i++;
        }
    }

    @Override
    public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
        removeObsoleteElements(names, sharedElements, mapObsoleteElements(names));

        // update transition names
        setTransitionNames();

        for (int i = 0; i < mSharedElements.size(); i++)
            mapSharedElement(names, sharedElements, mSharedElements.get(i));
    }

    public int getCurrentPosition() {
        return currentPosition;
    }

    public void setCurrentPosition(int position) {
        // AppLogger.d("usm_shared_callback_1", "currentPosition= " + position);
        this.currentPosition = position;
    }

    public void setEnableChangeImageTransform(boolean enableTransform) {
        this.enableChangeImageTransform = enableTransform;
    }

    /**
     * This method is used to set Transition name of shared views.
     * Note: Keep the sequence exactly same in which the views are
     * passed for transition.
     */
    public void setTransitionNames() {
        //  AppLogger.d("usm_shared_callback_2", "Setting transition names: " + currentPosition);
        if (mSharedElements.size() > 0)
            ViewCompat.setTransitionName(mSharedElements.get(0), PostTags.TRANSITION_NAME_POST_BODY + currentPosition);
        if (mSharedElements.size() > 1)
            ViewCompat.setTransitionName(mSharedElements.get(1), PostTags.TRANSITION_NAME_USER_PIC + currentPosition);
        if (mSharedElements.size() > 2)
            ViewCompat.setTransitionName(mSharedElements.get(2), PostTags.TRANSITION_NAME_USERNAME + currentPosition);
        if (mSharedElements.size() > 3)
            ViewCompat.setTransitionName(mSharedElements.get(3), PostTags.TRANSITION_NAME_TAGS + currentPosition);
        if (mSharedElements.size() > 4)
            ViewCompat.setTransitionName(mSharedElements.get(4), PostTags.TRANSITION_NAME_BOX_ICON + currentPosition);

    }

    public List<View> getSharedViews() {
        // AppLogger.d("usm_shared_callback_4", "setSharedViews is called");
        if (mSharedElements != null)
            return mSharedElements;
        return null;
    }

    public void setSharedViews(@NonNull View... sharedViews) {
        // AppLogger.d("usm_shared_callback_4", "setSharedViews is called");
        clearSharedViews();
        for (View view : sharedViews) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                mSharedElements.add(view);
            }
        }
    }

    public void clearSharedViews() {
        mSharedElements.clear();
        tempMatrixes.clear();
    }

    /**
     * Maps all views that don't start with "android" namespace.
     *
     * @param names All shared element names.
     * @return The obsolete shared element names.
     */
    @NonNull
    private List<String> mapObsoleteElements(List<String> names) {
        List<String> elementsToRemove = new ArrayList<>(names.size());
        for (String name : names) {
            if (name.startsWith("android")) continue;
            elementsToRemove.add(name);
        }
        return elementsToRemove;
    }

    /**
     * Removes obsolete elements from names and shared elements.
     *
     * @param names            Shared element names.
     * @param sharedElements   Shared elements.
     * @param elementsToRemove The elements that should be removed.
     */
    private void removeObsoleteElements(List<String> names,
                                        Map<String, View> sharedElements,
                                        List<String> elementsToRemove) {
        if (elementsToRemove.size() > 0) {
            names.removeAll(elementsToRemove);
            for (String elementToRemove : elementsToRemove) {
                sharedElements.remove(elementToRemove);
            }
        }
    }

    /**
     * Puts a shared element to transitions and names.
     *
     * @param names          The names for this transition.
     * @param sharedElements The elements for this transition.
     * @param view           The view to add.
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void mapSharedElement(List<String> names, Map<String, View> sharedElements, View view) {
        String transitionName = view.getTransitionName();
        names.add(transitionName);
        sharedElements.put(transitionName, view);
    }

    /**
     * This method is overridden to avoid memory leaks
     * @param context
     * @param snapshot
     * @return
     */
    @Override
    public View onCreateSnapshotView(Context context, Parcelable snapshot) {

        // AppLogger.d("usm_shared_element_call_back", "onCreateSnapshotView: mSharedElements= " + mSharedElements.size());

        View view = null;
        if (snapshot instanceof Bundle) {
            Bundle bundle = (Bundle) snapshot;
            Bitmap bitmap = bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP);

            if (bitmap == null) {
                bundle.clear();
                return null;
            }

            // Curiously, this is required to have the bitmap be GCed almost immediately after transition ends
            // otherwise, garbage-collectable mem will still build up quickly
            bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);

            if (bitmap == null) {
                return null;
            }

            if (BUNDLE_SNAPSHOT_TYPE_IMAGE_VIEW.equals(((Bundle) snapshot).getString(BUNDLE_SNAPSHOT_TYPE))) {
                ImageView imageView = new ImageView(context);
                view = imageView;
                imageView.setImageBitmap(bitmap);
                imageView.setScaleType(
                        ImageView.ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE)));
                if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) {
                    float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX);
                    Matrix matrix = new Matrix();
                    matrix.setValues(values);
                    imageView.setImageMatrix(matrix);
                }
            } else {
                view = new View(context);
                Resources resources = context.getResources();
                view.setBackground(new BitmapDrawable(resources, bitmap));
            }
            bundle.clear();
        }

        return view;
        //  return super.onCreateSnapshotView(context, snapshot);
    }


    /**
     * This method is overridden to avoid memory leaks
     * @param sharedElement
     * @param viewToGlobalMatrix
     * @param screenBounds
     * @return
     */
    @Override
    public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, RectF screenBounds) {
        //AppLogger.d("usm_shared_element_call_back", "onCaptureSharedElementSnapshot: mSharedElements= " + mSharedElements.size()
        //        + " ,sharedElement name= " + sharedElement.getTransitionName() + " ,id= " + sharedElement.getId());


        if (sharedElement instanceof ImageView) {
            ImageView imageView = ((ImageView) sharedElement);
            Drawable d = imageView.getDrawable();
            Drawable bg = imageView.getBackground();
            if (d != null && (bg == null || bg.getAlpha() == 0)) {
                Bitmap bitmap = TransitionUtils.createDrawableBitmap(d);
                if (bitmap != null) {
                    Bundle bundle = new Bundle();
                    bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap);
                    bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE,
                            imageView.getScaleType().toString());
                    if (imageView.getScaleType() == ImageView.ScaleType.MATRIX) {
                        Matrix matrix = imageView.getImageMatrix();
                        float[] values = new float[9];
                        matrix.getValues(values);
                        bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values);
                    }

                    bundle.putString(BUNDLE_SNAPSHOT_TYPE, BUNDLE_SNAPSHOT_TYPE_IMAGE_VIEW);

                    return bundle;
                }
            }
        }
        if (tempMatrixes.get(sharedElement.getId(), null) == null) {
            tempMatrixes.put(sharedElement.getId(), new Matrix(viewToGlobalMatrix));
        } else {
            tempMatrixes.get(sharedElement.getId()).set(viewToGlobalMatrix);
        }

        Bundle bundle = new Bundle();
        Bitmap bitmap = TransitionUtils.createViewBitmap(sharedElement, tempMatrixes.get(sharedElement.getId()), screenBounds);
        bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap);

        return bundle;
        // return super.onCaptureSharedElementSnapshot(sharedElement, viewToGlobalMatrix, screenBounds);
    }
}
1

There are 1 answers

0
Muhammad Saad Rafique On

In your manifest file under application tag add the following line

<application
    android:largeHeap="true"
>

That is not a very good approach but this will resolve your issue or you have to find a way to manage your memory allocation. But these days devices are coming with large memories. So this will not affect noticeable performance. You are good to go with this.

For further information regarding memory allocation do visit this link: performance and memory