blob: 5c00f86dc01bcf4475d27235c2f69f9137498118 [file] [log] [blame]
Jon Miranda16ea1b12017-12-12 14:52:48 -08001/*
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 */
16package com.android.wallpaper.asset;
17
18import android.app.Activity;
19import android.content.res.Resources;
20import android.graphics.Bitmap;
21import android.graphics.Bitmap.Config;
22import android.graphics.Point;
23import android.graphics.Rect;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.ColorDrawable;
26import android.graphics.drawable.Drawable;
27import android.graphics.drawable.TransitionDrawable;
28import android.os.AsyncTask;
29import android.support.annotation.Nullable;
30import android.widget.ImageView;
31
32import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
33
34/**
35 * Interface representing an image asset.
36 */
37public abstract class Asset {
38
39 /**
40 * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and
41 * filled completely with pixels of the provided placeholder color.
42 */
43 protected static Drawable getPlaceholderDrawable(
44 Activity activity, ImageView imageView, int placeholderColor) {
45 Point imageViewDimensions = getImageViewDimensions(imageView);
46 Bitmap placeholderBitmap =
47 Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888);
48 placeholderBitmap.eraseColor(placeholderColor);
49 return new BitmapDrawable(activity.getResources(), placeholderBitmap);
50 }
51
52 /**
53 * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't been
54 * laid out yet, then gets the absolute value of the layout params.
55 */
56 private static Point getImageViewDimensions(ImageView imageView) {
57 int width = imageView.getWidth() > 0
58 ? imageView.getWidth()
59 : Math.abs(imageView.getLayoutParams().width);
60 int height = imageView.getHeight() > 0
61 ? imageView.getHeight()
62 : Math.abs(imageView.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 */
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 */
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 */
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 */
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 */
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 */
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 *
131 * @param activity Activity hosting the ImageView.
132 * @param imageView ImageView which is the target view of this asset.
133 * @param placeholderColor Color of placeholder set to ImageView while waiting for image to load.
134 */
135 public void loadDrawable(final Activity activity, final ImageView imageView,
136 int placeholderColor) {
137 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
138 // question is empty.
139 final boolean needsTransition = imageView.getDrawable() == null;
140 final Drawable placeholderDrawable = new ColorDrawable(placeholderColor);
141 if (needsTransition) {
142 imageView.setImageDrawable(placeholderDrawable);
143 }
144
145 // Set requested height and width to the either the actual height and width of the view in
146 // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout params.
147 int width = imageView.getWidth() > 0
148 ? imageView.getWidth()
149 : Math.abs(imageView.getLayoutParams().width);
150 int height = imageView.getHeight() > 0
151 ? imageView.getHeight()
152 : Math.abs(imageView.getLayoutParams().height);
153
154 decodeBitmap(width, height, new BitmapReceiver() {
155 @Override
156 public void onBitmapDecoded(Bitmap bitmap) {
157 if (!needsTransition) {
158 imageView.setImageBitmap(bitmap);
159 return;
160 }
161
162 Resources resources = activity.getResources();
163
164 Drawable[] layers = new Drawable[2];
165 layers[0] = placeholderDrawable;
166 layers[1] = new BitmapDrawable(resources, bitmap);
167
168 TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
169 transitionDrawable.setCrossFadeEnabled(true);
170
171 imageView.setImageDrawable(transitionDrawable);
172 transitionDrawable.startTransition(resources.getInteger(
173 android.R.integer.config_shortAnimTime));
174 }
175 });
176 }
177
178 /**
179 * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition
180 * with the given duration from the Drawable previously set on the ImageView.
181 *
182 * @param activity 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 * drawable loads.
188 */
189 public void loadDrawableWithTransition(
190 final Activity activity,
191 final ImageView imageView,
192 final int transitionDurationMillis,
193 final DrawableLoadedListener drawableLoadedListener,
194 int placeholderColor) {
195 Point imageViewDimensions = getImageViewDimensions(imageView);
196
197 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in
198 // question is empty.
199 boolean needsPlaceholder = imageView.getDrawable() == null;
200 if (needsPlaceholder) {
201 imageView.setImageDrawable(getPlaceholderDrawable(activity, imageView, placeholderColor));
202 }
203
204 decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() {
205 @Override
206 public void onBitmapDecoded(Bitmap bitmap) {
207 final Resources resources = activity.getResources();
208
209 new CenterCropBitmapTask(bitmap, imageView, new BitmapReceiver() {
210 @Override
211 public void onBitmapDecoded(@Nullable Bitmap newBitmap) {
212 Drawable[] layers = new Drawable[2];
213 Drawable existingDrawable = imageView.getDrawable();
214
215 if (existingDrawable instanceof TransitionDrawable) {
216 // Take only the second layer in the existing TransitionDrawable so we don't keep
217 // around a reference to older layers which are no longer shown (this way we avoid a
218 // memory leak).
219 TransitionDrawable existingTransitionDrawable =
220 (TransitionDrawable) existingDrawable;
221 int id = existingTransitionDrawable.getId(1);
222 layers[0] = existingTransitionDrawable.findDrawableByLayerId(id);
223 } else {
224 layers[0] = existingDrawable;
225 }
226 layers[1] = new BitmapDrawable(resources, newBitmap);
227
228 TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
229 transitionDrawable.setCrossFadeEnabled(true);
230
231 imageView.setImageDrawable(transitionDrawable);
232 transitionDrawable.startTransition(transitionDurationMillis);
233
234 drawableLoadedListener.onDrawableLoaded();
235 }
236 }).execute();
237 }
238 });
239 }
240
241 /**
242 * Interface for receiving decoded Bitmaps.
243 */
244 public interface BitmapReceiver {
245
246 /**
247 * Called with a decoded Bitmap object or null if there was an error decoding the bitmap.
248 */
249 void onBitmapDecoded(@Nullable Bitmap bitmap);
250 }
251
252 /**
253 * Interface for receiving raw asset dimensions.
254 */
255 public interface DimensionsReceiver {
256
257 /**
258 * Called with raw dimensions of asset or null if the asset is unable to decode the raw
259 * dimensions.
260 *
261 * @param dimensions Dimensions as a Point where width is represented by "x" and height by "y".
262 */
263 void onDimensionsDecoded(@Nullable Point dimensions);
264 }
265
266 /**
267 * Interface for being notified when a drawable has been loaded.
268 */
269 public interface DrawableLoadedListener {
270 void onDrawableLoaded();
271 }
272
273 /**
274 * Custom AsyncTask which returns a copy of the given bitmap which is center cropped and scaled to
275 * fit in the given ImageView.
276 */
277 protected static class CenterCropBitmapTask extends AsyncTask<Void, Void, Bitmap> {
278
279 private Bitmap mBitmap;
280 private BitmapReceiver mBitmapReceiver;
281
282 private int mImageViewWidth;
283 private int mImageViewHeight;
284
285 public CenterCropBitmapTask(Bitmap bitmap, ImageView imageView,
286 BitmapReceiver bitmapReceiver) {
287 mBitmap = bitmap;
288 mBitmapReceiver = bitmapReceiver;
289
290 Point imageViewDimensions = getImageViewDimensions(imageView);
291
292 mImageViewWidth = imageViewDimensions.x;
293 mImageViewHeight = imageViewDimensions.y;
294 }
295
296 @Override
297 protected Bitmap doInBackground(Void... unused) {
298 int measuredWidth = mImageViewWidth;
299 int measuredHeight = mImageViewHeight;
300
301 int bitmapWidth = mBitmap.getWidth();
302 int bitmapHeight = mBitmap.getHeight();
303
304 float scale = Math.min(
305 (float) bitmapWidth / measuredWidth,
306 (float) bitmapHeight / measuredHeight);
307
308 Bitmap scaledBitmap = Bitmap.createScaledBitmap(
309 mBitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale), 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
323 protected void onPostExecute(Bitmap newBitmap) {
324 mBitmapReceiver.onBitmapDecoded(newBitmap);
325 }
326 }
327}