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 been 57 * 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 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 */ decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, BitmapReceiver receiver)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 */ decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)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 */ hasLowResDataSource()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 */ loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)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 */ supportsTiling()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 * @param context Activity hosting the ImageView. 131 * @param imageView ImageView which is the target view of this asset. 132 * @param placeholderColor Color of placeholder set to ImageView while waiting for image to load. 133 */ loadDrawable(final Context context, final ImageView imageView, int placeholderColor)134 public void loadDrawable(final Context context, final ImageView imageView, 135 int placeholderColor) { 136 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in 137 // question is empty. 138 final boolean needsTransition = imageView.getDrawable() == null; 139 final Drawable placeholderDrawable = new ColorDrawable(placeholderColor); 140 if (needsTransition) { 141 imageView.setImageDrawable(placeholderDrawable); 142 } 143 144 // Set requested height and width to the either the actual height and width of the view in 145 // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout params. 146 int width = imageView.getWidth() > 0 147 ? imageView.getWidth() 148 : Math.abs(imageView.getLayoutParams().width); 149 int height = imageView.getHeight() > 0 150 ? imageView.getHeight() 151 : Math.abs(imageView.getLayoutParams().height); 152 153 decodeBitmap(width, height, new BitmapReceiver() { 154 @Override 155 public void onBitmapDecoded(Bitmap bitmap) { 156 if (!needsTransition) { 157 imageView.setImageBitmap(bitmap); 158 return; 159 } 160 161 Resources resources = context.getResources(); 162 163 Drawable[] layers = new Drawable[2]; 164 layers[0] = placeholderDrawable; 165 layers[1] = new BitmapDrawable(resources, bitmap); 166 167 TransitionDrawable transitionDrawable = new TransitionDrawable(layers); 168 transitionDrawable.setCrossFadeEnabled(true); 169 170 imageView.setImageDrawable(transitionDrawable); 171 transitionDrawable.startTransition(resources.getInteger( 172 android.R.integer.config_shortAnimTime)); 173 } 174 }); 175 } 176 177 /** 178 * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition 179 * with the given duration from the Drawable previously set on the ImageView. 180 * @param context Activity hosting the ImageView. 181 * @param imageView ImageView which is the target view of this asset. 182 * @param transitionDurationMillis Duration of the crossfade, in milliseconds. 183 * @param drawableLoadedListener Listener called once the transition has begun. 184 * @param placeholderColor Color of the placeholder if the provided ImageView is empty before the 185 */ loadDrawableWithTransition( final Context context, final ImageView imageView, final int transitionDurationMillis, @Nullable final DrawableLoadedListener drawableLoadedListener, int placeholderColor)186 public void loadDrawableWithTransition( 187 final Context context, 188 final ImageView imageView, 189 final int transitionDurationMillis, 190 @Nullable final DrawableLoadedListener drawableLoadedListener, 191 int placeholderColor) { 192 Point imageViewDimensions = getViewDimensions(imageView); 193 194 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in 195 // question is empty. 196 boolean needsPlaceholder = imageView.getDrawable() == null; 197 if (needsPlaceholder) { 198 imageView.setImageDrawable(getPlaceholderDrawable(context, imageView, placeholderColor)); 199 } 200 201 decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() { 202 @Override 203 public void onBitmapDecoded(Bitmap bitmap) { 204 final Resources resources = context.getResources(); 205 206 new CenterCropBitmapTask(bitmap, imageView, new BitmapReceiver() { 207 @Override 208 public void onBitmapDecoded(@Nullable Bitmap newBitmap) { 209 Drawable[] layers = new Drawable[2]; 210 Drawable existingDrawable = imageView.getDrawable(); 211 212 if (existingDrawable instanceof TransitionDrawable) { 213 // Take only the second layer in the existing TransitionDrawable so we don't keep 214 // around a reference to older layers which are no longer shown (this way we avoid a 215 // memory leak). 216 TransitionDrawable existingTransitionDrawable = 217 (TransitionDrawable) existingDrawable; 218 int id = existingTransitionDrawable.getId(1); 219 layers[0] = existingTransitionDrawable.findDrawableByLayerId(id); 220 } else { 221 layers[0] = existingDrawable; 222 } 223 layers[1] = new BitmapDrawable(resources, newBitmap); 224 225 TransitionDrawable transitionDrawable = new TransitionDrawable(layers); 226 transitionDrawable.setCrossFadeEnabled(true); 227 228 imageView.setImageDrawable(transitionDrawable); 229 transitionDrawable.startTransition(transitionDurationMillis); 230 231 if (drawableLoadedListener != null) { 232 drawableLoadedListener.onDrawableLoaded(); 233 } 234 } 235 }).execute(); 236 } 237 }); 238 } 239 240 /** 241 * Interface for receiving decoded Bitmaps. 242 */ 243 public interface BitmapReceiver { 244 245 /** 246 * Called with a decoded Bitmap object or null if there was an error decoding the bitmap. 247 */ onBitmapDecoded(@ullable Bitmap bitmap)248 void onBitmapDecoded(@Nullable Bitmap bitmap); 249 } 250 251 /** 252 * Interface for receiving raw asset dimensions. 253 */ 254 public interface DimensionsReceiver { 255 256 /** 257 * Called with raw dimensions of asset or null if the asset is unable to decode the raw 258 * dimensions. 259 * 260 * @param dimensions Dimensions as a Point where width is represented by "x" and height by "y". 261 */ onDimensionsDecoded(@ullable Point dimensions)262 void onDimensionsDecoded(@Nullable Point dimensions); 263 } 264 265 /** 266 * Interface for being notified when a drawable has been loaded. 267 */ 268 public interface DrawableLoadedListener { onDrawableLoaded()269 void onDrawableLoaded(); 270 } 271 272 /** 273 * Custom AsyncTask which returns a copy of the given bitmap which is center cropped and scaled to 274 * fit in the given ImageView. 275 */ 276 public static class CenterCropBitmapTask extends AsyncTask<Void, Void, Bitmap> { 277 278 private Bitmap mBitmap; 279 private BitmapReceiver mBitmapReceiver; 280 281 private int mImageViewWidth; 282 private int mImageViewHeight; 283 CenterCropBitmapTask(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver)284 public CenterCropBitmapTask(Bitmap bitmap, View view, 285 BitmapReceiver bitmapReceiver) { 286 mBitmap = bitmap; 287 mBitmapReceiver = bitmapReceiver; 288 289 Point imageViewDimensions = getViewDimensions(view); 290 291 mImageViewWidth = imageViewDimensions.x; 292 mImageViewHeight = imageViewDimensions.y; 293 } 294 295 @Override doInBackground(Void... unused)296 protected Bitmap doInBackground(Void... unused) { 297 int measuredWidth = mImageViewWidth; 298 int measuredHeight = mImageViewHeight; 299 300 int bitmapWidth = mBitmap.getWidth(); 301 int bitmapHeight = mBitmap.getHeight(); 302 303 float scale = Math.min( 304 (float) bitmapWidth / measuredWidth, 305 (float) bitmapHeight / measuredHeight); 306 307 Bitmap scaledBitmap = Bitmap.createScaledBitmap( 308 mBitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale), 309 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 onPostExecute(Bitmap newBitmap)323 protected void onPostExecute(Bitmap newBitmap) { 324 mBitmapReceiver.onBitmapDecoded(newBitmap); 325 } 326 } 327 } 328