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