diff options
Diffstat (limited to 'src/com/android/messaging/ui/AsyncImageView.java')
-rw-r--r-- | src/com/android/messaging/ui/AsyncImageView.java | 457 |
1 files changed, 0 insertions, 457 deletions
diff --git a/src/com/android/messaging/ui/AsyncImageView.java b/src/com/android/messaging/ui/AsyncImageView.java deleted file mode 100644 index 9aaf0b1..0000000 --- a/src/com/android/messaging/ui/AsyncImageView.java +++ /dev/null @@ -1,457 +0,0 @@ -/* - * Copyright (C) 2015 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.messaging.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Path; -import android.graphics.RectF; -import android.graphics.drawable.ColorDrawable; -import android.graphics.drawable.Drawable; -import android.support.annotation.Nullable; -import android.support.rastermill.FrameSequenceDrawable; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.widget.ImageView; - -import com.android.messaging.R; -import com.android.messaging.datamodel.binding.Binding; -import com.android.messaging.datamodel.binding.BindingBase; -import com.android.messaging.datamodel.media.BindableMediaRequest; -import com.android.messaging.datamodel.media.GifImageResource; -import com.android.messaging.datamodel.media.ImageRequest; -import com.android.messaging.datamodel.media.ImageRequestDescriptor; -import com.android.messaging.datamodel.media.ImageResource; -import com.android.messaging.datamodel.media.MediaRequest; -import com.android.messaging.datamodel.media.MediaResourceManager; -import com.android.messaging.datamodel.media.MediaResourceManager.MediaResourceLoadListener; -import com.android.messaging.util.Assert; -import com.android.messaging.util.LogUtil; -import com.android.messaging.util.ThreadUtil; -import com.android.messaging.util.UiUtils; -import com.google.common.annotations.VisibleForTesting; - -import java.util.HashSet; - -/** - * An ImageView used to asynchronously request an image from MediaResourceManager and render it. - */ -public class AsyncImageView extends ImageView implements MediaResourceLoadListener<ImageResource> { - private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG; - // 100ms delay before disposing the image in case the AsyncImageView is re-added to the UI - private static final int DISPOSE_IMAGE_DELAY = 100; - - // AsyncImageView has a 1-1 binding relationship with an ImageRequest instance that requests - // the image from the MediaResourceManager. Since the request is done asynchronously, we - // want to make sure the image view is always bound to the latest image request that it - // issues, so that when the image is loaded, the ImageRequest (which extends BindableData) - // will be able to figure out whether the binding is still valid and whether the loaded image - // should be delivered to the AsyncImageView via onMediaResourceLoaded() callback. - @VisibleForTesting - public final Binding<BindableMediaRequest<ImageResource>> mImageRequestBinding; - - /** True if we want the image to fade in when it loads */ - private boolean mFadeIn; - - /** True if we want the image to reveal (scale) when it loads. When set to true, this - * will take precedence over {@link #mFadeIn} */ - private final boolean mReveal; - - // The corner radius for drawing rounded corners around bitmap. The default value is zero - // (no rounded corners) - private final int mCornerRadius; - private final Path mRoundedCornerClipPath; - private int mClipPathWidth; - private int mClipPathHeight; - - // A placeholder drawable that takes the spot of the image when it's loading. The default - // setting is null (no placeholder). - private final Drawable mPlaceholderDrawable; - protected ImageResource mImageResource; - private final Runnable mDisposeRunnable = new Runnable() { - @Override - public void run() { - if (mImageRequestBinding.isBound()) { - mDetachedRequestDescriptor = (ImageRequestDescriptor) - mImageRequestBinding.getData().getDescriptor(); - } - unbindView(); - releaseImageResource(); - } - }; - - private AsyncImageViewDelayLoader mDelayLoader; - private ImageRequestDescriptor mDetachedRequestDescriptor; - - public AsyncImageView(final Context context, final AttributeSet attrs) { - super(context, attrs); - mImageRequestBinding = BindingBase.createBinding(this); - final TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.AsyncImageView, - 0, 0); - mFadeIn = attr.getBoolean(R.styleable.AsyncImageView_fadeIn, true); - mReveal = attr.getBoolean(R.styleable.AsyncImageView_reveal, false); - mPlaceholderDrawable = attr.getDrawable(R.styleable.AsyncImageView_placeholderDrawable); - mCornerRadius = attr.getDimensionPixelSize(R.styleable.AsyncImageView_cornerRadius, 0); - mRoundedCornerClipPath = new Path(); - - attr.recycle(); - } - - /** - * The main entrypoint for AsyncImageView to load image resource given an ImageRequestDescriptor - * @param descriptor the request descriptor, or null if no image should be displayed - */ - public void setImageResourceId(@Nullable final ImageRequestDescriptor descriptor) { - final String requestKey = (descriptor == null) ? null : descriptor.getKey(); - if (mImageRequestBinding.isBound()) { - if (TextUtils.equals(mImageRequestBinding.getData().getKey(), requestKey)) { - // Don't re-request the bitmap if the new request is for the same resource. - return; - } - unbindView(); - } - setImage(null); - resetTransientViewStates(); - if (!TextUtils.isEmpty(requestKey)) { - maybeSetupPlaceholderDrawable(descriptor); - final BindableMediaRequest<ImageResource> imageRequest = - descriptor.buildAsyncMediaRequest(getContext(), this); - requestImage(imageRequest); - } - } - - /** - * Sets a delay loader that centrally manages image request delay loading logic. - */ - public void setDelayLoader(final AsyncImageViewDelayLoader delayLoader) { - Assert.isTrue(mDelayLoader == null); - mDelayLoader = delayLoader; - } - - /** - * Called by the delay loader when we can resume image loading. - */ - public void resumeLoading() { - Assert.notNull(mDelayLoader); - Assert.isTrue(mImageRequestBinding.isBound()); - MediaResourceManager.get().requestMediaResourceAsync(mImageRequestBinding.getData()); - } - - /** - * Setup the placeholder drawable if: - * 1. There's an image to be loaded AND - * 2. We are given a placeholder drawable AND - * 3. The descriptor provided us with source width and height. - */ - private void maybeSetupPlaceholderDrawable(final ImageRequestDescriptor descriptor) { - if (!TextUtils.isEmpty(descriptor.getKey()) && mPlaceholderDrawable != null) { - if (descriptor.sourceWidth != ImageRequest.UNSPECIFIED_SIZE && - descriptor.sourceHeight != ImageRequest.UNSPECIFIED_SIZE) { - // Set a transparent inset drawable to the foreground so it will mimick the final - // size of the image, and use the background to show the actual placeholder - // drawable. - setImageDrawable(PlaceholderInsetDrawable.fromDrawable( - new ColorDrawable(Color.TRANSPARENT), - descriptor.sourceWidth, descriptor.sourceHeight)); - } - setBackground(mPlaceholderDrawable); - } - } - - protected void setImage(final ImageResource resource) { - setImage(resource, false /* isCached */); - } - - protected void setImage(final ImageResource resource, final boolean isCached) { - // Switch reference to the new ImageResource. Make sure we release the current - // resource and addRef() on the new resource so that the underlying bitmaps don't - // get leaked or get recycled by the bitmap cache. - releaseImageResource(); - // Ensure that any pending dispose runnables get removed. - ThreadUtil.getMainThreadHandler().removeCallbacks(mDisposeRunnable); - // The drawable may require work to get if its a static object so try to only make this call - // once. - final Drawable drawable = (resource != null) ? resource.getDrawable(getResources()) : null; - if (drawable != null) { - mImageResource = resource; - mImageResource.addRef(); - setImageDrawable(drawable); - if (drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).start(); - } - - if (getVisibility() == VISIBLE) { - if (mReveal) { - setVisibility(INVISIBLE); - UiUtils.revealOrHideViewWithAnimation(this, VISIBLE, null); - } else if (mFadeIn && !isCached) { - // Hide initially to avoid flash. - setAlpha(0F); - animate().alpha(1F).start(); - } - } - - if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) { - if (mImageResource instanceof GifImageResource) { - LogUtil.v(TAG, "setImage size unknown -- it's a GIF"); - } else { - LogUtil.v(TAG, "setImage size: " + mImageResource.getMediaSize() + - " width: " + mImageResource.getBitmap().getWidth() + - " heigh: " + mImageResource.getBitmap().getHeight()); - } - } - } - invalidate(); - } - - private void requestImage(final BindableMediaRequest<ImageResource> request) { - mImageRequestBinding.bind(request); - if (mDelayLoader == null || !mDelayLoader.isDelayLoadingImage()) { - MediaResourceManager.get().requestMediaResourceAsync(request); - } else { - mDelayLoader.registerView(this); - } - } - - @Override - public void onMediaResourceLoaded(final MediaRequest<ImageResource> request, - final ImageResource resource, final boolean isCached) { - if (mImageResource != resource) { - setImage(resource, isCached); - } - } - - @Override - public void onMediaResourceLoadError( - final MediaRequest<ImageResource> request, final Exception exception) { - // Media load failed, unbind and reset bitmap to default. - unbindView(); - setImage(null); - } - - private void releaseImageResource() { - final Drawable drawable = getDrawable(); - if (drawable instanceof FrameSequenceDrawable) { - ((FrameSequenceDrawable) drawable).stop(); - ((FrameSequenceDrawable) drawable).destroy(); - } - if (mImageResource != null) { - mImageResource.release(); - mImageResource = null; - } - setImageDrawable(null); - setBackground(null); - } - - /** - * Resets transient view states (eg. alpha, animations) before rebinding/reusing the view. - */ - private void resetTransientViewStates() { - clearAnimation(); - setAlpha(1F); - } - - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - // If it was recently removed, then cancel disposing, we're still using it. - ThreadUtil.getMainThreadHandler().removeCallbacks(mDisposeRunnable); - - // When the image view gets detached and immediately re-attached, any fade-in animation - // will be terminated, leaving the view in a semi-transparent state. Make sure we restore - // alpha when the view is re-attached. - if (mFadeIn) { - setAlpha(1F); - } - - // Check whether we are in a simple reuse scenario: detached from window, and reattached - // later without rebinding. This may be done by containers such as the RecyclerView to - // reuse the views. In this case, we would like to rebind the original image request. - if (!mImageRequestBinding.isBound() && mDetachedRequestDescriptor != null) { - setImageResourceId(mDetachedRequestDescriptor); - } - mDetachedRequestDescriptor = null; - } - - @Override - protected void onDetachedFromWindow() { - super.onDetachedFromWindow(); - // Dispose the bitmap, but if an AysncImageView is removed from the window, then quickly - // re-added, we shouldn't dispose, so wait a short time before disposing - ThreadUtil.getMainThreadHandler().postDelayed(mDisposeRunnable, DISPOSE_IMAGE_DELAY); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - - // The base implementation does not honor the minimum sizes. We try to to honor it here. - - final int measuredWidth = getMeasuredWidth(); - final int measuredHeight = getMeasuredHeight(); - if (measuredWidth >= getMinimumWidth() || measuredHeight >= getMinimumHeight()) { - // We are ok if either of the minimum sizes is honored. Note that satisfying both the - // sizes may not be possible, depending on the aspect ratio of the image and whether - // a maximum size has been specified. This implementation only tries to handle the case - // where both the minimum sizes are not being satisfied. - return; - } - - if (!getAdjustViewBounds()) { - // The base implementation is reasonable in this case. If the view bounds cannot be - // changed, it is not possible to satisfy the minimum sizes anyway. - return; - } - - final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); - final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); - if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) { - // The base implementation is reasonable in this case. - return; - } - - int width = measuredWidth; - int height = measuredHeight; - // Get the minimum sizes that will honor other constraints as well. - final int minimumWidth = resolveSize( - getMinimumWidth(), getMaxWidth(), widthMeasureSpec); - final int minimumHeight = resolveSize( - getMinimumHeight(), getMaxHeight(), heightMeasureSpec); - final float aspectRatio = measuredWidth / (float) measuredHeight; - if (aspectRatio == 0) { - // If the image is (close to) infinitely high, there is not much we can do. - return; - } - - if (width < minimumWidth) { - height = resolveSize((int) (minimumWidth / aspectRatio), - getMaxHeight(), heightMeasureSpec); - width = (int) (height * aspectRatio); - } - - if (height < minimumHeight) { - width = resolveSize((int) (minimumHeight * aspectRatio), - getMaxWidth(), widthMeasureSpec); - height = (int) (width / aspectRatio); - } - - setMeasuredDimension(width, height); - } - - private static int resolveSize(int desiredSize, int maxSize, int measureSpec) { - final int specMode = MeasureSpec.getMode(measureSpec); - final int specSize = MeasureSpec.getSize(measureSpec); - switch(specMode) { - case MeasureSpec.UNSPECIFIED: - return Math.min(desiredSize, maxSize); - - case MeasureSpec.AT_MOST: - return Math.min(Math.min(desiredSize, specSize), maxSize); - - default: - Assert.fail("Unreachable"); - return specSize; - } - } - - @Override - protected void onDraw(final Canvas canvas) { - if (mCornerRadius > 0) { - final int currentWidth = this.getWidth(); - final int currentHeight = this.getHeight(); - if (mClipPathWidth != currentWidth || mClipPathHeight != currentHeight) { - final RectF rect = new RectF(0, 0, currentWidth, currentHeight); - mRoundedCornerClipPath.reset(); - mRoundedCornerClipPath.addRoundRect(rect, mCornerRadius, mCornerRadius, - Path.Direction.CW); - mClipPathWidth = currentWidth; - mClipPathHeight = currentHeight; - } - - final int saveCount = canvas.getSaveCount(); - canvas.save(); - canvas.clipPath(mRoundedCornerClipPath); - super.onDraw(canvas); - canvas.restoreToCount(saveCount); - } else { - super.onDraw(canvas); - } - } - - private void unbindView() { - if (mImageRequestBinding.isBound()) { - mImageRequestBinding.unbind(); - if (mDelayLoader != null) { - mDelayLoader.unregisterView(this); - } - } - } - - /** - * As a performance optimization, the consumer of the AsyncImageView may opt to delay loading - * the image when it's busy doing other things (such as when a list view is scrolling). In - * order to do this, the consumer can create a new AsyncImageViewDelayLoader instance to be - * shared among all relevant AsyncImageViews (through setDelayLoader() method), and call - * onStartDelayLoading() and onStopDelayLoading() to start and stop delay loading, respectively. - */ - public static class AsyncImageViewDelayLoader { - private boolean mShouldDelayLoad; - private final HashSet<AsyncImageView> mAttachedViews; - - public AsyncImageViewDelayLoader() { - mAttachedViews = new HashSet<AsyncImageView>(); - } - - private void registerView(final AsyncImageView view) { - mAttachedViews.add(view); - } - - private void unregisterView(final AsyncImageView view) { - mAttachedViews.remove(view); - } - - public boolean isDelayLoadingImage() { - return mShouldDelayLoad; - } - - /** - * Called by the consumer of this view to delay loading images - */ - public void onDelayLoading() { - // Don't need to explicitly tell the AsyncImageView to stop loading since - // ImageRequests are not cancellable. - mShouldDelayLoad = true; - } - - /** - * Called by the consumer of this view to resume loading images - */ - public void onResumeLoading() { - if (mShouldDelayLoad) { - mShouldDelayLoad = false; - - // Notify all attached views to resume loading. - for (final AsyncImageView view : mAttachedViews) { - view.resumeLoading(); - } - mAttachedViews.clear(); - } - } - } -} |