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 package com.android.wallpaper.asset;
17 
18 import android.app.Activity;
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.graphics.Bitmap;
22 import android.graphics.Bitmap.Config;
23 import android.graphics.Point;
24 import android.graphics.Rect;
25 import android.graphics.drawable.BitmapDrawable;
26 import android.graphics.drawable.ColorDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.graphics.drawable.TransitionDrawable;
29 import android.os.AsyncTask;
30 import android.view.View;
31 import android.widget.ImageView;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
36 
37 /**
38  * Interface representing an image asset.
39  */
40 public abstract class Asset {
41 
42     /**
43      * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and
44      * filled completely with pixels of the provided placeholder color.
45      */
getPlaceholderDrawable( Context context, ImageView imageView, int placeholderColor)46     protected static Drawable getPlaceholderDrawable(
47             Context context, ImageView imageView, int placeholderColor) {
48         Point imageViewDimensions = getViewDimensions(imageView);
49         Bitmap placeholderBitmap =
50                 Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888);
51         placeholderBitmap.eraseColor(placeholderColor);
52         return new BitmapDrawable(context.getResources(), placeholderBitmap);
53     }
54 
55     /**
56      * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't
57      * been laid out yet, then gets the absolute value of the layout params.
58      */
getViewDimensions(View view)59     private static Point getViewDimensions(View view) {
60         int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width);
61         int height = view.getHeight() > 0 ? view.getHeight()
62                 : Math.abs(view.getLayoutParams().height);
63 
64         return new Point(width, height);
65     }
66 
67     /**
68      * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
69      *
70      * @param targetWidth  Width of target view in physical pixels.
71      * @param targetHeight Height of target view in physical pixels.
72      * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
73      *                     bitmap.
74      */
decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)75     public abstract void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver);
76 
77     /**
78      * Decodes and downscales a bitmap region off the main UI thread.
79      *
80      * @param rect         Rect representing the crop region in terms of the original image's
81      *                     resolution.
82      * @param targetWidth  Width of target view in physical pixels.
83      * @param targetHeight Height of target view in physical pixels.
84      * @param receiver     Called with the decoded bitmap region or null if there was an error
85      *                     decoding the bitmap region.
86      */
decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, BitmapReceiver receiver)87     public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
88             BitmapReceiver receiver);
89 
90     /**
91      * Calculates the raw dimensions of the asset at its original resolution off the main UI thread.
92      * Avoids decoding the entire bitmap if possible to conserve memory.
93      *
94      * @param activity Activity in which this decoding request is made. Allows for early termination
95      *                 of fetching image data and/or decoding to a bitmap. May be null, in which
96      *                 case the request is made in the application context instead.
97      * @param receiver Called with the decoded raw dimensions of the whole image or null if there
98      *                 was an error decoding the dimensions.
99      */
decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)100     public abstract void decodeRawDimensions(@Nullable Activity activity,
101             DimensionsReceiver receiver);
102 
103     /**
104      * Returns whether this asset has access to a separate, lower fidelity source of image data
105      * (that may be able to be loaded more quickly to simulate progressive loading).
106      */
hasLowResDataSource()107     public boolean hasLowResDataSource() {
108         return false;
109     }
110 
111     /**
112      * Loads the asset from the separate low resolution data source (if there is one) into the
113      * provided ImageView with the placeholder color and bitmap transformation.
114      *
115      * @param transformation Bitmap transformation that can transform the thumbnail image
116      *                       post-decoding.
117      */
loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)118     public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
119             BitmapTransformation transformation) {
120         // No op
121     }
122 
123     /**
124      * Returns whether the asset supports rendering tile regions at varying pixel densities.
125      */
supportsTiling()126     public abstract boolean supportsTiling();
127 
128     /**
129      * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to
130      * load, first loads a ColorDrawable based on the provided placeholder color.
131      *
132      * @param context          Activity hosting the ImageView.
133      * @param imageView        ImageView which is the target view of this asset.
134      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
135      *                         load.
136      */
loadDrawable(final Context context, final ImageView imageView, int placeholderColor)137     public void loadDrawable(final Context context, final ImageView imageView,
138             int placeholderColor) {
139         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
140         // question is empty.
141         final boolean needsTransition = imageView.getDrawable() == null;
142         final Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
143         if (needsTransition) {
144             imageView.setImageDrawable(placeholderDrawable);
145         }
146 
147         // Set requested height and width to the either the actual height and width of the view in
148         // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout
149         // params.
150         int width = imageView.getWidth() > 0
151                 ? imageView.getWidth()
152                 : Math.abs(imageView.getLayoutParams().width);
153         int height = imageView.getHeight() > 0
154                 ? imageView.getHeight()
155                 : Math.abs(imageView.getLayoutParams().height);
156 
157         decodeBitmap(width, height, new BitmapReceiver() {
158             @Override
159             public void onBitmapDecoded(Bitmap bitmap) {
160                 if (!needsTransition) {
161                     imageView.setImageBitmap(bitmap);
162                     return;
163                 }
164 
165                 Resources resources = context.getResources();
166 
167                 Drawable[] layers = new Drawable[2];
168                 layers[0] = placeholderDrawable;
169                 layers[1] = new BitmapDrawable(resources, bitmap);
170 
171                 TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
172                 transitionDrawable.setCrossFadeEnabled(true);
173 
174                 imageView.setImageDrawable(transitionDrawable);
175                 transitionDrawable.startTransition(resources.getInteger(
176                         android.R.integer.config_shortAnimTime));
177             }
178         });
179     }
180 
181     /**
182      * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition
183      * with the given duration from the Drawable previously set on the ImageView.
184      *
185      * @param context                  Activity hosting the ImageView.
186      * @param imageView                ImageView which is the target view of this asset.
187      * @param transitionDurationMillis Duration of the crossfade, in milliseconds.
188      * @param drawableLoadedListener   Listener called once the transition has begun.
189      * @param placeholderColor         Color of the placeholder if the provided ImageView is empty
190      *                                 before the
191      */
loadDrawableWithTransition( final Context context, final ImageView imageView, final int transitionDurationMillis, @Nullable final DrawableLoadedListener drawableLoadedListener, int placeholderColor)192     public void loadDrawableWithTransition(
193             final Context context,
194             final ImageView imageView,
195             final int transitionDurationMillis,
196             @Nullable final DrawableLoadedListener drawableLoadedListener,
197             int placeholderColor) {
198         Point imageViewDimensions = getViewDimensions(imageView);
199 
200         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
201         // question is empty.
202         boolean needsPlaceholder = imageView.getDrawable() == null;
203         if (needsPlaceholder) {
204             imageView.setImageDrawable(
205                     getPlaceholderDrawable(context, imageView, placeholderColor));
206         }
207 
208         decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() {
209             @Override
210             public void onBitmapDecoded(Bitmap bitmap) {
211                 final Resources resources = context.getResources();
212 
213                 new CenterCropBitmapTask(bitmap, imageView, new BitmapReceiver() {
214                     @Override
215                     public void onBitmapDecoded(@Nullable Bitmap newBitmap) {
216                         Drawable[] layers = new Drawable[2];
217                         Drawable existingDrawable = imageView.getDrawable();
218 
219                         if (existingDrawable instanceof TransitionDrawable) {
220                             // Take only the second layer in the existing TransitionDrawable so
221                             // we don't keep
222                             // around a reference to older layers which are no longer shown (this
223                             // way we avoid a
224                             // memory leak).
225                             TransitionDrawable existingTransitionDrawable =
226                                     (TransitionDrawable) existingDrawable;
227                             int id = existingTransitionDrawable.getId(1);
228                             layers[0] = existingTransitionDrawable.findDrawableByLayerId(id);
229                         } else {
230                             layers[0] = existingDrawable;
231                         }
232                         layers[1] = new BitmapDrawable(resources, newBitmap);
233 
234                         TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
235                         transitionDrawable.setCrossFadeEnabled(true);
236 
237                         imageView.setImageDrawable(transitionDrawable);
238                         transitionDrawable.startTransition(transitionDurationMillis);
239 
240                         if (drawableLoadedListener != null) {
241                             drawableLoadedListener.onDrawableLoaded();
242                         }
243                     }
244                 }).execute();
245             }
246         });
247     }
248 
249     /**
250      * Interface for receiving decoded Bitmaps.
251      */
252     public interface BitmapReceiver {
253 
254         /**
255          * Called with a decoded Bitmap object or null if there was an error decoding the bitmap.
256          */
onBitmapDecoded(@ullable Bitmap bitmap)257         void onBitmapDecoded(@Nullable Bitmap bitmap);
258     }
259 
260     /**
261      * Interface for receiving raw asset dimensions.
262      */
263     public interface DimensionsReceiver {
264 
265         /**
266          * Called with raw dimensions of asset or null if the asset is unable to decode the raw
267          * dimensions.
268          *
269          * @param dimensions Dimensions as a Point where width is represented by "x" and height by
270          *                   "y".
271          */
onDimensionsDecoded(@ullable Point dimensions)272         void onDimensionsDecoded(@Nullable Point dimensions);
273     }
274 
275     /**
276      * Interface for being notified when a drawable has been loaded.
277      */
278     public interface DrawableLoadedListener {
onDrawableLoaded()279         void onDrawableLoaded();
280     }
281 
282     /**
283      * Custom AsyncTask which returns a copy of the given bitmap which is center cropped and scaled
284      * to fit in the given ImageView.
285      */
286     public static class CenterCropBitmapTask extends AsyncTask<Void, Void, Bitmap> {
287 
288         private Bitmap mBitmap;
289         private BitmapReceiver mBitmapReceiver;
290 
291         private int mImageViewWidth;
292         private int mImageViewHeight;
293 
CenterCropBitmapTask(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver)294         public CenterCropBitmapTask(Bitmap bitmap, View view,
295                 BitmapReceiver bitmapReceiver) {
296             mBitmap = bitmap;
297             mBitmapReceiver = bitmapReceiver;
298 
299             Point imageViewDimensions = getViewDimensions(view);
300 
301             mImageViewWidth = imageViewDimensions.x;
302             mImageViewHeight = imageViewDimensions.y;
303         }
304 
305         @Override
doInBackground(Void... unused)306         protected Bitmap doInBackground(Void... unused) {
307             int measuredWidth = mImageViewWidth;
308             int measuredHeight = mImageViewHeight;
309 
310             int bitmapWidth = mBitmap.getWidth();
311             int bitmapHeight = mBitmap.getHeight();
312 
313             float scale = Math.min(
314                     (float) bitmapWidth / measuredWidth,
315                     (float) bitmapHeight / measuredHeight);
316 
317             Bitmap scaledBitmap = Bitmap.createScaledBitmap(
318                     mBitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale),
319                     true);
320 
321             int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2);
322             int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2);
323 
324             return Bitmap.createBitmap(
325                     scaledBitmap,
326                     horizontalGutterPx,
327                     verticalGutterPx,
328                     scaledBitmap.getWidth() - (2 * horizontalGutterPx),
329                     scaledBitmap.getHeight() - (2 * verticalGutterPx));
330         }
331 
332         @Override
onPostExecute(Bitmap newBitmap)333         protected void onPostExecute(Bitmap newBitmap) {
334             mBitmapReceiver.onBitmapDecoded(newBitmap);
335         }
336     }
337 }
338