| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.wallpaper.asset; |
| |
| import android.app.Activity; |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.Config; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.ColorDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.graphics.drawable.TransitionDrawable; |
| import android.os.AsyncTask; |
| import android.view.Display; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| import androidx.annotation.Nullable; |
| |
| import com.android.wallpaper.module.BitmapCropper; |
| import com.android.wallpaper.module.InjectorProvider; |
| import com.android.wallpaper.util.ScreenSizeCalculator; |
| import com.android.wallpaper.util.WallpaperCropUtils; |
| |
| import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; |
| |
| /** |
| * Interface representing an image asset. |
| */ |
| public abstract class Asset { |
| |
| /** |
| * Creates and returns a placeholder Drawable instance sized exactly to the target ImageView and |
| * filled completely with pixels of the provided placeholder color. |
| */ |
| protected static Drawable getPlaceholderDrawable( |
| Context context, ImageView imageView, int placeholderColor) { |
| Point imageViewDimensions = getViewDimensions(imageView); |
| Bitmap placeholderBitmap = |
| Bitmap.createBitmap(imageViewDimensions.x, imageViewDimensions.y, Config.ARGB_8888); |
| placeholderBitmap.eraseColor(placeholderColor); |
| return new BitmapDrawable(context.getResources(), placeholderBitmap); |
| } |
| |
| /** |
| * Returns the visible height and width in pixels of the provided ImageView, or if it hasn't |
| * been laid out yet, then gets the absolute value of the layout params. |
| */ |
| private static Point getViewDimensions(View view) { |
| int width = view.getWidth() > 0 ? view.getWidth() : Math.abs(view.getLayoutParams().width); |
| int height = view.getHeight() > 0 ? view.getHeight() |
| : Math.abs(view.getLayoutParams().height); |
| |
| return new Point(width, height); |
| } |
| |
| /** |
| * Decodes a bitmap sized for the destination view's dimensions off the main UI thread. |
| * |
| * @param targetWidth Width of target view in physical pixels. |
| * @param targetHeight Height of target view in physical pixels. |
| * @param receiver Called with the decoded bitmap or null if there was an error decoding the |
| * bitmap. |
| */ |
| public abstract void decodeBitmap(int targetWidth, int targetHeight, BitmapReceiver receiver); |
| |
| /** |
| * Decodes and downscales a bitmap region off the main UI thread. |
| * |
| * @param rect Rect representing the crop region in terms of the original image's |
| * resolution. |
| * @param targetWidth Width of target view in physical pixels. |
| * @param targetHeight Height of target view in physical pixels. |
| * @param receiver Called with the decoded bitmap region or null if there was an error |
| * decoding the bitmap region. |
| */ |
| public abstract void decodeBitmapRegion(Rect rect, int targetWidth, int targetHeight, |
| BitmapReceiver receiver); |
| |
| /** |
| * Calculates the raw dimensions of the asset at its original resolution off the main UI thread. |
| * Avoids decoding the entire bitmap if possible to conserve memory. |
| * |
| * @param activity Activity in which this decoding request is made. Allows for early termination |
| * of fetching image data and/or decoding to a bitmap. May be null, in which |
| * case the request is made in the application context instead. |
| * @param receiver Called with the decoded raw dimensions of the whole image or null if there |
| * was an error decoding the dimensions. |
| */ |
| public abstract void decodeRawDimensions(@Nullable Activity activity, |
| DimensionsReceiver receiver); |
| |
| /** |
| * Returns whether this asset has access to a separate, lower fidelity source of image data |
| * (that may be able to be loaded more quickly to simulate progressive loading). |
| */ |
| public boolean hasLowResDataSource() { |
| return false; |
| } |
| |
| /** |
| * Loads the asset from the separate low resolution data source (if there is one) into the |
| * provided ImageView with the placeholder color and bitmap transformation. |
| * |
| * @param transformation Bitmap transformation that can transform the thumbnail image |
| * post-decoding. |
| */ |
| public void loadLowResDrawable(Activity activity, ImageView imageView, int placeholderColor, |
| BitmapTransformation transformation) { |
| // No op |
| } |
| |
| /** |
| * Returns whether the asset supports rendering tile regions at varying pixel densities. |
| */ |
| public abstract boolean supportsTiling(); |
| |
| /** |
| * Loads a Drawable for this asset into the provided ImageView. While waiting for the image to |
| * load, first loads a ColorDrawable based on the provided placeholder color. |
| * |
| * @param context Activity hosting the ImageView. |
| * @param imageView ImageView which is the target view of this asset. |
| * @param placeholderColor Color of placeholder set to ImageView while waiting for image to |
| * load. |
| */ |
| public void loadDrawable(final Context context, final ImageView imageView, |
| int placeholderColor) { |
| // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in |
| // question is empty. |
| final boolean needsTransition = imageView.getDrawable() == null; |
| final Drawable placeholderDrawable = new ColorDrawable(placeholderColor); |
| if (needsTransition) { |
| imageView.setImageDrawable(placeholderDrawable); |
| } |
| |
| // Set requested height and width to the either the actual height and width of the view in |
| // pixels, or if it hasn't been laid out yet, then to the absolute value of the layout |
| // params. |
| int width = imageView.getWidth() > 0 |
| ? imageView.getWidth() |
| : Math.abs(imageView.getLayoutParams().width); |
| int height = imageView.getHeight() > 0 |
| ? imageView.getHeight() |
| : Math.abs(imageView.getLayoutParams().height); |
| |
| decodeBitmap(width, height, new BitmapReceiver() { |
| @Override |
| public void onBitmapDecoded(Bitmap bitmap) { |
| if (!needsTransition) { |
| imageView.setImageBitmap(bitmap); |
| return; |
| } |
| |
| Resources resources = context.getResources(); |
| |
| Drawable[] layers = new Drawable[2]; |
| layers[0] = placeholderDrawable; |
| layers[1] = new BitmapDrawable(resources, bitmap); |
| |
| TransitionDrawable transitionDrawable = new TransitionDrawable(layers); |
| transitionDrawable.setCrossFadeEnabled(true); |
| |
| imageView.setImageDrawable(transitionDrawable); |
| transitionDrawable.startTransition(resources.getInteger( |
| android.R.integer.config_shortAnimTime)); |
| } |
| }); |
| } |
| |
| /** |
| * Loads a Drawable for this asset into the provided ImageView, providing a crossfade transition |
| * with the given duration from the Drawable previously set on the ImageView. |
| * |
| * @param context Activity hosting the ImageView. |
| * @param imageView ImageView which is the target view of this asset. |
| * @param transitionDurationMillis Duration of the crossfade, in milliseconds. |
| * @param drawableLoadedListener Listener called once the transition has begun. |
| * @param placeholderColor Color of the placeholder if the provided ImageView is empty |
| * before the |
| */ |
| public void loadDrawableWithTransition( |
| final Context context, |
| final ImageView imageView, |
| final int transitionDurationMillis, |
| @Nullable final DrawableLoadedListener drawableLoadedListener, |
| int placeholderColor) { |
| Point imageViewDimensions = getViewDimensions(imageView); |
| |
| // Transition from a placeholder ColorDrawable to the decoded bitmap when the ImageView in |
| // question is empty. |
| boolean needsPlaceholder = imageView.getDrawable() == null; |
| if (needsPlaceholder) { |
| imageView.setImageDrawable( |
| getPlaceholderDrawable(context, imageView, placeholderColor)); |
| } |
| |
| decodeBitmap(imageViewDimensions.x, imageViewDimensions.y, new BitmapReceiver() { |
| @Override |
| public void onBitmapDecoded(Bitmap bitmap) { |
| final Resources resources = context.getResources(); |
| |
| new CenterCropBitmapTask(bitmap, imageView, new BitmapReceiver() { |
| @Override |
| public void onBitmapDecoded(@Nullable Bitmap newBitmap) { |
| Drawable[] layers = new Drawable[2]; |
| Drawable existingDrawable = imageView.getDrawable(); |
| |
| if (existingDrawable instanceof TransitionDrawable) { |
| // Take only the second layer in the existing TransitionDrawable so |
| // we don't keep |
| // around a reference to older layers which are no longer shown (this |
| // way we avoid a |
| // memory leak). |
| TransitionDrawable existingTransitionDrawable = |
| (TransitionDrawable) existingDrawable; |
| int id = existingTransitionDrawable.getId(1); |
| layers[0] = existingTransitionDrawable.findDrawableByLayerId(id); |
| } else { |
| layers[0] = existingDrawable; |
| } |
| layers[1] = new BitmapDrawable(resources, newBitmap); |
| |
| TransitionDrawable transitionDrawable = new TransitionDrawable(layers); |
| transitionDrawable.setCrossFadeEnabled(true); |
| |
| imageView.setImageDrawable(transitionDrawable); |
| transitionDrawable.startTransition(transitionDurationMillis); |
| |
| if (drawableLoadedListener != null) { |
| drawableLoadedListener.onDrawableLoaded(); |
| } |
| } |
| }).execute(); |
| } |
| }); |
| } |
| |
| /** |
| * Loads the image for this asset into the provided ImageView which is used for the preview. |
| * While waiting for the image to load, first loads a ColorDrawable based on the provided |
| * placeholder color. |
| * |
| * @param activity Activity hosting the ImageView. |
| * @param imageView ImageView which is the target view of this asset. |
| * @param placeholderColor Color of placeholder set to ImageView while waiting for image to |
| * load. |
| */ |
| public void loadPreviewImage(Activity activity, ImageView imageView, int placeholderColor) { |
| boolean needsTransition = imageView.getDrawable() == null; |
| Drawable placeholderDrawable = new ColorDrawable(placeholderColor); |
| if (needsTransition) { |
| imageView.setImageDrawable(placeholderDrawable); |
| } |
| |
| decodeRawDimensions(activity, dimensions -> { |
| if (dimensions == null) { |
| loadDrawable(activity, imageView, placeholderColor); |
| return; |
| } |
| |
| Display defaultDisplay = activity.getWindowManager().getDefaultDisplay(); |
| Point screenSize = ScreenSizeCalculator.getInstance().getScreenSize(defaultDisplay); |
| Rect visibleRawWallpaperRect = |
| WallpaperCropUtils.calculateVisibleRect(dimensions, screenSize); |
| adjustCropRect(activity, dimensions, visibleRawWallpaperRect); |
| |
| BitmapCropper bitmapCropper = InjectorProvider.getInjector().getBitmapCropper(); |
| bitmapCropper.cropAndScaleBitmap(this, /* scale= */ 1f, visibleRawWallpaperRect, |
| new BitmapCropper.Callback() { |
| @Override |
| public void onBitmapCropped(Bitmap croppedBitmap) { |
| // Since the size of the cropped bitmap may not exactly the same with |
| // image view(maybe has 1px or 2px difference), |
| // so set CENTER_CROP to let the bitmap to fit the image view. |
| imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); |
| if (!needsTransition) { |
| imageView.setImageBitmap(croppedBitmap); |
| return; |
| } |
| |
| Resources resources = activity.getResources(); |
| |
| Drawable[] layers = new Drawable[2]; |
| layers[0] = placeholderDrawable; |
| layers[1] = new BitmapDrawable(resources, croppedBitmap); |
| |
| TransitionDrawable transitionDrawable = new TransitionDrawable(layers); |
| transitionDrawable.setCrossFadeEnabled(true); |
| |
| imageView.setImageDrawable(transitionDrawable); |
| transitionDrawable.startTransition(resources.getInteger( |
| android.R.integer.config_shortAnimTime)); |
| } |
| |
| @Override |
| public void onError(@Nullable Throwable e) { |
| |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * Interface for receiving decoded Bitmaps. |
| */ |
| public interface BitmapReceiver { |
| |
| /** |
| * Called with a decoded Bitmap object or null if there was an error decoding the bitmap. |
| */ |
| void onBitmapDecoded(@Nullable Bitmap bitmap); |
| } |
| |
| /** |
| * Interface for receiving raw asset dimensions. |
| */ |
| public interface DimensionsReceiver { |
| |
| /** |
| * Called with raw dimensions of asset or null if the asset is unable to decode the raw |
| * dimensions. |
| * |
| * @param dimensions Dimensions as a Point where width is represented by "x" and height by |
| * "y". |
| */ |
| void onDimensionsDecoded(@Nullable Point dimensions); |
| } |
| |
| /** |
| * Interface for being notified when a drawable has been loaded. |
| */ |
| public interface DrawableLoadedListener { |
| void onDrawableLoaded(); |
| } |
| |
| protected void adjustCropRect(Context context, Point assetDimensions, Rect cropRect) { |
| WallpaperCropUtils.adjustCropRect(context, cropRect, true /* zoomIn */); |
| } |
| |
| /** |
| * Custom AsyncTask which returns a copy of the given bitmap which is center cropped and scaled |
| * to fit in the given ImageView. |
| */ |
| public static class CenterCropBitmapTask extends AsyncTask<Void, Void, Bitmap> { |
| |
| private Bitmap mBitmap; |
| private BitmapReceiver mBitmapReceiver; |
| |
| private int mImageViewWidth; |
| private int mImageViewHeight; |
| |
| public CenterCropBitmapTask(Bitmap bitmap, View view, |
| BitmapReceiver bitmapReceiver) { |
| mBitmap = bitmap; |
| mBitmapReceiver = bitmapReceiver; |
| |
| Point imageViewDimensions = getViewDimensions(view); |
| |
| mImageViewWidth = imageViewDimensions.x; |
| mImageViewHeight = imageViewDimensions.y; |
| } |
| |
| @Override |
| protected Bitmap doInBackground(Void... unused) { |
| int measuredWidth = mImageViewWidth; |
| int measuredHeight = mImageViewHeight; |
| |
| int bitmapWidth = mBitmap.getWidth(); |
| int bitmapHeight = mBitmap.getHeight(); |
| |
| float scale = Math.min( |
| (float) bitmapWidth / measuredWidth, |
| (float) bitmapHeight / measuredHeight); |
| |
| Bitmap scaledBitmap = Bitmap.createScaledBitmap( |
| mBitmap, Math.round(bitmapWidth / scale), Math.round(bitmapHeight / scale), |
| true); |
| |
| int horizontalGutterPx = Math.max(0, (scaledBitmap.getWidth() - measuredWidth) / 2); |
| int verticalGutterPx = Math.max(0, (scaledBitmap.getHeight() - measuredHeight) / 2); |
| |
| return Bitmap.createBitmap( |
| scaledBitmap, |
| horizontalGutterPx, |
| verticalGutterPx, |
| scaledBitmap.getWidth() - (2 * horizontalGutterPx), |
| scaledBitmap.getHeight() - (2 * verticalGutterPx)); |
| } |
| |
| @Override |
| protected void onPostExecute(Bitmap newBitmap) { |
| mBitmapReceiver.onBitmapDecoded(newBitmap); |
| } |
| } |
| } |