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