Initial checkin of AOSP Messaging app.
b/23110861 Change-Id: I9aa980d7569247d6b2ca78f5dcb4502e1eaadb8a
This commit is contained in:
457
src/com/android/messaging/ui/AsyncImageView.java
Normal file
457
src/com/android/messaging/ui/AsyncImageView.java
Normal file
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user