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