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