Index: chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..f33c119bb520c1f5274d28b4581024dd8a26d03a |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/photo_picker/PickerCategoryView.java |
@@ -0,0 +1,370 @@ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+package org.chromium.chrome.browser.photo_picker; |
+ |
+import android.content.Context; |
+import android.graphics.Bitmap; |
+import android.graphics.BitmapFactory; |
+import android.graphics.Canvas; |
+import android.graphics.Paint; |
+import android.graphics.PorterDuff; |
+import android.graphics.PorterDuffColorFilter; |
+import android.graphics.Rect; |
+import android.support.v4.content.ContextCompat; |
+import android.support.v7.widget.DefaultItemAnimator; |
+import android.support.v7.widget.GridLayoutManager; |
+import android.support.v7.widget.RecyclerView; |
+import android.util.AttributeSet; |
+import android.util.LruCache; |
+import android.view.View; |
+import android.widget.RelativeLayout; |
+ |
+import org.chromium.base.VisibleForTesting; |
+import org.chromium.chrome.R; |
+import org.chromium.chrome.browser.widget.selection.SelectionDelegate; |
+import org.chromium.ui.PhotoPickerListener; |
+ |
+import java.util.ArrayList; |
+import java.util.Collections; |
+import java.util.List; |
+import java.util.Map; |
+ |
+/** |
+ * A class for keeping track of common data associated with showing photos in |
+ * the photo picker, for example the RecyclerView and the bitmap caches. |
+ */ |
+public class PickerCategoryView extends RelativeLayout |
+ implements FileEnumWorkerTask.FilesEnumeratedCallback, |
+ DecoderServiceHost.ServiceReadyCallback, RecyclerView.RecyclerListener { |
+ // Our context. |
+ private Context mContext; |
+ |
+ // The list of images on disk, sorted by last-modified first. |
+ private List<PickerBitmap> mPickerBitmaps; |
+ |
+ // True if multi-selection is allowed in the picker. |
+ private boolean mMultiSelection; |
+ |
+ // The callback to notify the listener of decisions reached in the picker. |
+ private PhotoPickerListener mListener; |
+ |
+ // The host class for the decoding service. |
+ private DecoderServiceHost mDecoderServiceHost; |
+ |
+ // The recycler view showing the images. |
+ private RecyclerView mRecyclerView; |
+ |
+ // The picker adapter for the RecyclerView. |
+ private PickerAdapter mPickerAdapter; |
+ |
+ // The selection delegate keeping track of which images are selected. |
+ private SelectionDelegate<PickerBitmap> mSelectionDelegate; |
+ |
+ // A low-resolution cache for images. Helpful for cache misses from the high-resolution cache |
+ // to avoid showing gray squares (show pixelated versions instead until image can be loaded off |
+ // disk). |
+ private LruCache<String, Bitmap> mLowResBitmaps; |
+ |
+ // A high-resolution cache for images. |
+ private LruCache<String, Bitmap> mHighResBitmaps; |
+ |
+ // The number of columns to show. Note: mColumns and mPadding (see below) should both be even |
+ // numbers or both odd, not a mix (the column padding will not be of uniform thickness if they |
+ // are a mix). |
+ private int mColumns; |
+ |
+ // The padding between columns. See also comment for mColumns. |
+ private int mPadding; |
+ |
+ // The size of the bitmaps (equal length for width and height). |
+ private int mImageSize; |
+ |
+ // The control to show for when an image is selected. |
+ private Bitmap mBitmapSelected; |
+ |
+ // The control to show for when an image is not selected. |
+ private Bitmap mBitmapUnselected; |
+ |
+ // A worker task for asynchronously enumerating files off the main thread. |
+ private FileEnumWorkerTask mWorkerTask; |
+ |
+ public PickerCategoryView(Context context) { |
+ super(context); |
+ init(context); |
+ } |
+ |
+ public PickerCategoryView(Context context, AttributeSet attrs) { |
+ super(context, attrs); |
+ init(context); |
+ } |
+ |
+ public PickerCategoryView(Context context, AttributeSet attrs, int defStyle) { |
+ super(context, attrs, defStyle); |
+ init(context); |
+ } |
+ |
+ /** |
+ * A helper function for initializing the PickerCategoryView. |
+ * @param context The context to use. |
+ */ |
+ private void init(Context context) { |
+ mContext = context; |
+ |
+ mDecoderServiceHost = new DecoderServiceHost(this); |
+ mDecoderServiceHost.bind(mContext); |
+ |
+ if (((mColumns % 2) == 0) != ((mPadding % 2) == 0)) { |
+ throw new AssertionError("Columns and padding should both be odd or both even"); |
+ } |
+ inflate(mContext, R.layout.picker_category_view, this); |
+ } |
+ |
+ /** |
+ * Severs the connection to the decoding utility process. |
+ */ |
+ public void endConnection() { |
+ if (mDecoderServiceHost != null) { |
+ mDecoderServiceHost.unbind(mContext); |
+ } |
+ } |
+ |
+ /** |
+ * Sets the starting state for the PickerCategoryView object. |
+ * @param selectionDelegate The selection delegate to use. |
+ * @param listener The listener who should be notified of actions. |
+ * @param multiSelection Whether to allow the user to select more than one image. |
+ * @param width The width of the dialog showing the photos. |
+ */ |
+ public void setStartingState(SelectionDelegate<PickerBitmap> selectionDelegate, |
+ PhotoPickerListener listener, boolean multiSelection, int width) { |
+ mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); |
+ mRecyclerView.setRecyclerListener(this); |
+ mSelectionDelegate = selectionDelegate; |
+ mMultiSelection = multiSelection; |
+ mListener = listener; |
+ |
+ mBitmapSelected = BitmapFactory.decodeResource( |
+ mContext.getResources(), R.drawable.ic_check_circle_black_24dp); |
+ mBitmapUnselected = BitmapFactory.decodeResource( |
+ mContext.getResources(), R.drawable.ic_radio_button_unchecked_black_24dp); |
+ |
+ // Apply color to the bitmaps. |
+ int prefAccentColor = ContextCompat.getColor(mContext, R.color.pref_accent_color); |
+ mBitmapSelected = colorBitmap(mBitmapSelected, prefAccentColor); |
+ int unselectedColor = ContextCompat.getColor(mContext, R.color.white_mode_tint); |
+ mBitmapUnselected = colorBitmap(mBitmapUnselected, unselectedColor); |
+ |
+ calculateGridMetrics(width); |
+ |
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); |
+ final int cacheSizeLarge = maxMemory / 2; // 1/2 of the available memory. |
+ final int cacheSizeSmall = maxMemory / 8; // 1/8th of the available memory. |
+ mLowResBitmaps = new LruCache<String, Bitmap>(cacheSizeSmall) { |
+ @Override |
+ protected int sizeOf(String key, Bitmap bitmap) { |
+ return bitmap.getByteCount() / 1024; |
+ } |
+ }; |
+ mHighResBitmaps = new LruCache<String, Bitmap>(cacheSizeLarge) { |
+ @Override |
+ protected int sizeOf(String key, Bitmap bitmap) { |
+ return bitmap.getByteCount() / 1024; |
+ } |
+ }; |
+ |
+ mPickerAdapter = new PickerAdapter(this); |
+ RecyclerView.LayoutManager mLayoutManager = new GridLayoutManager(mContext, mColumns); |
+ mRecyclerView.setHasFixedSize(true); |
+ mRecyclerView.setLayoutManager(mLayoutManager); |
+ mRecyclerView.setItemAnimator(new DefaultItemAnimator()); |
+ mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(mColumns, mPadding)); |
+ } |
+ |
+ // DecoderServiceHost.ServiceReadyCallback: |
+ |
+ @Override |
+ public void serviceReady() { |
+ prepareBitmaps(); |
+ } |
+ |
+ // FileEnumWorkerTask.FilesEnumeratedCallback: |
+ |
+ @Override |
+ public void filesEnumeratedCallback(List<PickerBitmap> files) { |
+ mPickerBitmaps = files; |
+ if (files != null && files.size() > 0) { |
+ mRecyclerView.setAdapter(mPickerAdapter); |
+ } else { |
+ setVisibility(View.GONE); |
+ } |
+ } |
+ |
+ // RecyclerView.RecyclerListener: |
+ |
+ @Override |
+ public void onViewRecycled(RecyclerView.ViewHolder holder) { |
+ PickerBitmapViewHolder bitmapHolder = (PickerBitmapViewHolder) holder; |
+ String filePath = bitmapHolder.getFilePath(); |
+ if (filePath != null) { |
+ getDecoderServiceHost().cancelDecodeImage(filePath); |
+ } |
+ } |
+ |
+ // Simple accessors: |
+ |
+ public int getImageSize() { |
+ return mImageSize; |
+ } |
+ public SelectionDelegate<PickerBitmap> getSelectionDelegate() { |
+ return mSelectionDelegate; |
+ } |
+ public List<PickerBitmap> getPickerBitmaps() { |
+ return mPickerBitmaps; |
+ } |
+ public DecoderServiceHost getDecoderServiceHost() { |
+ return mDecoderServiceHost; |
+ } |
+ public LruCache<String, Bitmap> getLowResBitmaps() { |
+ return mLowResBitmaps; |
+ } |
+ public LruCache<String, Bitmap> getHighResBitmaps() { |
+ return mHighResBitmaps; |
+ } |
+ public boolean isMultiSelect() { |
+ return mMultiSelection; |
+ } |
+ |
+ /** |
+ * Notifies the caller that the user selected to launch the gallery. |
+ */ |
+ public void showGallery() { |
+ mListener.onPickerUserAction(PhotoPickerListener.Action.LAUNCH_GALLERY, null); |
+ } |
+ |
+ /** |
+ * Notifies the caller that the user selected to launch the camera intent. |
+ */ |
+ public void showCamera() { |
+ mListener.onPickerUserAction(PhotoPickerListener.Action.LAUNCH_CAMERA, null); |
+ } |
+ |
+ /** |
+ * Returns the selection bitmaps (control indicating whether the image is selected or not). |
+ * @param selected See return value. |
+ * @return If |selected| is true, the selection bitmap is returned. Otherwise the unselection |
+ * bitmap is returned. |
+ */ |
+ public Bitmap getSelectionBitmap(boolean selected) { |
+ if (selected) { |
+ return mBitmapSelected; |
+ } else { |
+ return mBitmapUnselected; |
+ } |
+ } |
+ |
+ /** |
+ * Applies a color filter to a bitmap. |
+ * @param original The bitmap to color. |
+ * @param color The color to apply. |
+ * @return A colored bitmap. |
+ */ |
+ private Bitmap colorBitmap(Bitmap original, int color) { |
+ Bitmap mutable = original.copy(Bitmap.Config.ARGB_8888, true); |
+ Canvas canvas = new Canvas(mutable); |
+ Paint paint = new Paint(); |
+ paint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); |
+ canvas.drawBitmap(mutable, 0.f, 0.f, paint); |
+ return mutable; |
+ } |
+ |
+ /** |
+ * Calculates image size and how many columns can fit on-screen. |
+ * @param width The total width of the boundary to show the images in. |
+ */ |
+ private void calculateGridMetrics(int width) { |
+ int minSize = |
+ mContext.getResources().getDimensionPixelSize(R.dimen.file_picker_tile_min_size); |
+ mPadding = mContext.getResources().getDimensionPixelSize(R.dimen.file_picker_tile_gap); |
+ mColumns = Math.max(1, (width - mPadding) / (minSize + mPadding)); |
+ mImageSize = (width - mPadding * (mColumns + 1)) / (mColumns); |
+ } |
+ |
+ @VisibleForTesting |
+ public RecyclerView getRecyclerViewForTesting() { |
+ return mRecyclerView; |
+ } |
+ |
+ @VisibleForTesting |
+ private boolean loadTestFiles() { |
+ if (mListener == null) { |
+ return false; |
+ } |
+ Map<String, Long> testFiles = mListener.getFilesForTesting(); |
+ if (testFiles == null) { |
+ return false; |
+ } |
+ |
+ List<PickerBitmap> files = new ArrayList<>(); |
+ for (Map.Entry<String, Long> entry : testFiles.entrySet()) { |
+ String key = entry.getKey(); |
+ Long value = entry.getValue(); |
+ files.add(new PickerBitmap(key, value, PickerBitmap.TileTypes.PICTURE)); |
+ } |
+ Collections.sort(files); |
+ filesEnumeratedCallback(files); |
+ return true; |
+ } |
+ |
+ /** |
+ * Prepares bitmaps for loading. |
+ */ |
+ private void prepareBitmaps() { |
+ if (loadTestFiles()) return; |
+ |
+ if (mWorkerTask != null) { |
+ mWorkerTask.cancel(true); |
+ } |
+ |
+ mWorkerTask = new FileEnumWorkerTask(this, AcceptFileFilter.forAttr("image/*,video/*")); |
+ mWorkerTask.execute(); |
+ } |
+ |
+ /** |
+ * A class for implementing grid spacing between items. |
+ */ |
+ private class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { |
+ // The number of spans to account for. |
+ private int mSpanCount; |
+ |
+ // The amount of spacing to use. |
+ private int mSpacing; |
+ |
+ public GridSpacingItemDecoration(int spanCount, int spacing) { |
+ mSpanCount = spanCount; |
+ mSpacing = spacing; |
+ } |
+ |
+ @Override |
+ public void getItemOffsets( |
+ Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { |
+ int left = 0, right = 0, top = 0, bottom = 0; |
+ int position = parent.getChildAdapterPosition(view); |
+ |
+ if (position >= 0) { |
+ int column = position % mSpanCount; |
+ |
+ left = mSpacing - column * mSpacing / mSpanCount; |
+ right = (column + 1) * mSpacing / mSpanCount; |
+ |
+ if (position < mSpanCount) { |
+ top = mSpacing; |
+ } |
+ bottom = mSpacing; |
+ } |
+ |
+ outRect.set(left, top, right, bottom); |
+ } |
+ } |
+} |