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.Handler; 30 import android.os.Looper; 31 import android.view.Display; 32 import android.view.View; 33 import android.widget.ImageView; 34 35 import androidx.annotation.Nullable; 36 import androidx.annotation.WorkerThread; 37 38 import com.android.wallpaper.module.BitmapCropper; 39 import com.android.wallpaper.module.InjectorProvider; 40 import com.android.wallpaper.util.ScreenSizeCalculator; 41 import com.android.wallpaper.util.WallpaperCropUtils; 42 43 import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; 44 45 import java.util.concurrent.ExecutorService; 46 import java.util.concurrent.Executors; 47 48 /** 49 * Interface representing an image asset. 50 */ 51 public abstract class Asset { 52 private static final ExecutorService sExecutorService = Executors.newSingleThreadExecutor(); 53 /** 54 * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and 55 * filled completely with pixels of the provided placeholder color. 56 */ getPlaceholderDrawable( Context context, ImageView imageView, int placeholderColor)57 protected static Drawable getPlaceholderDrawable( 58 Context context, ImageView imageView, int placeholderColor) { 59 Point imageViewDimensions = getViewDimensions(imageView); 60 Bitmap placeholderBitmap = 61 Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888); 62 placeholderBitmap.eraseColor(placeholderColor); 63 return new BitmapDrawable(context.getResources(), placeholderBitmap); 64 } 65 66 /** 67 * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't 68 * been laid out yet, then gets the absolute value of the layout params. 69 */ getViewDimensions(View view)70 private static Point getViewDimensions(View view) { 71 int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width); 72 int height = view.getHeight() > 0 ? view.getHeight() 73 : Math.abs(view.getLayoutParams().height); 74 75 return new Point(width, height); 76 } 77 78 /** 79 * Decodes a bitmap sized for the destination view's dimensions off the main UI thread. 80 * 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 or null if there was an error decoding the 84 * bitmap. 85 */ decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver)86 public abstract void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver); 87 88 /** 89 * For {@link #decodeBitmap(int, int, BitmapReceiver)} to use when it is done. It then call 90 * the receiver with decoded bitmap in the main thread. 91 * 92 * @param receiver The receiver to handle decoded bitmap or null if decoding failed. 93 * @param decodedBitmap The bitmap which is already decoded. 94 */ decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap)95 protected void decodeBitmapCompleted(BitmapReceiver receiver, Bitmap decodedBitmap) { 96 new Handler(Looper.getMainLooper()).post(() -> receiver.onBitmapDecoded(decodedBitmap)); 97 } 98 99 /** 100 * Decodes and downscales a bitmap region off the main UI thread. 101 * @param rect Rect representing the crop region in terms of the original image's 102 * resolution. 103 * @param targetWidth Width of target view in physical pixels. 104 * @param targetHeight Height of target view in physical pixels. 105 * @param shouldAdjustForRtl whether the region selected should be adjusted for RTL (that is, 106 * the crop region will be considered starting from the right) 107 * @param receiver Called with the decoded bitmap region or null if there was an error 108 */ decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, boolean shouldAdjustForRtl, BitmapReceiver receiver)109 public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, 110 boolean shouldAdjustForRtl, BitmapReceiver receiver); 111 112 /** 113 * Calculates the raw dimensions of the asset at its original resolution off the main UI thread. 114 * Avoids decoding the entire bitmap if possible to conserve memory. 115 * 116 * @param activity Activity in which this decoding request is made. Allows for early termination 117 * of fetching image data and/or decoding to a bitmap. May be null, in which 118 * case the request is made in the application context instead. 119 * @param receiver Called with the decoded raw dimensions of the whole image or null if there 120 * was an error decoding the dimensions. 121 */ decodeRawDimensions(@ullable Activity activity, DimensionsReceiver receiver)122 public abstract void decodeRawDimensions(@Nullable Activity activity, 123 DimensionsReceiver receiver); 124 125 /** 126 * Returns whether this asset has access to a separate, lower fidelity source of image data 127 * (that may be able to be loaded more quickly to simulate progressive loading). 128 */ hasLowResDataSource()129 public boolean hasLowResDataSource() { 130 return false; 131 } 132 133 /** 134 * Loads the asset from the separate low resolution data source (if there is one) into the 135 * provided ImageView with the placeholder color and bitmap transformation. 136 * 137 * @param transformation Bitmap transformation that can transform the thumbnail image 138 * post-decoding. 139 */ loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, BitmapTransformation transformation)140 public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, 141 BitmapTransformation transformation) { 142 // No op 143 } 144 145 /** 146 * Returns a Bitmap from the separate low resolution data source (if there is one) or 147 * {@code null} otherwise. 148 * This could be an I/O operation so DO NOT CALL ON UI THREAD 149 */ 150 @WorkerThread 151 @Nullable getLowResBitmap(Context context)152 public Bitmap getLowResBitmap(Context context) { 153 return null; 154 } 155 156 /** 157 * Returns whether the asset supports rendering tile regions at varying pixel densities. 158 */ supportsTiling()159 public abstract boolean supportsTiling(); 160 161 /** 162 * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to 163 * load, first loads a ColorDrawable based on the provided placeholder color. 164 * 165 * @param context Activity hosting the ImageView. 166 * @param imageView ImageView which is the target view of this asset. 167 * @param placeholderColor Color of placeholder set to ImageView while waiting for image to 168 * load. 169 */ loadDrawable(final Context context, final ImageView imageView, int placeholderColor)170 public void loadDrawable(final Context context, final ImageView imageView, 171 int placeholderColor) { 172 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in 173 // question is empty. 174 final boolean needsTransition = imageView.getDrawable() == null; 175 final Drawable placeholderDrawable = new ColorDrawable(placeholderColor); 176 if (needsTransition) { 177 imageView.setImageDrawable(placeholderDrawable); 178 } 179 180 // Set requested height and width to the either the actual height and width of the view in 181 // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout 182 // params. 183 int width = imageView.getWidth() > 0 184 ? imageView.getWidth() 185 : Math.abs(imageView.getLayoutParams().width); 186 int height = imageView.getHeight() > 0 187 ? imageView.getHeight() 188 : Math.abs(imageView.getLayoutParams().height); 189 190 decodeBitmap(width, height, new BitmapReceiver() { 191 @Override 192 public void onBitmapDecoded(Bitmap bitmap) { 193 if (!needsTransition) { 194 imageView.setImageBitmap(bitmap); 195 return; 196 } 197 198 Resources resources = context.getResources(); 199 200 Drawable[] layers = new Drawable[2]; 201 layers[0] = placeholderDrawable; 202 layers[1] = new BitmapDrawable(resources, bitmap); 203 204 TransitionDrawable transitionDrawable = new TransitionDrawable(layers); 205 transitionDrawable.setCrossFadeEnabled(true); 206 207 imageView.setImageDrawable(transitionDrawable); 208 transitionDrawable.startTransition(resources.getInteger( 209 android.R.integer.config_shortAnimTime)); 210 } 211 }); 212 } 213 214 /** 215 * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition 216 * with the given duration from the Drawable previously set on the ImageView. 217 * 218 * @param context Activity hosting the ImageView. 219 * @param imageView ImageView which is the target view of this asset. 220 * @param transitionDurationMillis Duration of the crossfade, in milliseconds. 221 * @param drawableLoadedListener Listener called once the transition has begun. 222 * @param placeholderColor Color of the placeholder if the provided ImageView is empty 223 * before the 224 */ loadDrawableWithTransition( final Context context, final ImageView imageView, final int transitionDurationMillis, @Nullable final DrawableLoadedListener drawableLoadedListener, int placeholderColor)225 public void loadDrawableWithTransition( 226 final Context context, 227 final ImageView imageView, 228 final int transitionDurationMillis, 229 @Nullable final DrawableLoadedListener drawableLoadedListener, 230 int placeholderColor) { 231 Point imageViewDimensions = getViewDimensions(imageView); 232 233 // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in 234 // question is empty. 235 boolean needsPlaceholder = imageView.getDrawable() == null; 236 if (needsPlaceholder) { 237 imageView.setImageDrawable( 238 getPlaceholderDrawable(context, imageView, placeholderColor)); 239 } 240 241 decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() { 242 @Override 243 public void onBitmapDecoded(Bitmap bitmap) { 244 final Resources resources = context.getResources(); 245 246 centerCropBitmap(bitmap, imageView, new BitmapReceiver() { 247 @Override 248 public void onBitmapDecoded(@Nullable Bitmap newBitmap) { 249 Drawable[] layers = new Drawable[2]; 250 Drawable existingDrawable = imageView.getDrawable(); 251 252 if (existingDrawable instanceof TransitionDrawable) { 253 // Take only the second layer in the existing TransitionDrawable so 254 // we don't keep 255 // around a reference to older layers which are no longer shown (this 256 // way we avoid a 257 // memory leak). 258 TransitionDrawable existingTransitionDrawable = 259 (TransitionDrawable) existingDrawable; 260 int id = existingTransitionDrawable.getId(1); 261 layers[0] = existingTransitionDrawable.findDrawableByLayerId(id); 262 } else { 263 layers[0] = existingDrawable; 264 } 265 layers[1] = new BitmapDrawable(resources, newBitmap); 266 267 TransitionDrawable transitionDrawable = new TransitionDrawable(layers); 268 transitionDrawable.setCrossFadeEnabled(true); 269 270 imageView.setImageDrawable(transitionDrawable); 271 transitionDrawable.startTransition(transitionDurationMillis); 272 273 if (drawableLoadedListener != null) { 274 drawableLoadedListener.onDrawableLoaded(); 275 } 276 } 277 }); 278 } 279 }); 280 } 281 282 /** 283 * Loads the image for this asset into the provided ImageView which is used for the preview. 284 * While waiting for the image to load, first loads a ColorDrawable based on the provided 285 * placeholder color. 286 * 287 * @param activity Activity hosting the ImageView. 288 * @param imageView ImageView which is the target view of this asset. 289 * @param placeholderColor Color of placeholder set to ImageView while waiting for image to 290 * load. 291 * @param offsetToStart true to let the preview show from the start of the image, false to 292 * center-aligned to the image. 293 */ loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, boolean offsetToStart)294 public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor, 295 boolean offsetToStart) { 296 boolean needsTransition = imageView.getDrawable() == null; 297 Drawable placeholderDrawable = new ColorDrawable(placeholderColor); 298 if (needsTransition) { 299 imageView.setImageDrawable(placeholderDrawable); 300 } 301 302 decodeRawDimensions(activity, dimensions -> { 303 if (dimensions == null) { 304 loadDrawable(activity, imageView, placeholderColor); 305 return; 306 } 307 308 Display defaultDisplay = activity.getWindowManager().getDefaultDisplay(); 309 Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay); 310 Rect visibleRawWallpaperRect = 311 WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize); 312 313 // TODO(b/264234793): Make offsetToStart general support or for the specific asset. 314 adjustCropRect(activity, dimensions, visibleRawWallpaperRect, offsetToStart); 315 316 BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper(); 317 bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect, 318 WallpaperCropUtils.isRtl(activity), 319 new BitmapCropper.Callback() { 320 @Override 321 public void onBitmapCropped(Bitmap croppedBitmap) { 322 // Since the size of the cropped bitmap may not exactly the same with 323 // image view(maybe has 1px or 2px difference), 324 // so set CENTER_CROP to let the bitmap to fit the image view. 325 imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); 326 if (!needsTransition) { 327 imageView.setImageBitmap(croppedBitmap); 328 return; 329 } 330 331 Resources resources = activity.getResources(); 332 333 Drawable[] layers = new Drawable[2]; 334 layers[0] = placeholderDrawable; 335 layers[1] = new BitmapDrawable(resources, croppedBitmap); 336 337 TransitionDrawable transitionDrawable = new TransitionDrawable(layers); 338 transitionDrawable.setCrossFadeEnabled(true); 339 340 imageView.setImageDrawable(transitionDrawable); 341 transitionDrawable.startTransition(resources.getInteger( 342 android.R.integer.config_shortAnimTime)); 343 } 344 345 @Override 346 public void onError(@Nullable Throwable e) { 347 348 } 349 }); 350 }); 351 } 352 353 /** 354 * Interface for receiving decoded Bitmaps. 355 */ 356 public interface BitmapReceiver { 357 358 /** 359 * Called with a decoded Bitmap object or null if there was an error decoding the bitmap. 360 */ onBitmapDecoded(@ullable Bitmap bitmap)361 void onBitmapDecoded(@Nullable Bitmap bitmap); 362 } 363 364 /** 365 * Interface for receiving raw asset dimensions. 366 */ 367 public interface DimensionsReceiver { 368 369 /** 370 * Called with raw dimensions of asset or null if the asset is unable to decode the raw 371 * dimensions. 372 * 373 * @param dimensions Dimensions as a Point where width is represented by "x" and height by 374 * "y". 375 */ onDimensionsDecoded(@ullable Point dimensions)376 void onDimensionsDecoded(@Nullable Point dimensions); 377 } 378 379 /** 380 * Interface for being notified when a drawable has been loaded. 381 */ 382 public interface DrawableLoadedListener { onDrawableLoaded()383 void onDrawableLoaded(); 384 } 385 adjustCropRect(Context context, Point assetDimensions, Rect cropRect, boolean offsetToStart)386 protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect, 387 boolean offsetToStart) { 388 WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */); 389 } 390 391 /** 392 * Returns a copy of the given bitmap which is center cropped and scaled 393 * to fit in the given ImageView and the thread runs on ExecutorService. 394 */ centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver)395 public void centerCropBitmap(Bitmap bitmap, View view, BitmapReceiver bitmapReceiver) { 396 Point imageViewDimensions = getViewDimensions(view); 397 sExecutorService.execute(() -> { 398 int measuredWidth = imageViewDimensions.x; 399 int measuredHeight = imageViewDimensions.y; 400 401 int bitmapWidth = bitmap.getWidth(); 402 int bitmapHeight = bitmap.getHeight(); 403 404 float scale = Math.min( 405 (float) bitmapWidth / measuredWidth, 406 (float) bitmapHeight / measuredHeight); 407 408 Bitmap scaledBitmap = Bitmap.createScaledBitmap( 409 bitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale), 410 true); 411 412 int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2); 413 int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2); 414 Bitmap result = Bitmap.createBitmap( 415 scaledBitmap, 416 horizontalGutterPx, 417 verticalGutterPx, 418 scaledBitmap.getWidth() - (2 * horizontalGutterPx), 419 scaledBitmap.getHeight() - (2 * verticalGutterPx)); 420 decodeBitmapCompleted(bitmapReceiver, result); 421 }); 422 } 423 } 424