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 
17 package com.android.tv.settings.util;
18 
19 import android.content.Context;
20 import android.graphics.Canvas;
21 import android.graphics.Color;
22 import android.graphics.ColorMatrix;
23 import android.graphics.ColorMatrixColorFilter;
24 import android.graphics.Rect;
25 import android.graphics.RectF;
26 import android.graphics.Region.Op;
27 import android.graphics.drawable.BitmapDrawable;
28 import android.util.AttributeSet;
29 import android.view.View;
30 import android.view.ViewGroup.LayoutParams;
31 
32 import com.android.tv.settings.widget.RefcountBitmapDrawable;
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  */
44 class 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 final Rect mSrcRect = new Rect();
78     private final RectF mSrcUnclipRect = new RectF();
79     private final RectF mSrcClipRect = new RectF();
80     private final Rect mDstRect = new Rect();
81 
82     private final RectF mClipRect = new RectF();
83     private final Rect mUnclipRect = new Rect();
84     private int mSrcBgColor;
85     private final ColorMatrix mColorMatrix = new ColorMatrix();
86 
87     private RectF mExcludeRect;
88 
TransitionImageView(Context context)89     public TransitionImageView(Context context) {
90         this(context, null);
91     }
92 
TransitionImageView(Context context, AttributeSet attrs)93     public TransitionImageView(Context context, AttributeSet attrs) {
94         this(context, attrs, 0);
95     }
96 
TransitionImageView(Context context, AttributeSet attrs, int defStyle)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 
setSourceTransition(TransitionImage src)105     public void setSourceTransition(TransitionImage src) {
106         mSrc = src;
107         initializeView();
108     }
109 
setDestTransition(TransitionImage dst)110     public void setDestTransition(TransitionImage dst) {
111         mDst = dst;
112         calculateDiffs();
113     }
114 
115     @Override
onDetachedFromWindow()116     protected void onDetachedFromWindow() {
117         if (mBitmapDrawable instanceof RefcountBitmapDrawable) {
118             ((RefcountBitmapDrawable) mBitmapDrawable).getRefcountObject().releaseRef();
119         }
120         super.onDetachedFromWindow();
121     }
122 
initializeView()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 
calculateDiffs()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 
getSourceTransition()207     public TransitionImage getSourceTransition() {
208         return mSrc;
209     }
210 
getDestTransition()211     public TransitionImage getDestTransition() {
212         return mDst;
213     }
214 
setProgress(float progress)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 
getProgress()274     public float getProgress() {
275         return mProgress;
276     }
277 
278     @Override
onDraw(Canvas canvas)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 
setExcludeClipRect(RectF rect)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 
clearExcludeClipRect()307     public void clearExcludeClipRect() {
308         mExcludeRect = null;
309     }
310 }
311