import com.android.camera.data.FilmstripItemType;
import com.android.camera.data.FilmstripItemUtils;
import com.android.camera.data.FixedLastProxyAdapter;
+import com.android.camera.data.GlideFilmstripManager;
import com.android.camera.data.LocalFilmstripDataAdapter;
import com.android.camera.data.LocalFilmstripDataAdapter.FilmstripItemListener;
import com.android.camera.data.MediaDetails;
// Prefill glides bitmap pool to prevent excessive jank
// when loading large images.
glide.preFillBitmapPool(
- new PreFillType.Builder(
- FilmstripItem.MAXIMUM_TEXTURE_SIZE)
+ new PreFillType.Builder(GlideFilmstripManager.MAXIMUM_TEXTURE_SIZE)
.setWeight(5),
// It's more important for jank and GC to have
// A larger weight of max texture size images than
// media store sized images.
new PreFillType.Builder(
- FilmstripItem.MEDIASTORE_THUMB_WIDTH,
- FilmstripItem.MEDIASTORE_THUMB_HEIGHT));
+ GlideFilmstripManager.MEDIASTORE_THUMB_WIDTH,
+ GlideFilmstripManager.MEDIASTORE_THUMB_HEIGHT));
}
mOnCreateTime = System.currentTimeMillis();
mPanoramaViewHelper.onCreate();
ContentResolver appContentResolver = mAppContext.getContentResolver();
- mPhotoItemFactory = new PhotoItemFactory(mAppContext, appContentResolver,
+ GlideFilmstripManager glideManager = new GlideFilmstripManager(mAppContext);
+ mPhotoItemFactory = new PhotoItemFactory(mAppContext, glideManager, appContentResolver,
new PhotoDataFactory());
- mVideoItemFactory = new VideoItemFactory(mAppContext, appContentResolver,
+ mVideoItemFactory = new VideoItemFactory(mAppContext, glideManager, appContentResolver,
new VideoDataFactory());
mDataAdapter = new CameraFilmstripDataAdapter(mAppContext,
mPhotoItemFactory, mVideoItemFactory);
return null;
}
- return mFilmstripItems.get(index).getView(Optional.fromNullable(recycled), mSuggestedWidth,
- mSuggestedHeight, this, /* inProgress */ false, videoClickedCallback);
- }
+ FilmstripItem item = mFilmstripItems.get(index);
+ item.setSuggestedSize(mSuggestedWidth, mSuggestedHeight);
- @Override
- public void resizeView(int index, View view, int w, int h) {
- if (index >= mFilmstripItems.size() || index < 0) {
- return;
- }
- mFilmstripItems.get(index).loadFullImage(mSuggestedWidth, mSuggestedHeight, view);
+ return item.getView(Optional.fromNullable(recycled), this, /* inProgress */ false,
+ videoClickedCallback);
}
@Override
import com.android.camera.debug.Log;
import com.android.camera.util.Size;
-import com.android.camera2.R;
import com.google.common.base.Optional;
+import javax.annotation.Nonnull;
+
/**
* An abstract interface that represents the Local filmstrip items.
*/
public interface FilmstripItem {
static final Log.Tag TAG = new Log.Tag("FilmstripItem");
- public static final int MEDIASTORE_THUMB_WIDTH = 512;
- public static final int MEDIASTORE_THUMB_HEIGHT = 384;
-
- // GL max texture size: keep bitmaps below this value.
- public static final int MAXIMUM_TEXTURE_SIZE = 2048;
- public static final int MAXIMUM_SMOOTH_TEXTURE_SIZE = 1024;
-
- /** Default placeholder to display while images load */
- static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
-
-
/**
* An action callback to be used for actions on the filmstrip items.
*/
* hierarchy. {@code FilmStripView} should always call this function after its
* corresponding view is removed from the view hierarchy.
*/
- public void recycle(View view);
+ public void recycle(@Nonnull View view);
/**
* Create or recycle an existing view (if provided) to render this item.
*
- * @param viewWidthPx Width in pixels of the suggested zoomed out view/image size.
- * @param viewHeightPx Height in pixels of the suggested zoomed out view/image size.
* @param adapter Data adapter for this data item.
*/
- public View getView(Optional<View> view, int viewWidthPx, int viewHeightPx,
+ public View getView(Optional<View> view,
LocalFilmstripDataAdapter adapter, boolean isInProgress,
VideoClickedCallback videoClickedCallback);
/**
- * Request resize of View created by getView().
+ * Configure the suggested width and height in pixels for this view to render at.
+ *
+ * @param widthPx Suggested width in pixels.
+ * @param heightPx Suggested height in pixels.
+ */
+ public void setSuggestedSize(int widthPx, int heightPx);
+
+ /**
+ * Request to load a tiny preview image into the view as fast as possible.
+ *
+ * @param view View created by getView();
+ */
+ public void renderTiny(@Nonnull View view);
+
+ /**
+ * Request to load screen sized version of the image into the view.
+ *
+ * @param view View created by getView();
+ */
+ public void renderThumbnail(@Nonnull View view);
+
+ /**
+ * Request to load the highest possible resolution image supported.
*
- * @param thumbWidth Width in pixels of the suggested zoomed out view/image size.
- * @param thumbHeight Height in pixels of the suggested zoomed out view/image size.
* @param view View created by getView();
*/
- public void loadFullImage(int thumbWidth, int thumbHeight, View view);
+ public void renderFullRes(@Nonnull View view);
/**
* Removes the data from the storage if possible.
package com.android.camera.data;
import android.content.Context;
-import android.graphics.Bitmap;
-import android.net.Uri;
import android.view.View;
import com.android.camera.Storage;
import com.android.camera.debug.Log;
import com.android.camera.util.Size;
-import com.bumptech.glide.BitmapRequestBuilder;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key;
import com.bumptech.glide.signature.MediaStoreSignature;
import java.io.File;
import java.text.DateFormat;
+import javax.annotation.Nonnull;
+
/**
* A base class for all the local media files. The bitmap is loaded in
* background thread. Subclasses should implement their own background loading
public static final int QUERY_ALL_MEDIA_ID = -1;
protected final Context mContext;
+ protected final GlideFilmstripManager mGlideManager;
protected final T mData;
protected final Metadata mMetaData;
protected final FilmstripItemAttributes mAttributes;
protected final DateFormat mDateFormatter = DateFormat.getDateTimeInstance();
- public FilmstripItemBase(Context context, T data, FilmstripItemAttributes attributes) {
+ protected int mSuggestedWidthPx;
+ protected int mSuggestedHeightPx;
+
+ public FilmstripItemBase(Context context, GlideFilmstripManager glideManager, T data,
+ FilmstripItemAttributes attributes) {
mContext = context;
+ mGlideManager = glideManager;
mData = data;
mAttributes = attributes;
+
mMetaData = new Metadata();
+
+ mSuggestedWidthPx = GlideFilmstripManager.TINY_THUMBNAIL_SIZE;
+ mSuggestedHeightPx = GlideFilmstripManager.TINY_THUMBNAIL_SIZE;
}
@Override
}
@Override
- public void loadFullImage(int thumbWidth, int thumbHeight, View view) {
- // Default is do nothing.
- // Can be implemented by sub-classes.
+ public void setSuggestedSize(int widthPx, int heightPx) {
+ if (widthPx > 0 && heightPx > 0) {
+ mSuggestedWidthPx = widthPx;
+ mSuggestedHeightPx = heightPx;
+ } else {
+ Log.w(TAG, "Suggested size was set to a zero area value!");
+ }
}
@Override
- public void recycle(View view) { }
+ public void recycle(@Nonnull View view) {
+ Glide.clear(view);
+ }
@Override
public Optional<MediaDetails> getMediaDetails() {
return mData.getOrientation();
}
- // TODO: Move the glide classes to a specific rendering class.
- protected BitmapRequestBuilder<Uri, Bitmap> glideFullResBitmap(Uri uri,
- int width, int height) {
- // compute a ratio such that viewWidth and viewHeight are less than
- // MAXIMUM_SMOOTH_TEXTURE_SIZE but maintain their aspect ratio.
- float downscaleRatio = downscaleRatioToFit(width, height,
- MAXIMUM_TEXTURE_SIZE);
-
- return Glide.with(mContext)
- .loadFromMediaStore(uri)
- .asBitmap()
- .atMost()
- .fitCenter()
- .signature(getGlideKey())
- .override(
- Math.round(width * downscaleRatio),
- Math.round(height * downscaleRatio));
- }
-
- protected BitmapRequestBuilder<Uri, Bitmap> glideFilmstripThumb(Uri uri,
- int viewWidth, int viewHeight) {
- // compute a ratio such that viewWidth and viewHeight are less than
- // MAXIMUM_SMOOTH_TEXTURE_SIZE but maintain their aspect ratio.
- float downscaleRatio = downscaleRatioToFit(viewWidth, viewHeight,
- MAXIMUM_SMOOTH_TEXTURE_SIZE);
-
- return Glide.with(mContext)
- .loadFromMediaStore(uri)
- .asBitmap()
- .atMost()
- .fitCenter()
- .signature(getGlideKey())
- .override(
- Math.round(viewWidth * downscaleRatio),
- Math.round(viewHeight * downscaleRatio));
- }
-
- protected BitmapRequestBuilder<Uri, Bitmap>glideMediaStoreThumb(Uri uri) {
- return Glide.with(mContext)
- .loadFromMediaStore(uri)
- .asBitmap()
- .atMost()
- .fitCenter()
- .signature(getGlideKey())
- // This attempts to ensure we load the cached media store version.
- .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT);
- }
-
- protected BitmapRequestBuilder<Uri, Bitmap> glideTinyThumb(Uri uri) {
- return Glide.with(mContext)
- .loadFromMediaStore(uri)
- .asBitmap()
- .atMost()
- .fitCenter()
- .signature(getGlideKey())
- .override(256, 265);
- }
-
- protected Key getGlideKey() {
+ protected final Key generateSignature(FilmstripItemData data) {
// Per Glide docs, make default mime type be the empty String
- String mimeType = (mData.getMimeType() == null) ? "" : mData.getMimeType();
- long modTimeSeconds = (mData.getLastModifiedDate() == null) ? 0 :
- mData.getLastModifiedDate().getTime() / 1000;
- return new MediaStoreSignature(mimeType, modTimeSeconds, mData.getOrientation());
- }
-
- private float downscaleRatioToFit(int width, int height, int fitWithinSize) {
- // Find the longest dimension
- int longest = Math.max(width, height);
-
- if (longest > fitWithinSize) {
- return (float)fitWithinSize / (float)longest;
- }
-
- return 1.0f;
+ String mimeType = (data.getMimeType() == null) ? "" : data.getMimeType();
+ long modTimeSeconds = (data.getLastModifiedDate() == null) ? 0 :
+ data.getLastModifiedDate().getTime() / 1000;
+ return new MediaStoreSignature(mimeType, modTimeSeconds, data.getOrientation());
}
private void deleteIfEmptyCameraSubDir(File directory) {
@Override
public View getView(View recycled, int index, VideoClickedCallback videoClickedCallback) {
if (index == 0) {
- return mFirstData.getView(Optional.fromNullable(recycled), mSuggestedWidth,
- mSuggestedHeight, null, false, videoClickedCallback);
+ mFirstData.setSuggestedSize(mSuggestedWidth, mSuggestedHeight);
+ return mFirstData.getView(Optional.fromNullable(recycled), null, false,
+ videoClickedCallback);
}
return mAdapter.getView(recycled, index - 1, videoClickedCallback);
}
}
@Override
- public void resizeView(int index, View view, int w, int h) {
- // Do nothing.
- }
-
- @Override
public FilmstripItem getFilmstripItemAt(int index) {
if (index == 0) {
return mFirstData;
if (index < totalNumber) {
return mAdapter.getView(recycled, index, videoClickedCallback);
} else if (index == totalNumber) {
- return mLastData.getView(Optional.fromNullable(recycled), mSuggestedWidth,
- mSuggestedHeight, null, false, videoClickedCallback);
+ mLastData.setSuggestedSize(mSuggestedWidth, mSuggestedHeight);
+ return mLastData.getView(Optional.fromNullable(recycled), null, false,
+ videoClickedCallback);
}
return null;
}
}
@Override
- public void resizeView(int index, View view, int w, int h) {
- // Do nothing.
- }
-
- @Override
public FilmstripItem getFilmstripItemAt(int index) {
int totalNumber = mAdapter.getTotalNumber();
--- /dev/null
+/*
+ * 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.camera.data;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+
+import com.android.camera2.R;
+import com.bumptech.glide.DrawableRequestBuilder;
+import com.bumptech.glide.GenericRequestBuilder;
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.RequestManager;
+import com.bumptech.glide.load.Key;
+import com.bumptech.glide.load.resource.bitmap.BitmapEncoder;
+import com.bumptech.glide.load.resource.drawable.GlideDrawable;
+import com.bumptech.glide.load.resource.gif.GifResourceEncoder;
+import com.bumptech.glide.load.resource.gifbitmap.GifBitmapWrapperResourceEncoder;
+import com.bumptech.glide.load.resource.transcode.BitmapToGlideDrawableTranscoder;
+
+/**
+ * Manage common glide image requests for the camera filmstrip.
+ */
+public final class GlideFilmstripManager {
+ /** Default placeholder to display while images load */
+ public static final int DEFAULT_PLACEHOLDER_RESOURCE = R.color.photo_placeholder;
+
+ // GL max texture size: keep bitmaps below this value.
+ public static final int MAXIMUM_TEXTURE_SIZE = 2048;
+ public static final int MAXIMUM_SMOOTH_PIXELS = 1024 * 1024;
+
+ public static final int MEDIASTORE_THUMB_WIDTH = 512;
+ public static final int MEDIASTORE_THUMB_HEIGHT = 384;
+
+ public static final int TINY_THUMBNAIL_SIZE = 256;
+
+ private static final int JPEG_COMPRESS_QUALITY = 90;
+
+ private final GenericRequestBuilder<Uri, ?, ?, GlideDrawable> mTinyImageBuilder;
+ private final DrawableRequestBuilder<Uri> mLargeImageBuilder;
+
+ public GlideFilmstripManager(Context context) {
+ Glide glide = Glide.get(context);
+ BitmapEncoder bitmapEncoder = new BitmapEncoder(Bitmap.CompressFormat.JPEG,
+ JPEG_COMPRESS_QUALITY);
+ GifBitmapWrapperResourceEncoder drawableEncoder = new GifBitmapWrapperResourceEncoder(
+ bitmapEncoder,
+ new GifResourceEncoder(glide.getBitmapPool()));
+ RequestManager request = Glide.with(context);
+
+ mTinyImageBuilder = request
+ .fromMediaStore()
+ .asBitmap() // This prevents gifs from animating at tiny sizes.
+ .transcode(new BitmapToGlideDrawableTranscoder(context), GlideDrawable.class)
+ .fitCenter()
+ .dontAnimate();
+
+ mLargeImageBuilder = request
+ .fromMediaStore()
+ .encoder(drawableEncoder)
+ .fitCenter()
+ .dontAnimate();
+ }
+
+ /**
+ * Create a full size drawable request for a given width and height that is
+ * as large as we can reasonably load into a view without causing massive
+ * jank problems.
+ */
+ public final DrawableRequestBuilder<Uri> loadFull(Uri uri, Key key, int width,
+ int height) {
+ // compute a ratio such that viewWidth and viewHeight are less than
+ // MAXIMUM_SMOOTH_TEXTURE_SIZE but maintain their aspect ratio.
+ float downscaleRatio = downscaleRatioToFit(width, height,
+ (double) MAXIMUM_TEXTURE_SIZE * MAXIMUM_TEXTURE_SIZE);
+
+ return mLargeImageBuilder
+ .clone()
+ .load(uri)
+ .signature(key)
+ .override(
+ Math.round(width * downscaleRatio),
+ Math.round(height * downscaleRatio));
+ }
+
+ /**
+ * Create a full size drawable request for a given width and height that is
+ * smaller than loadFull, but is intended be large enough to fill the screen
+ * pixels.
+ */
+ public DrawableRequestBuilder<Uri> loadScreen(Uri uri, Key key, int width,
+ int height) {
+ // compute a ratio such that viewWidth and viewHeight are less than
+ // MAXIMUM_SMOOTH_TEXTURE_SIZE but maintain their aspect ratio.
+ float downscaleRatio = downscaleRatioToFit(width, height, (double) MAXIMUM_SMOOTH_PIXELS);
+
+ return mLargeImageBuilder
+ .clone()
+ .load(uri)
+ .signature(key)
+ .override(
+ Math.round(width * downscaleRatio),
+ Math.round(height * downscaleRatio));
+ }
+
+ /**
+ * Create a small thumbnail sized image that has the same bounds as the
+ * media store thumbnail images.
+ *
+ * If the Uri points at an animated gif, the gif will not play.
+ */
+ public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadMediaStoreThumb(Uri uri, Key key) {
+ return mTinyImageBuilder
+ .clone()
+ .load(uri)
+ .signature(key)
+ .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
+ // This attempts to ensure we load the cached media store version.
+ .override(MEDIASTORE_THUMB_WIDTH, MEDIASTORE_THUMB_HEIGHT);
+ }
+
+ /**
+ * Create very tiny thumbnail request that should complete as fast
+ * as possible.
+ *
+ * If the Uri points at an animated gif, the gif will not play.
+ */
+ public GenericRequestBuilder<Uri, ?, ?, GlideDrawable> loadTinyThumb(Uri uri, Key key) {
+ return mTinyImageBuilder
+ .clone()
+ .load(uri)
+ .signature(key)
+ .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
+ .override(TINY_THUMBNAIL_SIZE, TINY_THUMBNAIL_SIZE);
+ }
+
+ private float downscaleRatioToFit(int width, int height, double area) {
+ // Compute a ratio that will keep the area of the image within the fit size parameter.
+
+ float ratio = (float) Math.sqrt(area / (height * width));
+ return Math.min(ratio, 1.0f);
+ }
+}
import com.android.camera.util.CameraUtil;
import com.android.camera.util.Size;
import com.android.camera2.R;
-import com.bumptech.glide.BitmapRequestBuilder;
+import com.bumptech.glide.DrawableRequestBuilder;
+import com.bumptech.glide.GenericRequestBuilder;
import com.bumptech.glide.Glide;
+import com.bumptech.glide.load.resource.drawable.GlideDrawable;
import com.google.common.base.Optional;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import javax.annotation.Nonnull;
+
/**
* Backing data for a single photo displayed in the filmstrip.
*/
private Bitmap mSessionPlaceholderBitmap;
- public PhotoItem(Context context, FilmstripItemData data, PhotoItemFactory photoItemFactory) {
- super(context, data, PHOTO_ITEM_ATTRIBUTES);
+ public PhotoItem(Context context, GlideFilmstripManager manager, FilmstripItemData data,
+ PhotoItemFactory photoItemFactory) {
+ super(context, manager, data, PHOTO_ITEM_ATTRIBUTES);
mPhotoItemFactory = photoItemFactory;
}
}
@Override
- public View getView(Optional<View> optionalView, int viewWidthPx, int viewHeightPx,
- LocalFilmstripDataAdapter adapter, boolean isInProgress,
- VideoClickedCallback videoClickedCallback) {
+ public View getView(Optional<View> optionalView, LocalFilmstripDataAdapter adapter,
+ boolean isInProgress, VideoClickedCallback videoClickedCallback) {
ImageView imageView;
if (optionalView.isPresent()) {
}
protected void fillImageView(final ImageView imageView) {
- Uri uri = mData.getUri();
-
- glideTinyThumb(uri)
- .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
- .dontAnimate()
- .into(imageView);
+ renderTinySize(mData.getUri()).into(imageView);
// TODO consider having metadata have a "get description" string
// or some other way of selecting rendering details based on metadata.
}
@Override
- public void recycle(View view) {
+ public void recycle(@Nonnull View view) {
Glide.clear(view);
mSessionPlaceholderBitmap = null;
}
}
@Override
- public void loadFullImage(int thumbWidth, int thumbHeight, View v) {
- Uri uri = mData.getUri();
- Size size = mData.getDimensions();
+ public void renderTiny(@Nonnull View view) {
+ if (view instanceof ImageView) {
+ renderTinySize(mData.getUri()).into((ImageView) view);
+ } else {
+ Log.w(TAG, "renderTiny was called with an object that is not an ImageView!");
+ }
+ }
- BitmapRequestBuilder<Uri, Bitmap> builder =
- glideFullResBitmap(uri, size.getWidth(), size.getHeight());
+ @Override
+ public void renderThumbnail(@Nonnull View view) {
+ if (view instanceof ImageView) {
+ renderScreenSize(mData.getUri()).into((ImageView) view);
+ } else {
+ Log.w(TAG, "renderThumbnail was called with an object that is not an ImageView!");
+ }
+ }
- if (mSessionPlaceholderBitmap != null) {
- builder.placeholder(new BitmapDrawable(mContext.getResources(),
- mSessionPlaceholderBitmap));
+ @Override
+ public void renderFullRes(@Nonnull View view) {
+ if (view instanceof ImageView) {
+ renderFullSize(mData.getUri()).into((ImageView) view);
} else {
- builder
- .thumbnail(glideTinyThumb(uri))
- .placeholder(DEFAULT_PLACEHOLDER_RESOURCE);
+ Log.w(TAG, "renderFullRes was called with an object that is not an ImageView!");
}
+ }
+
+ private GenericRequestBuilder<Uri, ?, ?, GlideDrawable> renderTinySize(Uri uri) {
+ return mGlideManager.loadTinyThumb(uri, generateSignature(mData));
+ }
+
+ private DrawableRequestBuilder<Uri> renderScreenSize(Uri uri) {
+ DrawableRequestBuilder<Uri> request =
+ mGlideManager.loadScreen(uri, generateSignature(mData),
+ mSuggestedWidthPx, mSuggestedHeightPx);
- builder
- .dontAnimate()
- .into((ImageView) v);
+ // If we have a non-null placeholder, use that and do NOT ever render a
+ // tiny thumbnail to prevent un-intended "flash of low resolution image"
+ if (mSessionPlaceholderBitmap != null) {
+ return request.placeholder(new BitmapDrawable(mContext.getResources(),
+ mSessionPlaceholderBitmap));
+ }
+
+ // If we do not have a placeholder bitmap, render a thumbnail with
+ // the default placeholder resource like normal.
+ return request
+ .thumbnail(renderTinySize(uri));
+ }
+
+ private DrawableRequestBuilder<Uri> renderFullSize(Uri uri) {
+ Size size = mData.getDimensions();
+ return mGlideManager.loadFull(uri, generateSignature(mData), size.getWidth(),
+ size.getHeight())
+ .thumbnail(renderScreenSize(uri));
}
@Override
private static final Log.Tag TAG = new Log.Tag("PhotoItemFact");
private final Context mContext;
+ private final GlideFilmstripManager mGlideManager;
private final ContentResolver mContentResolver;
private final PhotoDataFactory mPhotoDataFactory;
- public PhotoItemFactory(Context context, ContentResolver contentResolver,
- PhotoDataFactory photoDataFactory) {
+ public PhotoItemFactory(Context context, GlideFilmstripManager glideManager,
+ ContentResolver contentResolver, PhotoDataFactory photoDataFactory) {
mContext = context;
+ mGlideManager = glideManager;
mContentResolver = contentResolver;
mPhotoDataFactory = photoDataFactory;
}
public PhotoItem get(Cursor c) {
FilmstripItemData data = mPhotoDataFactory.fromCursor(c);
if (data != null) {
- return new PhotoItem(mContext, data, this);
+ return new PhotoItem(mContext, mGlideManager, data, this);
} else {
Log.w(TAG, "skipping item with null data, returning null for item");
return null;
import java.util.Date;
import java.util.UUID;
+import javax.annotation.Nonnull;
+
/**
* A LocalData that does nothing but only shows a view.
*/
}
@Override
- public View getView(Optional<View> optionalView, int viewWidthPx, int viewHeightPx,
+ public View getView(Optional<View> optionalView,
LocalFilmstripDataAdapter adapter, boolean isInProgress,
VideoClickedCallback videoClickedCallback) {
return mView;
}
@Override
- public void loadFullImage(int w, int h, View view) {
- // do nothing.
- }
+ public void setSuggestedSize(int widthPx, int heightPx) { }
+
+ @Override
+ public void renderTiny(@Nonnull View view) { }
+
+ @Override
+ public void renderThumbnail(@Nonnull View view) { }
+
+ @Override
+ public void renderFullRes(@Nonnull View view) { }
@Override
- public void recycle(View view) {
+ public void recycle(@Nonnull View view) {
// Do nothing.
}
import java.util.Date;
+import javax.annotation.Nonnull;
+
/**
* This is used to represent a local data item that is in progress and not
* yet in the media store.
}
@Override
- public View getView(Optional<View> optionalView, int viewWidthPx, int viewHeightPx,
- LocalFilmstripDataAdapter adapter, boolean isInProgress,
- VideoClickedCallback videoClickedCallback) {
+ public View getView(Optional<View> optionalView, LocalFilmstripDataAdapter adapter,
+ boolean isInProgress, VideoClickedCallback videoClickedCallback) {
ImageView imageView;
if (optionalView.isPresent()) {
if (placeholder != null) {
imageView.setImageBitmap(placeholder);
} else {
- imageView.setImageResource(DEFAULT_PLACEHOLDER_RESOURCE);
+ imageView.setImageResource(GlideFilmstripManager.DEFAULT_PLACEHOLDER_RESOURCE);
}
imageView.setContentDescription(mContext.getResources().getString(
R.string.media_processing_content_description));
}
@Override
- public void loadFullImage(int width, int height, View view) {
- }
+ public void setSuggestedSize(int widthPx, int heightPx) { }
+
+ @Override
+ public void renderTiny(@Nonnull View view) { }
+
+ @Override
+ public void renderThumbnail(@Nonnull View view) { }
+
+ @Override
+ public void renderFullRes(@Nonnull View view) { }
@Override
public boolean delete() {
}
@Override
- public void recycle(View view) {
+ public void recycle(@Nonnull View view) {
Glide.clear(view);
}
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
-import android.net.Uri;
import android.provider.MediaStore;
import android.view.LayoutInflater;
import android.view.View;
import com.bumptech.glide.Glide;
import com.google.common.base.Optional;
+import javax.annotation.Nonnull;
+
/**
* Backing data for a single video displayed in the filmstrip.
*/
private Size mCachedSize;
- public VideoItem(Context context, VideoItemData data, VideoItemFactory videoItemFactory) {
- super(context, data, VIDEO_ITEM_ATTRIBUTES);
+ public VideoItem(Context context, GlideFilmstripManager manager, VideoItemData data,
+ VideoItemFactory videoItemFactory) {
+ super(context, manager, data, VIDEO_ITEM_ATTRIBUTES);
mVideoItemFactory = videoItemFactory;
}
}
@Override
- public View getView(Optional<View> optionalView, int viewWidthPx, int viewHeightPx,
+ public View getView(Optional<View> optionalView,
LocalFilmstripDataAdapter adapter, boolean isInProgress,
- VideoClickedCallback videoClickedCallback) {
+ final VideoClickedCallback videoClickedCallback) {
+
View view;
VideoViewHolder viewHolder;
if (optionalView.isPresent()) {
view = optionalView.get();
- viewHolder = (VideoViewHolder) view.getTag(R.id.mediadata_tag_target);
+ viewHolder = getViewHolder(view);
} else {
view = LayoutInflater.from(mContext).inflate(R.layout.filmstrip_video, null);
view.setTag(R.id.mediadata_tag_viewtype, getItemViewType().ordinal());
view.setTag(R.id.mediadata_tag_target, viewHolder);
}
- fillVideoView(view, viewHolder, viewWidthPx, viewHeightPx, videoClickedCallback);
-
- return view;
- }
+ if (viewHolder != null) {
+ // ImageView for the play icon.
+ viewHolder.mPlayButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ videoClickedCallback.playVideo(mData.getUri(), mData.getTitle());
+ }
+ });
- private void fillVideoView(View view, VideoViewHolder viewHolder, final int viewWidthPx,
- final int viewHeightPx, final VideoClickedCallback videoClickedCallback) {
+ view.setContentDescription(mContext.getResources().getString(
+ R.string.video_date_content_description,
+ mDateFormatter.format(mData.getLastModifiedDate())));
- //TODO: Figure out why these can be <= 0.
- if (viewWidthPx <= 0 || viewHeightPx <=0) {
- return;
+ renderTiny(viewHolder);
+ } else {
+ Log.w(TAG, "getView called with a view that is not compatible with VideoItem.");
}
- Uri uri = mData.getUri();
-
- glideFilmstripThumb(uri, viewWidthPx, viewHeightPx)
- .thumbnail(glideMediaStoreThumb(uri))
- .placeholder(DEFAULT_PLACEHOLDER_RESOURCE)
- .dontAnimate()
- .into(viewHolder.mVideoView);
+ return view;
+ }
- // ImageView for the play icon.
- viewHolder.mPlayButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- videoClickedCallback.playVideo(mData.getUri(), mData.getTitle());
- }
- });
+ @Override
+ public void renderTiny(@Nonnull View view) {
+ renderTiny(getViewHolder(view));
+ }
- view.setContentDescription(mContext.getResources().getString(
- R.string.video_date_content_description,
- mDateFormatter.format(mData.getLastModifiedDate())));
+ @Override
+ public void renderThumbnail(@Nonnull View view) {
+ mGlideManager.loadScreen(mData.getUri(), generateSignature(mData),
+ mSuggestedWidthPx, mSuggestedHeightPx)
+ .thumbnail(mGlideManager.loadMediaStoreThumb(mData.getUri(),
+ generateSignature(mData)))
+ .into(getViewHolder(view).mVideoView);
}
+ @Override
+ public void renderFullRes(@Nonnull View view) { }
@Override
- public void recycle(View view) {
- VideoViewHolder videoViewHolder =
- (VideoViewHolder) view.getTag(R.id.mediadata_tag_target);
- Glide.clear(videoViewHolder.mVideoView);
+ public void recycle(@Nonnull View view) {
+ VideoViewHolder holder = getViewHolder(view);
+ if (holder != null) {
+ Glide.clear(getViewHolder(view).mVideoView);
+ }
}
@Override
public String toString() {
return "VideoItem: " + mData.toString();
}
+
+ private void renderTiny(@Nonnull VideoViewHolder viewHolder) {
+ mGlideManager.loadMediaStoreThumb(mData.getUri(), generateSignature(mData))
+ .into(viewHolder.mVideoView);
+ }
+
+ private VideoViewHolder getViewHolder(@Nonnull View view) {
+ Object container = view.getTag(R.id.mediadata_tag_target);
+ if (container instanceof VideoViewHolder) {
+ return (VideoViewHolder) container;
+ }
+
+ return null;
+ }
}
+ " DESC, " + MediaStore.Video.VideoColumns._ID + " DESC";
private final Context mContext;
+ private final GlideFilmstripManager mGlideManager;
private final ContentResolver mContentResolver;
private final VideoDataFactory mVideoDataFactory;
- public VideoItemFactory(Context context, ContentResolver contentResolver,
- VideoDataFactory videoDataFactory) {
+ public VideoItemFactory(Context context, GlideFilmstripManager glideManager,
+ ContentResolver contentResolver, VideoDataFactory videoDataFactory) {
mContext = context;
+ mGlideManager = glideManager;
mContentResolver = contentResolver;
mVideoDataFactory = videoDataFactory;
}
public VideoItem get(Cursor c) {
VideoItemData data = mVideoDataFactory.fromCursor(c);
if (data != null) {
- return new VideoItem(mContext, data, this);
+ return new VideoItem(mContext, mGlideManager, data, this);
} else {
Log.w(TAG, "skipping item with null data, returning null for item");
return null;
public int getItemViewType(int index);
/**
- * Resizes the view used to visually present the image data. This is
- * useful when the view contains a bitmap.
- *
- * @param index The ID of the resize data to be presented.
- * @param view The view to update that was created by getView().
- * @param w Width in pixels of rendered view.
- * @param h Height in pixels of rendered view.
- */
- public void resizeView(int index, View view, int w, int h);
-
- /**
* Returns the {@link FilmstripItem} specified by the ID.
*
* @param index The ID of the {@link FilmstripItem}.
* A helper class to tract and calculate the view coordination.
*/
private static class ViewItem {
+ private static enum RenderSize {
+ TINY,
+ THUMBNAIL,
+ FULL_RES
+ }
+
+ private final FilmstripView mFilmstrip;
+ private final View mView;
+ private final RectF mViewArea;
+
private int mIndex;
/** The position of the left of the view in the whole filmstrip. */
private int mLeftPosition;
- private final View mView;
private FilmstripItem mData;
- private final RectF mViewArea;
- private boolean mMaximumBitmapRequested;
+ private RenderSize mRenderSize;
private ValueAnimator mTranslationXAnimator;
private ValueAnimator mTranslationYAnimator;
private ValueAnimator mAlphaAnimator;
- private final FilmstripView mFilmstrip;
/**
* Constructor.
* @param v The {@code View} representing the data.
*/
public ViewItem(int index, View v, FilmstripItem data, FilmstripView filmstrip) {
- v.setPivotX(0f);
- v.setPivotY(0f);
+ mFilmstrip = filmstrip;
+ mView = v;
+ mViewArea = new RectF();
+
mIndex = index;
mData = data;
- mView = v;
- mMaximumBitmapRequested = false;
mLeftPosition = -1;
- mViewArea = new RectF();
- mFilmstrip = filmstrip;
+ mRenderSize = RenderSize.TINY;
+
+ mView.setPivotX(0f);
+ mView.setPivotY(0f);
}
public void setData(FilmstripItem item) {
mData = item;
- mMaximumBitmapRequested = false;
+
+ renderTiny();
}
- public boolean isMaximumBitmapRequested() {
- return mMaximumBitmapRequested;
+ public void renderTiny() {
+ if (mRenderSize != RenderSize.TINY) {
+ mRenderSize = RenderSize.TINY;
+
+ Log.i(TAG, "[ViewItem:" + mIndex + "] mData.renderTiny()");
+ mData.renderTiny(mView);
+ }
}
- public void setMaximumBitmapRequested() {
- mMaximumBitmapRequested = true;
+ public void renderThumbnail() {
+ if (mRenderSize != RenderSize.THUMBNAIL) {
+ mRenderSize = RenderSize.THUMBNAIL;
+
+ Log.i(TAG, "[ViewItem:" + mIndex + "] mData.renderThumbnail()");
+ mData.renderThumbnail(mView);
+ }
+ }
+
+ public void renderFullRes() {
+ if (mRenderSize != RenderSize.FULL_RES) {
+ mRenderSize = RenderSize.FULL_RES;
+
+ Log.i(TAG, "[ViewItem:" + mIndex + "] mData.renderFullRes()");
+ mData.renderFullRes(mView);
+ }
}
/**
}
/**
- * Notifies the {@link com.android.camera.filmstrip.FilmstripDataAdapter} to
- * resize the view.
- */
- public void resizeView(int w, int h) {
- mFilmstrip.mDataAdapter.resizeView(mIndex, mView, w, h);
- }
-
- /**
* Adds the view of the data to the view hierarchy if necessary.
*/
public void addViewToHierarchy() {
return Math.round(mViewArea.left);
}
- public void copyAttributes(ViewItem item) {
- setLeftPosition(item.getLeftPosition());
- // X
- setTranslationX(item.getTranslationX());
- if (item.mTranslationXAnimator != null) {
- mTranslationXAnimator = item.mTranslationXAnimator;
- mTranslationXAnimator.removeAllUpdateListeners();
- mTranslationXAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- // We invalidate the filmstrip view instead of setting the
- // translation X because the translation X of the view is
- // touched in onLayout(). See the documentation of
- // animateTranslationX().
- mFilmstrip.invalidate();
- }
- });
- }
- // Y
- setTranslationY(item.getTranslationY());
- if (item.mTranslationYAnimator != null) {
- mTranslationYAnimator = item.mTranslationYAnimator;
- mTranslationYAnimator.removeAllUpdateListeners();
- mTranslationYAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- setTranslationY((Float) valueAnimator.getAnimatedValue());
- }
- });
- }
- // Alpha
- setAlpha(item.getAlpha());
- if (item.mAlphaAnimator != null) {
- mAlphaAnimator = item.mAlphaAnimator;
- mAlphaAnimator.removeAllUpdateListeners();
- mAlphaAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- ViewItem.this.setAlpha((Float) valueAnimator.getAnimatedValue());
- }
- });
- }
- }
-
/**
* Apply a scale factor (i.e. {@code postScale}) on top of current scale at
* pivot point ({@code focusX}, {@code focusY}). Visually it should be the
// Always scale by fixed filmstrip scale, since we only show items when
// in filmstrip. Preloading images with a different scale and bounds
// interferes with caching.
- int width = Math.round(FILM_STRIP_SCALE * getWidth());
- int height = Math.round(FILM_STRIP_SCALE * getHeight());
+ int width = Math.round(FULL_SCREEN_SCALE * getWidth());
+ int height = Math.round(FULL_SCREEN_SCALE * getHeight());
+
Log.v(TAG, "suggesting item bounds: " + width + "x" + height);
mDataAdapter.suggestViewSizeBound(width, height);
View recycled = getRecycledView(index);
- View v = mDataAdapter.getView(recycled, index,
- mVideoClickedCallback);
+ View v = mDataAdapter.getView(recycled, index, mVideoClickedCallback);
if (v == null) {
return null;
}
return item;
}
- private void ensureItemAtMaxSize(int bufferIndex) {
+ private void renderFullRes(int bufferIndex) {
ViewItem item = mViewItems[bufferIndex];
- if (item == null || item.isMaximumBitmapRequested()) {
+ if (item == null) {
return;
}
- item.setMaximumBitmapRequested();
- // Request full size bitmap, or max that DataAdapter will create.
- int index = item.getAdapterIndex();
- int h = mDataAdapter.getFilmstripItemAt(index).getDimensions().getHeight();
- int w = mDataAdapter.getFilmstripItemAt(index).getDimensions().getWidth();
- item.resizeView(w, h);
+
+ item.renderFullRes();
+ }
+
+ private void renderThumbnail(int bufferIndex) {
+ ViewItem item = mViewItems[bufferIndex];
+ if (item == null) {
+ return;
+ }
+
+ item.renderThumbnail();
}
- private void ensureBufferItemsAtMaxSize() {
+ private void renderAllThumbnails() {
for(int i = 0; i < BUFFER_SIZE; i++) {
- ensureItemAtMaxSize(i);
+ renderThumbnail(i);
}
}
}
mViewItems[bufferIndex] = viewItem;
- ensureItemAtMaxSize(bufferIndex);
+ renderThumbnail(bufferIndex);
viewItem.setAlpha(0f);
viewItem.setTranslationY(getHeight() / 8);
slideViewBack(viewItem);
mListener.onDataFocusChanged(index, getCurrentItemAdapterIndex());
}
Log.d(TAG, "onFilmstripItemInserted()");
- ensureBufferItemsAtMaxSize();
+ renderAllThumbnails();
}
@Override
mListener.onDataFocusChanged(index, getCurrentItemAdapterIndex());
}
Log.d(TAG, "onFilmstripItemRemoved()");
- ensureBufferItemsAtMaxSize();
+ renderAllThumbnails();
}
});
}
// is unreliable. Load the full resolution if either value
// reports that the item is not scrolling.
if (!mController.isScrolling() || !mIsUserScrolling) {
- ensureItemAtMaxSize(bufferIndex);
+ renderThumbnail(bufferIndex);
}
adjustChildZOrder();
adjustChildZOrder();
Log.d(TAG, "reload() - Ensure all items are loaded at max size.");
- ensureBufferItemsAtMaxSize();
+ renderAllThumbnails();
invalidate();
if (mListener != null) {
Log.d(TAG, "[fling] onScrollEnd() - Ensuring that items are at"
+ " full resolution.");
- ensureItemAtMaxSize(BUFFER_CENTER);
- ensureItemAtMaxSize(BUFFER_CENTER + 1);
- ensureItemAtMaxSize(BUFFER_CENTER - 1);
- ensureItemAtMaxSize(BUFFER_CENTER + 2);
+ renderThumbnail(BUFFER_CENTER);
+ renderThumbnail(BUFFER_CENTER + 1);
+ renderThumbnail(BUFFER_CENTER - 1);
+ renderThumbnail(BUFFER_CENTER + 2);
}
if (isCurrentItemCentered()
}
if (inFullScreen()) {
mController.zoomAt(current, x, y);
- ensureItemAtMaxSize(BUFFER_CENTER);
+ renderFullRes(BUFFER_CENTER);
return true;
} else if (mScale > FULL_SCREEN_SCALE) {
// In zoom view.
onLeaveZoomView();
}
mScale = newScale;
+ renderThumbnail(BUFFER_CENTER);
onEnterFilmstrip();
invalidate();
} else {
} else {
onEnterZoomView();
}
- ensureItemAtMaxSize(BUFFER_CENTER);
+ renderFullRes(BUFFER_CENTER);
}
return true;
}