1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.transition;
18
19import android.animation.Animator;
20import android.animation.ObjectAnimator;
21import android.animation.TypeEvaluator;
22import android.content.Context;
23import android.graphics.Matrix;
24import android.graphics.Rect;
25import android.graphics.drawable.Drawable;
26import android.support.annotation.NonNull;
27import android.util.AttributeSet;
28import android.util.Property;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.ImageView;
32
33import java.util.Map;
34
35/**
36 * This Transition captures an ImageView's matrix before and after the
37 * scene change and animates it during the transition.
38 *
39 * <p>In combination with ChangeBounds, ChangeImageTransform allows ImageViews
40 * that change size, shape, or {@link android.widget.ImageView.ScaleType} to animate contents
41 * smoothly.</p>
42 */
43public class ChangeImageTransform extends Transition {
44
45    private static final String PROPNAME_MATRIX = "android:changeImageTransform:matrix";
46    private static final String PROPNAME_BOUNDS = "android:changeImageTransform:bounds";
47
48    private static final String[] sTransitionProperties = {
49            PROPNAME_MATRIX,
50            PROPNAME_BOUNDS,
51    };
52
53    private static final TypeEvaluator<Matrix> NULL_MATRIX_EVALUATOR = new TypeEvaluator<Matrix>() {
54        @Override
55        public Matrix evaluate(float fraction, Matrix startValue, Matrix endValue) {
56            return null;
57        }
58    };
59
60    private static final Property<ImageView, Matrix> ANIMATED_TRANSFORM_PROPERTY =
61            new Property<ImageView, Matrix>(Matrix.class, "animatedTransform") {
62                @Override
63                public void set(ImageView view, Matrix matrix) {
64                    ImageViewUtils.animateTransform(view, matrix);
65                }
66
67                @Override
68                public Matrix get(ImageView object) {
69                    return null;
70                }
71            };
72
73    public ChangeImageTransform() {
74    }
75
76    public ChangeImageTransform(Context context, AttributeSet attrs) {
77        super(context, attrs);
78    }
79
80    private void captureValues(TransitionValues transitionValues) {
81        View view = transitionValues.view;
82        if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) {
83            return;
84        }
85        ImageView imageView = (ImageView) view;
86        Drawable drawable = imageView.getDrawable();
87        if (drawable == null) {
88            return;
89        }
90        Map<String, Object> values = transitionValues.values;
91
92        int left = view.getLeft();
93        int top = view.getTop();
94        int right = view.getRight();
95        int bottom = view.getBottom();
96
97        Rect bounds = new Rect(left, top, right, bottom);
98        values.put(PROPNAME_BOUNDS, bounds);
99        values.put(PROPNAME_MATRIX, copyImageMatrix(imageView));
100    }
101
102    @Override
103    public void captureStartValues(@NonNull TransitionValues transitionValues) {
104        captureValues(transitionValues);
105    }
106
107    @Override
108    public void captureEndValues(@NonNull TransitionValues transitionValues) {
109        captureValues(transitionValues);
110    }
111
112    @Override
113    public String[] getTransitionProperties() {
114        return sTransitionProperties;
115    }
116
117    /**
118     * Creates an Animator for ImageViews moving, changing dimensions, and/or changing
119     * {@link android.widget.ImageView.ScaleType}.
120     *
121     * @param sceneRoot   The root of the transition hierarchy.
122     * @param startValues The values for a specific target in the start scene.
123     * @param endValues   The values for the target in the end scene.
124     * @return An Animator to move an ImageView or null if the View is not an ImageView,
125     * the Drawable changed, the View is not VISIBLE, or there was no change.
126     */
127    @Override
128    public Animator createAnimator(@NonNull ViewGroup sceneRoot, TransitionValues startValues,
129            final TransitionValues endValues) {
130        if (startValues == null || endValues == null) {
131            return null;
132        }
133        Rect startBounds = (Rect) startValues.values.get(PROPNAME_BOUNDS);
134        Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS);
135        if (startBounds == null || endBounds == null) {
136            return null;
137        }
138
139        Matrix startMatrix = (Matrix) startValues.values.get(PROPNAME_MATRIX);
140        Matrix endMatrix = (Matrix) endValues.values.get(PROPNAME_MATRIX);
141
142        boolean matricesEqual = (startMatrix == null && endMatrix == null)
143                || (startMatrix != null && startMatrix.equals(endMatrix));
144
145        if (startBounds.equals(endBounds) && matricesEqual) {
146            return null;
147        }
148
149        final ImageView imageView = (ImageView) endValues.view;
150        Drawable drawable = imageView.getDrawable();
151        int drawableWidth = drawable.getIntrinsicWidth();
152        int drawableHeight = drawable.getIntrinsicHeight();
153
154        ImageViewUtils.startAnimateTransform(imageView);
155
156        ObjectAnimator animator;
157        if (drawableWidth == 0 || drawableHeight == 0) {
158            animator = createNullAnimator(imageView);
159        } else {
160            if (startMatrix == null) {
161                startMatrix = MatrixUtils.IDENTITY_MATRIX;
162            }
163            if (endMatrix == null) {
164                endMatrix = MatrixUtils.IDENTITY_MATRIX;
165            }
166            ANIMATED_TRANSFORM_PROPERTY.set(imageView, startMatrix);
167            animator = createMatrixAnimator(imageView, startMatrix, endMatrix);
168        }
169
170        ImageViewUtils.reserveEndAnimateTransform(imageView, animator);
171
172        return animator;
173    }
174
175    private ObjectAnimator createNullAnimator(ImageView imageView) {
176        return ObjectAnimator.ofObject(imageView, ANIMATED_TRANSFORM_PROPERTY,
177                NULL_MATRIX_EVALUATOR, null, null);
178    }
179
180    private ObjectAnimator createMatrixAnimator(final ImageView imageView, Matrix startMatrix,
181            final Matrix endMatrix) {
182        return ObjectAnimator.ofObject(imageView, ANIMATED_TRANSFORM_PROPERTY,
183                new TransitionUtils.MatrixEvaluator(), startMatrix, endMatrix);
184    }
185
186    private static Matrix copyImageMatrix(ImageView view) {
187        switch (view.getScaleType()) {
188            case FIT_XY:
189                return fitXYMatrix(view);
190            case CENTER_CROP:
191                return centerCropMatrix(view);
192            default:
193                return new Matrix(view.getImageMatrix());
194        }
195    }
196
197    /**
198     * Calculates the image transformation matrix for an ImageView with ScaleType FIT_XY. This
199     * needs to be manually calculated as the platform does not give us the value for this case.
200     */
201    private static Matrix fitXYMatrix(ImageView view) {
202        final Drawable image = view.getDrawable();
203        final Matrix matrix = new Matrix();
204        matrix.postScale(
205                ((float) view.getWidth()) / image.getIntrinsicWidth(),
206                ((float) view.getHeight()) / image.getIntrinsicHeight());
207        return matrix;
208    }
209
210    /**
211     * Calculates the image transformation matrix for an ImageView with ScaleType CENTER_CROP. This
212     * needs to be manually calculated for consistent behavior across all the API levels.
213     */
214    private static Matrix centerCropMatrix(ImageView view) {
215        final Drawable image = view.getDrawable();
216        final int imageWidth = image.getIntrinsicWidth();
217        final int imageViewWidth = view.getWidth();
218        final float scaleX = ((float) imageViewWidth) / imageWidth;
219
220        final int imageHeight = image.getIntrinsicHeight();
221        final int imageViewHeight = view.getHeight();
222        final float scaleY = ((float) imageViewHeight) / imageHeight;
223
224        final float maxScale = Math.max(scaleX, scaleY);
225
226        final float width = imageWidth * maxScale;
227        final float height = imageHeight * maxScale;
228        final int tx = Math.round((imageViewWidth - width) / 2f);
229        final int ty = Math.round((imageViewHeight - height) / 2f);
230
231        final Matrix matrix = new Matrix();
232        matrix.postScale(maxScale, maxScale);
233        matrix.postTranslate(tx, ty);
234        return matrix;
235    }
236
237}
238