• 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.view.Display;
31 import android.view.View;
32 import android.widget.ImageView;
33 
34 import androidx.annotation.Nullable;
35 import androidx.annotation.WorkerThread;
36 
37 import com.android.wallpaper.module.BitmapCropper;
38 import com.android.wallpaper.module.InjectorProvider;
39 import com.android.wallpaper.util.ScreenSizeCalculator;
40 import com.android.wallpaper.util.WallpaperCropUtils;
41 
42 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
43 
44 /**
45  * Interface representing an image asset.
46  */
47 public abstract class Asset {
48 
49     /**
50      * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and
51      * filled completely with pixels of the provided placeholder color.
52      */
getPlaceholderDrawable( Context context, ImageView imageView, int placeholderColor)53     protected static Drawable getPlaceholderDrawable(
54             Context context, ImageView imageView, int placeholderColor) {
55         Point imageViewDimensions = getViewDimensions(imageView);
56         Bitmap placeholderBitmap =
57                 Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888);
58         placeholderBitmap.eraseColor(placeholderColor);
59         return new BitmapDrawable(context.getResources(), placeholderBitmap);
60     }
61 
62     /**
63      * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't
64      * been laid out yet, then gets the absolute value of the layout params.
65      */
getViewDimensions(View view)66     private static Point getViewDimensions(View view) {
67         int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width);
68         int height = view.getHeight() > 0 ? view.getHeight()
69                 : Math.abs(view.getLayoutParams().height);
70 
71         return new Point(width, height);
72     }
73 
74     /**
75      * Decodes a bitmap sized for the destination view's dimensions off the main UI thread.
76      *
77      * @param targetWidth  Width of target view in physical pixels.
78      * @param targetHeight Height of target view in physical pixels.
79      * @param receiver     Called with the decoded bitmap or null if there was an error decoding the
80      *                     bitmap.
81      */
decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)82     public abstract void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver);
83 
84     /**
85      * Decodes and downscales a bitmap region off the main UI thread.
86      * @param rect         Rect representing the crop region in terms of the original image's
87      *                     resolution.
88      * @param targetWidth  Width of target view in physical pixels.
89      * @param targetHeight Height of target view in physical pixels.
90      * @param shouldAdjustForRtl whether the region selected should be adjusted for RTL (that is,
91      *                           the crop region will be considered starting from the right)
92      * @param receiver     Called with the decoded bitmap region or null if there was an error
93      */
decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)94     public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight,
95             boolean shouldAdjustForRtl, BitmapReceiver receiver);
96 
97     /**
98      * Calculates the raw dimensions of the asset at its original resolution off the main UI thread.
99      * Avoids decoding the entire bitmap if possible to conserve memory.
100      *
101      * @param activity Activity in which this decoding request is made. Allows for early termination
102      *                 of fetching image data and/or decoding to a bitmap. May be null, in which
103      *                 case the request is made in the application context instead.
104      * @param receiver Called with the decoded raw dimensions of the whole image or null if there
105      *                 was an error decoding the dimensions.
106      */
decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)107     public abstract void decodeRawDimensions(@Nullable Activity activity,
108             DimensionsReceiver receiver);
109 
110     /**
111      * Returns whether this asset has access to a separate, lower fidelity source of image data
112      * (that may be able to be loaded more quickly to simulate progressive loading).
113      */
hasLowResDataSource()114     public boolean hasLowResDataSource() {
115         return false;
116     }
117 
118     /**
119      * Loads the asset from the separate low resolution data source (if there is one) into the
120      * provided ImageView with the placeholder color and bitmap transformation.
121      *
122      * @param transformation Bitmap transformation that can transform the thumbnail image
123      *                       post-decoding.
124      */
loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)125     public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor,
126             BitmapTransformation transformation) {
127         // No op
128     }
129 
130     /**
131      * Returns a Bitmap from the separate low resolution data source (if there is one) or
132      * {@code null} otherwise.
133      * This could be an I/O operation so DO NOT CALL ON UI THREAD
134      */
135     @WorkerThread
136     @Nullable
getLowResBitmap(Context context)137     public Bitmap getLowResBitmap(Context context) {
138         return null;
139     }
140 
141     /**
142      * Returns whether the asset supports rendering tile regions at varying pixel densities.
143      */
supportsTiling()144     public abstract boolean supportsTiling();
145 
146     /**
147      * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to
148      * load, first loads a ColorDrawable based on the provided placeholder color.
149      *
150      * @param context          Activity hosting the ImageView.
151      * @param imageView        ImageView which is the target view of this asset.
152      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
153      *                         load.
154      */
loadDrawable(final Context context, final ImageView imageView, int placeholderColor)155     public void loadDrawable(final Context context, final ImageView imageView,
156             int placeholderColor) {
157         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
158         // question is empty.
159         final boolean needsTransition = imageView.getDrawable() == null;
160         final Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
161         if (needsTransition) {
162             imageView.setImageDrawable(placeholderDrawable);
163         }
164 
165         // Set requested height and width to the either the actual height and width of the view in
166         // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout
167         // params.
168         int width = imageView.getWidth() > 0
169                 ? imageView.getWidth()
170                 : Math.abs(imageView.getLayoutParams().width);
171         int height = imageView.getHeight() > 0
172                 ? imageView.getHeight()
173                 : Math.abs(imageView.getLayoutParams().height);
174 
175         decodeBitmap(width, height, new BitmapReceiver() {
176             @Override
177             public void onBitmapDecoded(Bitmap bitmap) {
178                 if (!needsTransition) {
179                     imageView.setImageBitmap(bitmap);
180                     return;
181                 }
182 
183                 Resources resources = context.getResources();
184 
185                 Drawable[] layers = new Drawable[2];
186                 layers[0] = placeholderDrawable;
187                 layers[1] = new BitmapDrawable(resources, bitmap);
188 
189                 TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
190                 transitionDrawable.setCrossFadeEnabled(true);
191 
192                 imageView.setImageDrawable(transitionDrawable);
193                 transitionDrawable.startTransition(resources.getInteger(
194                         android.R.integer.config_shortAnimTime));
195             }
196         });
197     }
198 
199     /**
200      * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition
201      * with the given duration from the Drawable previously set on the ImageView.
202      *
203      * @param context                  Activity hosting the ImageView.
204      * @param imageView                ImageView which is the target view of this asset.
205      * @param transitionDurationMillis Duration of the crossfade, in milliseconds.
206      * @param drawableLoadedListener   Listener called once the transition has begun.
207      * @param placeholderColor         Color of the placeholder if the provided ImageView is empty
208      *                                 before the
209      */
loadDrawableWithTransition( final Context context, final ImageView imageView, final int transitionDurationMillis, @Nullable final DrawableLoadedListener drawableLoadedListener, int placeholderColor)210     public void loadDrawableWithTransition(
211             final Context context,
212             final ImageView imageView,
213             final int transitionDurationMillis,
214             @Nullable final DrawableLoadedListener drawableLoadedListener,
215             int placeholderColor) {
216         Point imageViewDimensions = getViewDimensions(imageView);
217 
218         // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
219         // question is empty.
220         boolean needsPlaceholder = imageView.getDrawable() == null;
221         if (needsPlaceholder) {
222             imageView.setImageDrawable(
223                     getPlaceholderDrawable(context, imageView, placeholderColor));
224         }
225 
226         decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() {
227             @Override
228             public void onBitmapDecoded(Bitmap bitmap) {
229                 final Resources resources = context.getResources();
230 
231                 new CenterCropBitmapTask(bitmap, imageView, new BitmapReceiver() {
232                     @Override
233                     public void onBitmapDecoded(@Nullable Bitmap newBitmap) {
234                         Drawable[] layers = new Drawable[2];
235                         Drawable existingDrawable = imageView.getDrawable();
236 
237                         if (existingDrawable instanceof TransitionDrawable) {
238                             // Take only the second layer in the existing TransitionDrawable so
239                             // we don't keep
240                             // around a reference to older layers which are no longer shown (this
241                             // way we avoid a
242                             // memory leak).
243                             TransitionDrawable existingTransitionDrawable =
244                                     (TransitionDrawable) existingDrawable;
245                             int id = existingTransitionDrawable.getId(1);
246                             layers[0] = existingTransitionDrawable.findDrawableByLayerId(id);
247                         } else {
248                             layers[0] = existingDrawable;
249                         }
250                         layers[1] = new BitmapDrawable(resources, newBitmap);
251 
252                         TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
253                         transitionDrawable.setCrossFadeEnabled(true);
254 
255                         imageView.setImageDrawable(transitionDrawable);
256                         transitionDrawable.startTransition(transitionDurationMillis);
257 
258                         if (drawableLoadedListener != null) {
259                             drawableLoadedListener.onDrawableLoaded();
260                         }
261                     }
262                 }).execute();
263             }
264         });
265     }
266 
267     /**
268      * Loads the image for this asset into the provided ImageView which is used for the preview.
269      * While waiting for the image to load, first loads a ColorDrawable based on the provided
270      * placeholder color.
271      *
272      * @param activity         Activity hosting the ImageView.
273      * @param imageView        ImageView which is the target view of this asset.
274      * @param placeholderColor Color of placeholder set to ImageView while waiting for image to
275      *                         load.
276      */
loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor)277     public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor) {
278         boolean needsTransition = imageView.getDrawable() == null;
279         Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
280         if (needsTransition) {
281             imageView.setImageDrawable(placeholderDrawable);
282         }
283 
284         decodeRawDimensions(activity, dimensions -> {
285             if (dimensions == null) {
286                 loadDrawable(activity, imageView, placeholderColor);
287                 return;
288             }
289 
290             Display defaultDisplay = activity.getWindowManager().getDefaultDisplay();
291             Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay);
292             Rect visibleRawWallpaperRect =
293                     WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize);
294             adjustCropRect(activity, dimensions, visibleRawWallpaperRect);
295 
296             BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper();
297             bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect,
298                     WallpaperCropUtils.isRtl(activity),
299                     new BitmapCropper.Callback() {
300                         @Override
301                         public void onBitmapCropped(Bitmap croppedBitmap) {
302                             // Since the size of the cropped bitmap may not exactly the same with
303                             // image view(maybe has 1px or 2px difference),
304                             // so set CENTER_CROP to let the bitmap to fit the image view.
305                             imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
306                             if (!needsTransition) {
307                                 imageView.setImageBitmap(croppedBitmap);
308                                 return;
309                             }
310 
311                             Resources resources = activity.getResources();
312 
313                             Drawable[] layers = new Drawable[2];
314                             layers[0] = placeholderDrawable;
315                             layers[1] = new BitmapDrawable(resources, croppedBitmap);
316 
317                             TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
318                             transitionDrawable.setCrossFadeEnabled(true);
319 
320                             imageView.setImageDrawable(transitionDrawable);
321                             transitionDrawable.startTransition(resources.getInteger(
322                                     android.R.integer.config_shortAnimTime));
323                         }
324 
325                         @Override
326                         public void onError(@Nullable Throwable e) {
327 
328                         }
329                     });
330         });
331     }
332 
333     /**
334      * Interface for receiving decoded Bitmaps.
335      */
336     public interface BitmapReceiver {
337 
338         /**
339          * Called with a decoded Bitmap object or null if there was an error decoding the bitmap.
340          */
onBitmapDecoded(@ullable Bitmap bitmap)341         void onBitmapDecoded(@Nullable Bitmap bitmap);
342     }
343 
344     /**
345      * Interface for receiving raw asset dimensions.
346      */
347     public interface DimensionsReceiver {
348 
349         /**
350          * Called with raw dimensions of asset or null if the asset is unable to decode the raw
351          * dimensions.
352          *
353          * @param dimensions Dimensions as a Point where width is represented by "x" and height by
354          *                   "y".
355          */
onDimensionsDecoded(@ullable Point dimensions)356         void onDimensionsDecoded(@Nullable Point dimensions);
357     }
358 
359     /**
360      * Interface for being notified when a drawable has been loaded.
361      */
362     public interface DrawableLoadedListener {
onDrawableLoaded()363         void onDrawableLoaded();
364     }
365 
adjustCropRect(Context context, Point assetDimensions, Rect cropRect)366     protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect) {
367         WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */);
368     }
369 
370     /**
371      * Custom AsyncTask which returns a copy of the given bitmap which is center cropped and scaled
372      * to fit in the given ImageView.
373      */
374     public static class CenterCropBitmapTask extends AsyncTask<Void, Void, Bitmap> {
375 
376         private Bitmap mBitmap;
377         private BitmapReceiver mBitmapReceiver;
378 
379         private int mImageViewWidth;
380         private int mImageViewHeight;
381 
CenterCropBitmapTask(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver)382         public CenterCropBitmapTask(Bitmap bitmap, View view,
383                 BitmapReceiver bitmapReceiver) {
384             mBitmap = bitmap;
385             mBitmapReceiver = bitmapReceiver;
386 
387             Point imageViewDimensions = getViewDimensions(view);
388 
389             mImageViewWidth = imageViewDimensions.x;
390             mImageViewHeight = imageViewDimensions.y;
391         }
392 
393         @Override
doInBackground(Void... unused)394         protected Bitmap doInBackground(Void... unused) {
395             int measuredWidth = mImageViewWidth;
396             int measuredHeight = mImageViewHeight;
397 
398             int bitmapWidth = mBitmap.getWidth();
399             int bitmapHeight = mBitmap.getHeight();
400 
401             float scale = Math.min(
402                     (float) bitmapWidth / measuredWidth,
403                     (float) bitmapHeight / measuredHeight);
404 
405             Bitmap scaledBitmap = Bitmap.createScaledBitmap(
406                     mBitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale),
407                     true);
408 
409             int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2);
410             int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2);
411 
412             return Bitmap.createBitmap(
413                     scaledBitmap,
414                     horizontalGutterPx,
415                     verticalGutterPx,
416                     scaledBitmap.getWidth() - (2 * horizontalGutterPx),
417                     scaledBitmap.getHeight() - (2 * verticalGutterPx));
418         }
419 
420         @Override
onPostExecute(Bitmap newBitmap)421         protected void onPostExecute(Bitmap newBitmap) {
422             mBitmapReceiver.onBitmapDecoded(newBitmap);
423         }
424     }
425 }
426