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