com.waz.zclient.pages.main.conversation.SingleImageFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.waz.zclient.pages.main.conversation.SingleImageFragment.java

Source

/**
 * Wire
 * Copyright (C) 2016 Wire Swiss GmbH
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.waz.zclient.pages.main.conversation;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Toast;
import com.waz.api.ImageAsset;
import com.waz.api.LoadHandle;
import com.waz.zclient.OnBackPressedListener;
import com.waz.zclient.R;
import com.waz.zclient.controllers.drawing.IDrawingController;
import com.waz.zclient.controllers.permission.RequestPermissionsObserver;
import com.waz.zclient.pages.BaseFragment;
import com.waz.zclient.pages.main.conversation.views.image.ImageDragViewContainer;
import com.waz.zclient.ui.animation.interpolators.penner.Expo;
import com.waz.zclient.ui.animation.interpolators.penner.Quart;
import com.waz.zclient.ui.text.GlyphTextView;
import com.waz.zclient.ui.text.TypefaceTextView;
import com.waz.zclient.ui.utils.KeyboardUtils;
import com.waz.zclient.ui.utils.MathUtils;
import com.waz.zclient.utils.LayoutSpec;
import com.waz.zclient.utils.PermissionUtils;
import com.waz.zclient.utils.ViewUtils;
import com.waz.zclient.views.images.TouchImageView;
import timber.log.Timber;

public abstract class SingleImageFragment extends BaseFragment<SingleImageFragment>
        implements ImageAsset.SaveCallback, ImageDragViewContainer.Callback, OnBackPressedListener,
        RequestPermissionsObserver {

    private static final Interpolator ALPHA_INTERPOLATOR = new Quart.EaseOut();
    public static final float MIN_BACKGROUND_ALPHA = 0.64f;
    public static final float MIN_DRAG_DISTANCE_FADE_CONTROL = 0.1f;

    private static final String[] SAVE_IMAGE_PERMISSIONS = new String[] {
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE };
    private static final int SAVE_IMAGE_PERMISSION_REQUEST_ID = 6;

    private TypefaceTextView nameTextView;
    private TypefaceTextView timeTextView;
    private TouchImageView animatingImageView;
    private TouchImageView messageTouchImageView;
    private LoadHandle bitmapLoadHandle;
    private LoadHandle animatingSaveBitmapLoadHandle;
    private int clickedImageWidth;
    private int clickedImageHeight;
    private Point clickedImageLocation;
    private View background;
    private int openAnimationDuration;
    private int openAnimationBackgroundDuration;
    private int zoomOutAndRotateBackOnCloseDuration;
    private int closeAnimationBackgroundDelay;
    private int activityOrientation;
    private ImageView saveImageView;
    private ImageView animatingSaveImageView;
    private View saveImageViewContainer;
    private GlyphTextView saveButton;
    private GlyphTextView sketchButton;
    private View headerControls;
    private View footerControl;
    private ImageDragViewContainer dragViewContainer;
    private OnClickListener imageViewOnClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            boolean fadeIn = !MathUtils.floatEqual(headerControls.getAlpha(), 1f);
            fadeControls(fadeIn);
        }
    };
    private float flingImageTop;
    private float flingImageLeft;
    private float flingRotation;
    private float flingImagePivotX;
    private float flingImagePivotY;
    private boolean controlsVisibleOnStartDrag;
    private boolean isFading;
    private boolean isClosing;
    private final OnClickListener actionButtonsOnClickListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.gtv__single_image_message__close:
                backToConversation(false);
                break;
            case R.id.gtv__single_image_message__sketch:
                backToConversation(false);
                getControllerFactory().getDrawingController().showDrawing(getImage(),
                        IDrawingController.DrawingDestination.SINGLE_IMAGE_VIEW);
                break;
            case R.id.gtv__single_image_message__love:
                break;
            case R.id.gtv__single_image_message__save:
                if (PermissionUtils.hasSelfPermissions(getActivity(), SAVE_IMAGE_PERMISSIONS)) {
                    saveImageToDevice();
                } else {
                    ActivityCompat.requestPermissions(getActivity(), SAVE_IMAGE_PERMISSIONS,
                            SAVE_IMAGE_PERMISSION_REQUEST_ID);
                }
                break;
            default:
                //nothing
                break;
            }

        }
    };

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        getControllerFactory().getSingleImageController().updateViewReferences();
    }

    @Override
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    protected void onPostAttach(Activity activity) {
        super.onPostAttach(activity);
        activityOrientation = activity.getRequestedOrientation();
        activity.setRequestedOrientation(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
                ? ActivityInfo.SCREEN_ORIENTATION_FULL_USER
                : ActivityInfo.SCREEN_ORIENTATION_USER);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_single_image, container, false);

        dragViewContainer = ViewUtils.getView(view, R.id.dvc__single_image_message__drag_container);
        dragViewContainer.setCallback(this);

        messageTouchImageView = ViewUtils.getView(view, R.id.tiv__single_image_message__image);
        messageTouchImageView.setOnClickListener(imageViewOnClickListener);
        messageTouchImageView.setOnZoomLevelChangedListener(new TouchImageView.OnZoomLevelListener() {
            @Override
            public void onZoomLevelChanged(float scale) {
                if (messageTouchImageView == null || dragViewContainer == null) {
                    return;
                }
                boolean draggingEnabled = !messageTouchImageView.isZoomed();
                dragViewContainer.setDraggingEnabled(draggingEnabled);
            }
        });

        animatingImageView = ViewUtils.getView(view, R.id.tiv__single_image_message__animating_image);
        background = ViewUtils.getView(view, R.id.v__single_image_message__background);
        openAnimationDuration = getResources().getInteger(R.integer.single_image_message__open_animation__duration);
        openAnimationBackgroundDuration = getResources().getInteger(R.integer.framework_animation_duration_short);
        zoomOutAndRotateBackOnCloseDuration = getResources()
                .getInteger(R.integer.single_image_message__zoom_out_and_rotate_back_on_close_animation__duration);
        closeAnimationBackgroundDelay = getResources()
                .getInteger(R.integer.single_image_message__close_animation_background__delay);

        saveImageViewContainer = ViewUtils.getView(view, R.id.fl__single_image_message__save_image_container);
        saveImageView = ViewUtils.getView(view, R.id.iv__single_image_message__save_image);
        animatingSaveImageView = ViewUtils.getView(view, R.id.iv__single_image_message__save_image_blurred);
        nameTextView = ViewUtils.getView(view, R.id.ttv__single_image_message__name);
        timeTextView = ViewUtils.getView(view, R.id.ttv__single_image_message__time);

        headerControls = ViewUtils.getView(view, R.id.ll__single_image_message__header);
        footerControl = ViewUtils.getView(view, R.id.ll__single_image_message__footer);

        GlyphTextView closeGlyphText = ViewUtils.getView(view, R.id.gtv__single_image_message__close);
        closeGlyphText.setOnClickListener(actionButtonsOnClickListener);

        GlyphTextView loveGlyphText = ViewUtils.getView(view, R.id.gtv__single_image_message__love);
        loveGlyphText.setOnClickListener(actionButtonsOnClickListener);

        saveButton = ViewUtils.getView(view, R.id.gtv__single_image_message__save);
        saveButton.setOnClickListener(actionButtonsOnClickListener);

        sketchButton = ViewUtils.getView(view, R.id.gtv__single_image_message__sketch);
        sketchButton.setOnClickListener(actionButtonsOnClickListener);

        return view;
    }

    @Override
    public void onStart() {
        super.onStart();
        getControllerFactory().getRequestPermissionsController().addObserver(this);
        KeyboardUtils.closeKeyboardIfShown(getActivity());
        displayImage(getImage());
        animatingImageView.setScaleType(getScaleType());
        nameTextView.setTransformedText(getNameText());
        timeTextView.setTransformedText(getTimeText());
    }

    protected abstract ImageAsset getImage();

    protected abstract String getNameText();

    protected abstract String getTimeText();

    protected abstract ImageView.ScaleType getScaleType();

    @Override
    public void onStop() {
        getControllerFactory().getRequestPermissionsController().removeObserver(this);
        restoreRotation();
        if (bitmapLoadHandle != null) {
            bitmapLoadHandle.cancel();
            bitmapLoadHandle = null;
        }

        if (animatingSaveBitmapLoadHandle != null) {
            animatingSaveBitmapLoadHandle.cancel();
            animatingSaveBitmapLoadHandle = null;
        }

        super.onStop();
    }

    @SuppressWarnings("WrongConstant")
    private void restoreRotation() {
        Activity activity = getActivity();
        if (activity != null) {
            activity.setRequestedOrientation(activityOrientation);
        }
    }

    @Override
    public void onDestroyView() {
        messageTouchImageView.setImageDrawable(null);
        messageTouchImageView = null;
        animatingImageView.setImageDrawable(null);
        animatingImageView = null;
        dragViewContainer.setCallback(null);
        dragViewContainer = null;
        super.onDestroyView();
    }

    private void displayImage(ImageAsset imageAsset) {
        if (bitmapLoadHandle != null) {
            bitmapLoadHandle.cancel();
            bitmapLoadHandle = null;
        }
        if (imageAsset == null) {
            getFragmentManager().popBackStack();
            return;
        }
        if (imageAsset.getWidth() > 0) {
            fadeControls(true);
            bitmapLoadHandle = imageAsset.getBitmap(ViewUtils.getOrientationIndependentDisplayWidth(getActivity()),
                    new ImageAsset.BitmapCallback() {
                        @Override
                        public void onBitmapLoaded(Bitmap bitmap, boolean isPreview) {
                            if (getActivity() == null || messageTouchImageView == null) {
                                return;
                            }
                            if (!isPreview && messageTouchImageView.getDrawable() != null) {
                                // means we display the preview
                                showBitmap(bitmap);
                                return;
                            }
                            loadClickedImageSizeAndPosition();
                            positionViewAtAnimationStart();
                            showBitmap(bitmap);
                            animateOpeningTransition();
                        }

                        @Override
                        public void onBitmapLoadingFailed() {
                            // show error?
                            if (messageTouchImageView == null ||
                            // means we display nothing
                            messageTouchImageView.getDrawable() == null) {
                                getFragmentManager().popBackStack();
                            }
                        }
                    });
        } else {
            getFragmentManager().popBackStack();
        }
    }

    private void showBitmap(Bitmap bitmap) {
        messageTouchImageView.setImageDrawable(new BitmapDrawable(getResources(), bitmap));
        animatingImageView.setImageDrawable(new BitmapDrawable(getResources(), bitmap));
    }

    private void loadClickedImageSizeAndPosition() {
        final View clickedImage = getControllerFactory().getSingleImageController().getImageContainer();
        clickedImageHeight = clickedImage.getMeasuredHeight();
        clickedImageWidth = clickedImage.getMeasuredWidth();
        if (clickedImageHeight == 0 || clickedImageWidth == 0) {
            View parent = (View) clickedImage.getParent();
            final int widthSpec = View.MeasureSpec.makeMeasureSpec(
                    parent.getMeasuredWidth() - parent.getPaddingLeft() - parent.getPaddingRight(),
                    View.MeasureSpec.EXACTLY);
            final int heightSpec = View.MeasureSpec.makeMeasureSpec(
                    parent.getMeasuredHeight() - parent.getPaddingTop() - parent.getPaddingBottom(),
                    View.MeasureSpec.EXACTLY);
            clickedImage.measure(widthSpec, heightSpec);
            clickedImageHeight = clickedImage.getMeasuredHeight();
            clickedImageWidth = clickedImage.getMeasuredWidth();
        }
        clickedImageLocation = ViewUtils.getLocationOnScreen(clickedImage);
        int dx = 0;
        int dy = -ViewUtils.getStatusBarHeight(getActivity());
        clickedImageLocation.offset(dx, dy);
    }

    private void positionViewAtAnimationStart() {
        ViewGroup.LayoutParams layoutParams = animatingImageView.getLayoutParams();
        layoutParams.width = clickedImageWidth;
        layoutParams.height = clickedImageHeight;
        animatingImageView.setLayoutParams(layoutParams);
        animatingImageView.setX(clickedImageLocation.x);
        animatingImageView.setY(clickedImageLocation.y);
    }

    private void animateOpeningTransition() {
        final int displayHeight = ViewUtils.getOrientationDependentDisplayHeight(getActivity())
                - ViewUtils.getStatusBarHeight(getActivity());
        final int displayWidth = ViewUtils.getOrientationDependentDisplayWidth(getActivity());

        int fullImageHeight = displayHeight;
        int fullImageWidth = (int) (fullImageHeight * (float) clickedImageWidth / (float) clickedImageHeight);
        if (fullImageWidth > displayWidth) {
            fullImageWidth = displayWidth;
            fullImageHeight = (int) (fullImageWidth * (float) clickedImageHeight / (float) clickedImageWidth);
        }
        final float scale = Math.min(fullImageWidth / (float) clickedImageWidth,
                fullImageHeight / (float) clickedImageHeight);
        final int finalTop = (displayHeight - clickedImageHeight) / 2;
        final int finalLeft = (displayWidth - clickedImageWidth) / 2;

        animatingImageView.animate().y(finalTop).x(finalLeft).scaleX(scale).scaleY(scale)
                .setInterpolator(new Expo.EaseOut()).setDuration(openAnimationDuration)
                .withStartAction(new Runnable() {
                    @Override
                    public void run() {
                        ViewUtils.fadeOutView(
                                getControllerFactory().getSingleImageController().getLoadingIndicator(),
                                getResources().getInteger(R.integer.framework_animation_duration_short));
                        getControllerFactory().getSingleImageController().getImageContainer()
                                .setVisibility(View.INVISIBLE);
                    }
                }).withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        if (messageTouchImageView == null || animatingImageView == null) {
                            return;
                        }
                        messageTouchImageView.setVisibility(View.VISIBLE);
                        animatingImageView.setVisibility(View.GONE);
                    }
                }).start();

        background.animate().alpha(1f).setDuration(openAnimationBackgroundDuration)
                .setInterpolator(new Quart.EaseOut()).start();
    }

    private void backToConversation(boolean afterFling) {
        if (isClosing) {
            return;
        }
        isClosing = true;
        loadClickedImageSizeAndPosition();
        initAnimatingImageView(afterFling);

        restoreRotation();
        getControllerFactory().getSingleImageController().hideSingleImage();
        fadeControls(false);

        PointF currentFocusPoint = messageTouchImageView.getScrollPosition();
        if (currentFocusPoint == null) {
            getControllerFactory().getSingleImageController().clearReferences();
            getFragmentManager().popBackStack();
            return;
        }
        TouchImageView.FocusAndScale startFocusAndScale = new TouchImageView.FocusAndScale(currentFocusPoint.x,
                currentFocusPoint.y, messageTouchImageView.getCurrentZoom());
        TouchImageView.FocusAndScale finishFocusAndScale = new TouchImageView.FocusAndScale(0.5f, 0.5f, 1f);
        if ((MathUtils.floatEqual(currentFocusPoint.x, 0.5f) || MathUtils.floatEqual(currentFocusPoint.y, 0.5f))
                && MathUtils.floatEqual(messageTouchImageView.getCurrentZoom(), 1f)) {
            zoomOutAndRotateBackOnCloseDuration = 1;
        }
        ObjectAnimator
                .ofObject(messageTouchImageView, "focusAndScale", new TouchImageView.FocusAndScaleEvaluator(),
                        startFocusAndScale, finishFocusAndScale)
                .setDuration(zoomOutAndRotateBackOnCloseDuration).start();

        final boolean imageOffScreenInList = getControllerFactory().getSingleImageController()
                .isContainerOutOfScreen();
        ViewPropertyAnimator exitAnimation = animatingImageView.animate();
        if (imageOffScreenInList) {
            exitAnimation.alpha(0);
        } else {
            exitAnimation.x(clickedImageLocation.x).y(clickedImageLocation.y).rotation(0f).scaleX(1f).scaleY(1f);
        }
        exitAnimation.setDuration(openAnimationDuration).setStartDelay(zoomOutAndRotateBackOnCloseDuration)
                .setInterpolator(new Expo.EaseOut()).withStartAction(new Runnable() {
                    @Override
                    public void run() {
                        animatingImageView.setVisibility(View.VISIBLE);
                        messageTouchImageView.setVisibility(View.GONE);
                        if (imageOffScreenInList) {
                            getControllerFactory().getSingleImageController().getImageContainer()
                                    .setVisibility(View.VISIBLE);
                        } else {
                            getControllerFactory().getSingleImageController().getImageContainer()
                                    .setVisibility(View.INVISIBLE);
                        }
                        ViewUtils.fadeInView(
                                getControllerFactory().getSingleImageController().getLoadingIndicator(),
                                getResources().getInteger(R.integer.framework_animation_duration_short));
                    }
                }).withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        getControllerFactory().getSingleImageController().getImageContainer()
                                .setVisibility(View.VISIBLE);
                        getControllerFactory().getSingleImageController().clearReferences();
                        getFragmentManager().popBackStack();
                    }
                });

        exitAnimation.start();

        background.animate().alpha(0f)
                .setStartDelay(zoomOutAndRotateBackOnCloseDuration + closeAnimationBackgroundDelay)
                .setDuration(openAnimationBackgroundDuration).setInterpolator(new Quart.EaseOut()).start();
    }

    private void initAnimatingImageView(boolean afterFling) {
        if (getView() == null) {
            return;
        }
        ViewGroup parent = (ViewGroup) animatingImageView.getParent();
        parent.removeView(animatingImageView);

        final int displayHeight = ViewUtils.getOrientationDependentDisplayHeight(getActivity())
                - ViewUtils.getStatusBarHeight(getActivity());
        final int displayWidth = ViewUtils.getOrientationDependentDisplayWidth(getActivity());
        final int finalTop;
        final int finalLeft;
        if (afterFling) {
            finalLeft = (int) flingImageLeft;
            finalTop = (int) flingImageTop;
            animatingImageView.setPivotX(flingImagePivotX);
            animatingImageView.setPivotY(flingImagePivotY);
            animatingImageView.setRotation(flingRotation);
        } else {
            finalTop = (displayHeight - clickedImageHeight) / 2;
            finalLeft = (displayWidth - clickedImageWidth) / 2;
        }
        final float scale = Math.min(displayWidth / (float) clickedImageWidth,
                displayHeight / (float) clickedImageHeight);

        ViewGroup.LayoutParams layoutParams = new FrameLayout.LayoutParams(clickedImageWidth, clickedImageHeight);
        animatingImageView.setLayoutParams(layoutParams);
        animatingImageView.setX(finalLeft);
        animatingImageView.setY(finalTop);
        animatingImageView.setScaleX(scale);
        animatingImageView.setScaleY(scale);

        parent.addView(animatingImageView);
    }

    private void saveImageToDevice() {
        enableSaveImageButton(false);

        if (animatingSaveBitmapLoadHandle != null) {
            animatingSaveBitmapLoadHandle.cancel();
        }

        final ImageAsset image = getImage();
        final ImageAsset.BitmapCallback bitmapCallback = new ImageAsset.BitmapCallback() {
            @Override
            public void onBitmapLoaded(Bitmap bitmap, boolean b) {
                Timber.i("Got bitmap for save animation.");

                if (animatingSaveBitmapLoadHandle != null) {
                    animatingSaveBitmapLoadHandle.cancel();
                    animatingSaveBitmapLoadHandle = null;
                }

                animatingSaveImageView.setImageBitmap(bitmap);
                saveImageView.setImageDrawable(messageTouchImageView.getDrawable());

                saveImageViewContainer.setVisibility(View.VISIBLE);
                AnimatorSet animation = getSaveImageAnimation(image);
                animation.start();
            }

            @Override
            public void onBitmapLoadingFailed() {
                Timber.i("Failed loading bitmap for save animation.");
                if (animatingSaveBitmapLoadHandle != null) {
                    animatingSaveBitmapLoadHandle.cancel();
                    animatingSaveBitmapLoadHandle = null;
                }

                image.saveImageToGallery(SingleImageFragment.this);
            }
        };

        animatingSaveBitmapLoadHandle = image.getSingleBitmap(
                ViewUtils.getOrientationIndependentDisplayHeight(getActivity()) / 2, bitmapCallback);
    }

    private void unableToSaveImageNoPermissions() {
        Toast.makeText(getActivity(), R.string.single_image__save_to_disk__error, Toast.LENGTH_SHORT).show();
    }

    private void enableSaveImageButton(boolean enabled) {
        saveButton.setEnabled(enabled);
        saveButton.setClickable(enabled);
    }

    private AnimatorSet getSaveImageAnimation(final ImageAsset image) {
        int totalAnimationDuration = getResources().getInteger(R.integer.framework_animation_duration_long);

        ObjectAnimator fadeOutAnimator = ObjectAnimator.ofFloat(saveImageViewContainer, View.ALPHA, 1, 0);
        fadeOutAnimator.setDuration(totalAnimationDuration);

        ObjectAnimator scaleDownXAnimator = ObjectAnimator.ofFloat(saveImageViewContainer, View.SCALE_X, 0f);
        ObjectAnimator scaleDownYAnimator = ObjectAnimator.ofFloat(saveImageViewContainer, View.SCALE_Y, 0f);
        scaleDownXAnimator.setDuration(totalAnimationDuration);
        scaleDownYAnimator.setDuration(totalAnimationDuration);

        int moveY = ViewUtils.getOrientationIndependentDisplayHeight(getActivity()) / 2;
        int moveX;
        if (ViewUtils.isInLandscape(getActivity())) {
            moveX = ViewUtils.getOrientationIndependentDisplayWidth(getActivity());
        } else {
            moveX = ViewUtils.getOrientationIndependentDisplayWidth(getActivity()) / 2;
        }
        ObjectAnimator moveXAnimator = ObjectAnimator.ofFloat(saveImageViewContainer, View.TRANSLATION_X, -moveX);
        ObjectAnimator moveYAnimator = ObjectAnimator.ofFloat(saveImageViewContainer, View.TRANSLATION_Y, moveY);
        moveXAnimator.setDuration(totalAnimationDuration);
        moveYAnimator.setDuration(totalAnimationDuration);

        // Fade out top image view for blur effect
        ObjectAnimator fadeToBlurredAnimator = ObjectAnimator.ofFloat(saveImageView, View.ALPHA, 1, 0);
        fadeToBlurredAnimator.setDuration(getResources().getInteger(R.integer.framework_animation_duration_medium));

        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setInterpolator(new Expo.EaseIn());
        animatorSet.playTogether(fadeOutAnimator, scaleDownXAnimator, scaleDownYAnimator, moveXAnimator,
                moveYAnimator, fadeToBlurredAnimator);

        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (getStoreFactory() == null || getStoreFactory().isTornDown()) {
                    return;
                }

                resetSaveImageView();

                // Image saving is called here, as an image save triggers a notification which contains a bitmap;
                // currently that bitmap is quite large, android.app.Notification writes the bitmap to a parcel
                // on the main thread, dropping a large number of frames and breaking the save animation
                image.saveImageToGallery(SingleImageFragment.this);

                enableSaveImageButton(true);
            }
        });

        return animatorSet;
    }

    private void resetSaveImageView() {
        saveImageViewContainer.setScaleX(1f);
        saveImageViewContainer.setScaleY(1f);
        saveImageViewContainer.setTranslationX(0f);
        saveImageViewContainer.setTranslationY(0f);
        saveImageView.setAlpha(1f);

        animatingSaveImageView.setImageBitmap(null);
        saveImageView.setImageDrawable(null);
    }

    private void fadeControls(boolean fadeIn) {
        fadeView(headerControls, fadeIn);
        if (getImage() != null) {
            fadeView(footerControl, fadeIn);
        } else {
            footerControl.setVisibility(View.GONE);
        }
    }

    private void fadeView(final View view, final boolean fadeIn) {
        isFading = true;
        float toAlpha = fadeIn ? 1f : 0f;

        view.setClickable(fadeIn);
        view.animate().alpha(toAlpha).setInterpolator(new Quart.EaseOut())
                .setDuration(getResources().getInteger(R.integer.single_image_message__overlay__fade_duration))
                .withStartAction(new Runnable() {
                    @Override
                    public void run() {
                        if (fadeIn) {
                            view.setVisibility(View.VISIBLE);
                        }
                    }
                }).withEndAction(new Runnable() {
                    @Override
                    public void run() {
                        if (!fadeIn) {
                            view.setVisibility(View.GONE);
                        }
                        isFading = false;
                    }
                }).start();
    }

    @Override
    public boolean onBackPressed() {
        backToConversation(false);
        return true;
    }

    @Override
    public void imageSaved(Uri uri) {
        if (getContainer() == null || getActivity() == null) {
            return;
        }

        getControllerFactory().getNotificationsController().showImageSavedNotification(getImage(), uri);
    }

    @Override
    public void imageSavingFailed(Exception ex) {

    }

    @Override
    public void onFlingRequested(float left, float top, float rotation, float pivotX, float pivotY) {
        this.flingImageLeft = left;
        this.flingImageTop = top;
        this.flingImagePivotX = pivotX;
        this.flingImagePivotY = pivotY;
        this.flingRotation = rotation;
        backToConversation(true);
    }

    @Override
    public void onDragDistance(float distance) {
        if (ViewUtils.isInPortrait(getActivity()) || LayoutSpec.isTablet(getActivity())) {
            background.setAlpha(1f - (1f - MIN_BACKGROUND_ALPHA) * ALPHA_INTERPOLATOR.getInterpolation(distance));
        }
        if (distance >= MIN_DRAG_DISTANCE_FADE_CONTROL && footerControl.getVisibility() != View.GONE && !isFading) {
            fadeControls(false);
        }
    }

    @Override
    public void onStartDrag() {
        this.controlsVisibleOnStartDrag = MathUtils.floatEqual(footerControl.getAlpha(), 1f);
    }

    @Override
    public void onEndDrag() {
        if (controlsVisibleOnStartDrag) {
            fadeControls(true);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, int[] grantResults) {
        switch (requestCode) {
        case SAVE_IMAGE_PERMISSION_REQUEST_ID:
            if (PermissionUtils.verifyPermissions(grantResults)) {
                saveImageToDevice();
            } else {
                unableToSaveImageNoPermissions();
            }
            break;
        }
    }

    public interface Container {
    }
}