1/*
2 * Copyright (C) 2014 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 com.android.tv.settings.util;
18
19import com.android.tv.settings.widget.RefcountBitmapDrawable;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Color;
24import android.graphics.ColorMatrix;
25import android.graphics.ColorMatrixColorFilter;
26import android.graphics.Rect;
27import android.graphics.RectF;
28import android.graphics.Region.Op;
29import android.graphics.drawable.BitmapDrawable;
30import android.util.AttributeSet;
31import android.view.View;
32import android.view.ViewGroup.LayoutParams;
33
34/**
35 * Util widget that can animate the following factors
36 * - scale of the view
37 * - position of the view
38 * - background color of the view
39 * - unclipped bounds of bitmap
40 * - clipping bounds of bitmap
41 * - alpha of bitmap
42 * - saturation of bitmap
43 */
44class TransitionImageView extends View {
45
46    private TransitionImage mSrc;
47    private TransitionImage mDst;
48
49    private BitmapDrawable mBitmapDrawable;
50
51    /**
52     * values for difference between src and dst
53     */
54    private float mScaleX;
55    private float mScaleY;
56    private float mScaleXDiff;
57    private float mScaleYDiff;
58    private float mTranslationXDiff;
59    private float mTranslationYDiff;
60    private float mClipLeftDiff;
61    private float mClipRightDiff;
62    private float mClipTopDiff;
63    private float mClipBottomDiff;
64    private float mUnclipCenterXDiff;
65    private float mUnclipCenterYDiff;
66    private float mUnclipWidthDiffBeforeScale;
67    private float mUnclipHeightDiffBeforeScale;
68    private float mSaturationDiff;
69    private float mAlphaDiff;
70    private int mBgAlphaDiff;
71    private int mBgRedDiff;
72    private int mBgGreenDiff;
73    private int mBgBlueDiff;
74    private boolean mBgHasDiff;
75
76    private float mProgress;
77    private Rect mSrcRect = new Rect();
78    private RectF mSrcUnclipRect = new RectF();
79    private RectF mSrcClipRect = new RectF();
80    private Rect mDstRect = new Rect();
81
82    private RectF mClipRect = new RectF();
83    private Rect mUnclipRect = new Rect();
84    private int mSrcBgColor;
85    private ColorMatrix mColorMatrix = new ColorMatrix();
86
87    private RectF mExcludeRect;
88
89    public TransitionImageView(Context context) {
90        this(context, null);
91    }
92
93    public TransitionImageView(Context context, AttributeSet attrs) {
94        this(context, attrs, 0);
95    }
96
97    public TransitionImageView(Context context, AttributeSet attrs, int defStyle) {
98        super(context, attrs, defStyle);
99        // the scale and translation is based on left/up corner of the view
100        setPivotX(0f);
101        setPivotY(0f);
102        setWillNotDraw(false);
103    }
104
105    public void setSourceTransition(TransitionImage src) {
106        mSrc = src;
107        initializeView();
108    }
109
110    public void setDestTransition(TransitionImage dst) {
111        mDst = dst;
112        calculateDiffs();
113    }
114
115    @Override
116    protected void onDetachedFromWindow() {
117        if (mBitmapDrawable instanceof RefcountBitmapDrawable) {
118            ((RefcountBitmapDrawable) mBitmapDrawable).getRefcountObject().releaseRef();
119        }
120        super.onDetachedFromWindow();
121    }
122
123    private void initializeView() {
124        mBitmapDrawable = mSrc.getBitmap();
125        mBitmapDrawable.mutate();
126
127        mSrc.getOptimizedRect(mSrcRect);
128        // initialize size
129        LayoutParams params = getLayoutParams();
130        params.width = mSrcRect.width();
131        params.height = mSrcRect.height();
132
133        // get src clip rect relative to the view
134        mSrcClipRect.set(mSrc.getClippedRect());
135        mSrcClipRect.offset(-mSrcRect.left, -mSrcRect.top);
136
137        // get src rect relative to the view
138        mSrcUnclipRect.set(mSrc.getUnclippedRect());
139        mSrcUnclipRect.offset(-mSrcRect.left, -mSrcRect.top);
140
141        // initialize alpha, saturation, background color
142        if (mSrc.getAlpha() != 1f) {
143            mBitmapDrawable.setAlpha((int) (mSrc.getAlpha() * 255));
144        }
145        if (mSrc.getSaturation() != 1f) {
146            mColorMatrix.setSaturation(mSrc.getSaturation());
147            mBitmapDrawable.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
148        }
149        mSrcBgColor = mSrc.getBackground();
150        if (mSrcBgColor != Color.TRANSPARENT) {
151            setBackgroundColor(mSrcBgColor);
152            getBackground().setAlpha((int) (mSrc.getAlpha() * 255));
153        }
154
155        invalidate();
156    }
157
158    private void calculateDiffs() {
159        mDst.getOptimizedRect(mDstRect);
160        mScaleX = (float) mDstRect.width() / mSrcRect.width();
161        mScaleY = (float) mDstRect.height() / mSrcRect.height();
162        mScaleXDiff = mScaleX - 1f;
163        mScaleYDiff = mScaleY - 1f;
164        mTranslationXDiff = mDstRect.left - mSrcRect.left;
165        mTranslationYDiff = mDstRect.top - mSrcRect.top;
166
167        RectF dstClipRect = new RectF();
168        // get dst clip rect relative to the view
169        dstClipRect.set(mDst.getClippedRect());
170        dstClipRect.offset(-mDstRect.left, -mDstRect.top);
171        // get dst clip rect before scaling
172        dstClipRect.left /= mScaleX;
173        dstClipRect.right /= mScaleX;
174        dstClipRect.top /= mScaleY;
175        dstClipRect.bottom /= mScaleY;
176        mClipLeftDiff = dstClipRect.left - mSrcClipRect.left;
177        mClipRightDiff = dstClipRect.right - mSrcClipRect.right;
178        mClipTopDiff = dstClipRect.top - mSrcClipRect.top;
179        mClipBottomDiff = dstClipRect.bottom - mSrcClipRect.bottom;
180
181        RectF dstUnclipRect = new RectF();
182        // get dst rect relative to the view
183        dstUnclipRect.set(mDst.getUnclippedRect());
184        dstUnclipRect.offset(-mDstRect.left, -mDstRect.top);
185        mUnclipWidthDiffBeforeScale = dstUnclipRect.width() - mSrcUnclipRect.width();
186        mUnclipHeightDiffBeforeScale = dstUnclipRect.height() - mSrcUnclipRect.height();
187        // get dst clip rect before scaling
188        dstUnclipRect.left /= mScaleX;
189        dstUnclipRect.right /= mScaleX;
190        dstUnclipRect.top /= mScaleY;
191        dstUnclipRect.bottom /= mScaleY;
192        mUnclipCenterXDiff = dstUnclipRect.centerX() - mSrcUnclipRect.centerX();
193        mUnclipCenterYDiff = dstUnclipRect.centerY() - mSrcUnclipRect.centerY();
194
195        mAlphaDiff = mDst.getAlpha() - mSrc.getAlpha();
196        int srcColor = mSrc.getBackground();
197        int dstColor = mDst.getBackground();
198        mBgAlphaDiff = Color.alpha(dstColor) - Color.alpha(srcColor);
199        mBgRedDiff = Color.red(dstColor) - Color.red(srcColor);
200        mBgGreenDiff = Color.green(dstColor) - Color.green(srcColor);
201        mBgBlueDiff = Color.blue(dstColor) - Color.blue(srcColor);
202        mSaturationDiff = mDst.getSaturation() - mSrc.getSaturation();
203        mBgHasDiff = mBgAlphaDiff != 0 || mBgRedDiff != 0 || mBgGreenDiff != 0
204                || mBgBlueDiff != 0;
205    }
206
207    public TransitionImage getSourceTransition() {
208        return mSrc;
209    }
210
211    public TransitionImage getDestTransition() {
212        return mDst;
213    }
214
215    public void setProgress(float progress) {
216        mProgress = progress;
217
218        // animating scale factor
219        setScaleX(1f + mScaleXDiff * mProgress);
220        setScaleY(1f + mScaleYDiff * mProgress);
221
222        // animating view position
223        setTranslationX(mSrcRect.left + mProgress * mTranslationXDiff);
224        setTranslationY(mSrcRect.top + mProgress * mTranslationYDiff);
225
226        // animating unclipped bitmap bounds
227        float unclipCenterX = mSrcUnclipRect.centerX() + mUnclipCenterXDiff * mProgress;
228        float unclipCenterY = mSrcUnclipRect.centerY() + mUnclipCenterYDiff * mProgress;
229        float unclipWidthBeforeScale =
230                mSrcUnclipRect.width() + mUnclipWidthDiffBeforeScale * mProgress;
231        float unclipHeightBeforeScale =
232                mSrcUnclipRect.height() + mUnclipHeightDiffBeforeScale * mProgress;
233        float unclipWidth = unclipWidthBeforeScale / getScaleX();
234        float unclipHeight = unclipHeightBeforeScale / getScaleY();
235        mUnclipRect.left = (int) (unclipCenterX - unclipWidth * 0.5f);
236        mUnclipRect.top = (int) (unclipCenterY - unclipHeight * 0.5f);
237        mUnclipRect.right = (int) (unclipCenterX + unclipWidth * 0.5f);
238        mUnclipRect.bottom = (int) (unclipCenterY + unclipHeight * 0.5f);
239        // rounding to integer will cause a shaking effect if the target unclip rect is much
240        // smaller than view bounds; e.g. a portrait bitmap inside a landscape imageView.
241        mBitmapDrawable.setBounds(mUnclipRect);
242
243        // animate clip bounds
244        mClipRect.left = mSrcClipRect.left + mClipLeftDiff * mProgress;
245        mClipRect.top = mSrcClipRect.top + mClipTopDiff * mProgress;
246        mClipRect.right = mSrcClipRect.right + mClipRightDiff * mProgress;
247        mClipRect.bottom = mSrcClipRect.bottom + mClipBottomDiff * mProgress;
248
249        // animate bitmap alpha, bitmap saturation
250        if (mAlphaDiff != 0f) {
251            int alpha = (int) ((mSrc.getAlpha() + mAlphaDiff * mProgress) * 255);
252            mBitmapDrawable.setAlpha(alpha);
253            if (getBackground() != null) {
254                getBackground().setAlpha(alpha);
255            }
256        }
257        if (mSaturationDiff != 0f) {
258            mColorMatrix.setSaturation(mSrc.getSaturation() + mSaturationDiff * mProgress);
259            mBitmapDrawable.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
260        }
261
262        // animate background color
263        if (mBgHasDiff) {
264            setBackgroundColor(Color.argb(
265                    Color.alpha(mSrcBgColor) + (int) (mBgAlphaDiff * mProgress),
266                    Color.red(mSrcBgColor) + (int) (mBgRedDiff * mProgress),
267                    Color.green(mSrcBgColor) + (int) (mBgGreenDiff * mProgress),
268                    Color.blue(mSrcBgColor) + (int) (mBgBlueDiff * mProgress)));
269        }
270
271        invalidate();
272    }
273
274    public float getProgress() {
275        return mProgress;
276    }
277
278    @Override
279    protected void onDraw(Canvas canvas) {
280        super.onDraw(canvas);
281        if (mBitmapDrawable == null) {
282            return;
283        }
284        int count = canvas.save();
285        canvas.clipRect(mClipRect);
286        if (mExcludeRect != null) {
287            canvas.clipRect(mExcludeRect, Op.DIFFERENCE);
288        }
289        mBitmapDrawable.draw(canvas);
290        canvas.restoreToCount(count);
291    }
292
293    public void setExcludeClipRect(RectF rect) {
294        if (mExcludeRect == null) {
295            mExcludeRect = new RectF();
296        }
297        mExcludeRect.set(rect);
298        // get rect relative to left/top corner of the view
299        mExcludeRect.offset(-getX(), -getY());
300        // get locations before scale applied
301        mExcludeRect.left /= (1f + mScaleXDiff * mProgress);
302        mExcludeRect.right /= (1f + mScaleXDiff * mProgress);
303        mExcludeRect.top /= (1f + mScaleYDiff * mProgress);
304        mExcludeRect.bottom /= (1f + mScaleYDiff * mProgress);
305    }
306
307    public void clearExcludeClipRect() {
308        mExcludeRect = null;
309    }
310}
311