From 6616551e7d9bf78a6b630893602e63379e81ef2b Mon Sep 17 00:00:00 2001 From: Angus Kong Date: Fri, 22 Feb 2013 14:02:25 -0800 Subject: Horizontal scrollable filmstrip view. Change-Id: I076a07cd9a949ecdc8e4499b171b64e7becdbef2 --- src/com/android/camera/data/CameraDataAdapter.java | 330 ++++++++++++++++ src/com/android/camera/ui/FilmStripView.java | 421 +++++++++++++++++++++ 2 files changed, 751 insertions(+) create mode 100644 src/com/android/camera/data/CameraDataAdapter.java create mode 100644 src/com/android/camera/ui/FilmStripView.java (limited to 'src') diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java new file mode 100644 index 000000000..5d10953f8 --- /dev/null +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2013 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.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.drawable.ColorDrawable; +import android.os.AsyncTask; +import android.provider.MediaStore; +import android.provider.MediaStore.Images; +import android.provider.MediaStore.Images.ImageColumns; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; + +import com.android.camera.Storage; +import com.android.camera.ui.FilmStripView; + +import java.util.ArrayList; +import java.util.List; + +/** + * A FilmStripDataProvider that provide data in the camera folder. + * + * The given view for camera preview won't be added until the preview info + * has been set by setPreviewInfo(int, int, int) + */ +public class CameraDataAdapter implements FilmStripView.DataAdapter { + private static final String TAG = "CamreaFilmStripDataProvider"; + + private static final int DEFAULT_DECODE_SIZE = 3000; + private static final String ORDER_CLAUSE = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + private static final String[] CAMERA_PATH = { Storage.DIRECTORY + "%" }; + private static final int COL_ID = 0; + private static final int COL_TITLE = 1; + private static final int COL_MIME_TYPE = 2; + private static final int COL_DATE_TAKEN = 3; + private static final int COL_DATE_MODIFIED = 4; + private static final int COL_DATA = 5; + private static final int COL_ORIENTATION = 6; + private static final int COL_WIDTH = 7; + private static final int COL_HEIGHT = 8; + private static final int COL_SIZE = 9; + + private static final String[] PROJECTION = { + ImageColumns._ID, // 0, int + ImageColumns.TITLE, // 1, string + ImageColumns.MIME_TYPE, // 2, tring + ImageColumns.DATE_TAKEN, // 3, int + ImageColumns.DATE_MODIFIED, // 4, int + ImageColumns.DATA, // 5, string + ImageColumns.ORIENTATION, // 6, int, 0, 90, 180, 270 + ImageColumns.WIDTH, // 7, int + ImageColumns.HEIGHT, // 8, int + ImageColumns.SIZE // 9, int + }; + + // 32K buffer. + private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; + + private List mImages; + + private FilmStripView mFilmStripView; + private View mCameraPreviewView; + private ColorDrawable mPlaceHolder; + + private int mSuggestedWidth = DEFAULT_DECODE_SIZE; + private int mSuggestedHeight = DEFAULT_DECODE_SIZE; + + public CameraDataAdapter(View cameraPreviewView, int placeHolderColor) { + mCameraPreviewView = cameraPreviewView; + mPlaceHolder = new ColorDrawable(placeHolderColor); + } + + public void setCameraPreviewInfo(int width, int height, int orientation) { + addOrReplaceCameraData(buildCameraImageData(width, height, orientation)); + } + + @Override + public int getTotalNumber() { + return mImages.size(); + } + + @Override + public FilmStripView.ImageData getImageData(int id) { + if (id >= mImages.size()) return null; + return mImages.get(id); + } + + @Override + public void suggestSize(int w, int h) { + if (w <= 0 || h <= 0) { + mSuggestedWidth = mSuggestedHeight = DEFAULT_DECODE_SIZE; + } else { + mSuggestedWidth = (w < DEFAULT_DECODE_SIZE ? w : DEFAULT_DECODE_SIZE); + mSuggestedHeight = (h < DEFAULT_DECODE_SIZE ? h : DEFAULT_DECODE_SIZE); + } + } + + @Override + public void requestLoad(ContentResolver resolver) { + QueryTask qtask = new QueryTask(); + qtask.execute(resolver); + } + + @Override + public View getView(Context c, int dataID) { + if (dataID >= mImages.size() || dataID < 0) { + return null; + } + + LocalImageData data = mImages.get(dataID); + + if (data.isCameraData) return mCameraPreviewView; + + ImageView v = new ImageView(c); + v.setImageDrawable(mPlaceHolder); + + v.setScaleType(ImageView.ScaleType.FIT_XY); + LoadBitmapTask task = new LoadBitmapTask(data, v); + task.execute(); + return v; + } + + @Override + public void setDataListener(FilmStripView v) { + mFilmStripView = v; + } + + private LocalImageData buildCameraImageData(int width, int height, int orientation) { + LocalImageData d = new LocalImageData(); + d.width = width; + d.height = height; + d.orientation = orientation; + d.isCameraData = true; + return d; + } + + private void addOrReplaceCameraData(LocalImageData data) { + if (mImages == null) mImages = new ArrayList(); + if (mImages.size() == 0) { + mImages.add(0, data); + return; + } + + LocalImageData first = mImages.get(0); + if (first.isCameraData) { + mImages.set(0, data); + } else { + mImages.add(0, data); + } + } + + private LocalImageData buildCursorImageData(Cursor c) { + LocalImageData d = new LocalImageData(); + d.id = c.getInt(COL_ID); + d.title = c.getString(COL_TITLE); + d.mimeType = c.getString(COL_MIME_TYPE); + d.path = c.getString(COL_DATA); + d.orientation = c.getInt(COL_ORIENTATION); + d.width = c.getInt(COL_WIDTH); + d.height = c.getInt(COL_HEIGHT); + if (d.width <= 0 || d.height <= 0) { + Log.v(TAG, "warning! zero dimension for " + + d.path + ":" + d.width + "x" + d.height); + Dimension dim = decodeDimension(d.path); + if (dim != null) { + d.width = dim.width; + d.height = dim.height; + } else { + Log.v(TAG, "warning! dimension decode failed for " + d.path); + Bitmap b = BitmapFactory.decodeFile(d.path); + if (b == null) return null; + d.width = b.getWidth(); + d.height = b.getHeight(); + } + } + if (d.orientation == 90 || d.orientation == 270) { + int b = d.width; + d.width = d.height; + d.height = b; + } + return d; + } + + private Dimension decodeDimension(String path) { + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inJustDecodeBounds = true; + Bitmap b = BitmapFactory.decodeFile(path, opts); + if (b == null) return null; + Dimension d = new Dimension(); + d.width = opts.outWidth; + d.height = opts.outHeight; + return d; + } + + private class Dimension { + public int width; + public int height; + } + + private class LocalImageData implements FilmStripView.ImageData { + public boolean isCameraData; + public int id; + public String title; + public String mimeType; + public String path; + // from MediaStore, can only be 0, 90, 180, 270; + public int orientation; + // width and height should be adjusted according to orientation. + public int width; + public int height; + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public String toString() { + return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",orientation=" + orientation; + } + } + + private class QueryTask extends AsyncTask> { + private ContentResolver mResolver; + private LocalImageData mCameraImageData; + + @Override + protected List doInBackground(ContentResolver... resolver) { + List l = null; + Cursor c = resolver[0].query(Images.Media.EXTERNAL_CONTENT_URI, PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + ORDER_CLAUSE); + if (c == null) return null; + l = new ArrayList(); + c.moveToFirst(); + while (!c.isLast()) { + LocalImageData data = buildCursorImageData(c); + if (data != null) l.add(data); + else Log.e(TAG, "Error decoding file:" + c.getString(COL_DATA)); + c.moveToNext(); + } + c.close(); + return l; + } + + @Override + protected void onPostExecute(List l) { + boolean changed = (l != mImages); + LocalImageData first = null; + if (mImages != null && mImages.size() > 0) { + first = mImages.get(0); + if (!first.isCameraData) first = null; + } + mImages = l; + if (first != null) addOrReplaceCameraData(first); + // both might be null. + if (changed) mFilmStripView.onDataChanged(); + } + } + + private class LoadBitmapTask extends AsyncTask { + private LocalImageData mData; + private ImageView mView; + + public LoadBitmapTask( + LocalImageData d, ImageView v) { + mData = d; + mView = v; + } + + @Override + protected Bitmap doInBackground(Void... v) { + BitmapFactory.Options opts = null; + Bitmap b; + int sample = 1; + while (mSuggestedWidth * sample < mData.width + || mSuggestedHeight * sample < mData.height) { + sample *= 2; + } + opts = new BitmapFactory.Options(); + opts.inSampleSize = sample; + opts.inTempStorage = DECODE_TEMP_STORAGE; + if (isCancelled()) return null; + b = BitmapFactory.decodeFile(mData.path, opts); + if (mData.orientation != 0) { + if (isCancelled()) return null; + Matrix m = new Matrix(); + m.setRotate((float) mData.orientation); + b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); + } + return b; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Cannot decode bitmap file:" + mData.path); + return; + } + mView.setImageBitmap(bitmap); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java new file mode 100644 index 000000000..326e969b2 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripView.java @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2013 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.ui; + +import android.content.ContentResolver; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.Log; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Scroller; + +public class FilmStripView extends ViewGroup { + + private static final String TAG = "FilmStripView"; + private static final int BUFFER_SIZE = 5; + // Horizontal padding of children. + private static final int H_PADDING = 50; + // Duration to go back to the first. + private static final int BACK_SCROLL_DURATION = 500; + private static final float MIN_SCALE = 0.7f; + + private Context mContext; + private GestureDetector mGestureDetector; + private DataAdapter mDataAdapter; + private final Rect mDrawArea = new Rect(); + + private int mCurrentInfo; + private Scroller mScroller; + private boolean mIsScrolling; + private int mCenterPosition = -1; + private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; + + public interface ImageData { + // The values returned by getWidth() and getHeight() will be used for layout. + public int getWidth(); + public int getHeight(); + } + + public interface DataAdapter { + + public int getTotalNumber(); + public View getView(Context context, int id); + public ImageData getImageData(int id); + public void suggestSize(int w, int h); + + public void requestLoad(ContentResolver r); + public void setDataListener(FilmStripView v); + } + + private static class ViewInfo { + private int mDataID; + // the position of the left of the view in the whole filmstrip. + private int mLeftPosition; + private View mView; + + public ViewInfo(int id, View v) { + mDataID = id; + mView = v; + mLeftPosition = -1; + } + + public int getId() { + return mDataID; + } + + public void setLeftPosition(int pos) { + mLeftPosition = pos; + } + + public int getLeftPosition() { + return mLeftPosition; + } + + public int getCenterPosition() { + return mLeftPosition + mView.getWidth() / 2; + } + + public View getView() { + return mView; + } + + private void layoutAt(int l, int t) { + mView.layout(l, t, l + mView.getMeasuredWidth(), t + mView.getMeasuredHeight()); + } + + public void layoutIn(Rect drawArea, int refCenter) { + // drawArea is where to layout in. + // refCenter is the absolute horizontal position of the center of drawArea. + layoutAt(drawArea.centerX() + mLeftPosition - refCenter, + drawArea.centerY() - mView.getMeasuredHeight() / 2); + } + } + + public FilmStripView(Context context) { + super(context); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public FilmStripView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context); + } + + private void init(Context context) { + mCurrentInfo = (BUFFER_SIZE - 1) / 2; + setWillNotDraw(false); + mContext = context; + mScroller = new Scroller(context); + mGestureDetector = + new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + } + + @Override + public void onDraw(Canvas c) { + if (mIsScrolling) { + layoutChildren(); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int w = MeasureSpec.getSize(widthMeasureSpec); + int h = MeasureSpec.getSize(heightMeasureSpec); + float scale = MIN_SCALE; + if (mDataAdapter != null) mDataAdapter.suggestSize(w / 2, h / 2); + + int boundWidth = (int) (w * scale); + int boundHeight = (int) (h * scale); + + int wMode = View.MeasureSpec.EXACTLY; + int hMode = View.MeasureSpec.EXACTLY; + + for (int i = 0; i < mViewInfo.length; i++) { + ViewInfo info = mViewInfo[i]; + if (mViewInfo[i] == null) continue; + + int imageWidth = mDataAdapter.getImageData(info.getId()).getWidth(); + int imageHeight = mDataAdapter.getImageData(info.getId()).getHeight(); + + int scaledWidth = boundWidth; + int scaledHeight = boundHeight; + if (imageWidth * scaledHeight > scaledWidth * imageHeight) { + scaledHeight = imageHeight * scaledWidth / imageWidth; + } else { + scaledWidth = imageWidth * scaledHeight / imageHeight; + } + scaledWidth += H_PADDING * 2 * scale; + mViewInfo[i].getView().measure( + View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode) + , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode)); + } + setMeasuredDimension(w, h); + } + + private int findTheNearestView(int pointX) { + + int nearest = 0; + // find the first non-null ViewInfo. + for (; nearest < BUFFER_SIZE + && (mViewInfo[nearest] == null || mViewInfo[nearest].getLeftPosition() == -1); + nearest++); + // no existing available ViewInfo + if (nearest == BUFFER_SIZE) return -1; + int min = Math.abs(pointX - mViewInfo[nearest].getCenterPosition()); + + for (int infoID = nearest + 1; + infoID < BUFFER_SIZE && mViewInfo[infoID] != null; infoID++) { + // not measured yet. + if (mViewInfo[infoID].getLeftPosition() == -1) continue; + + int c = mViewInfo[infoID].getCenterPosition(); + int dist = Math.abs(pointX - c); + if (dist < min) { + min = dist; + nearest = infoID; + } + } + return nearest; + } + + // We try to keep the one closest to the center of the screen at position mCurrentInfo. + private void stepIfNeeded() { + int nearest = findTheNearestView(mCenterPosition); + // no change made. + if (nearest == -1 || nearest == mCurrentInfo) return; + + int adjust = nearest - mCurrentInfo; + if (adjust > 0) { + for (int k = 0; k < adjust; k++) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = 0; k + adjust < BUFFER_SIZE; k++) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = BUFFER_SIZE - adjust; k < BUFFER_SIZE; k++) { + mViewInfo[k] = null; + if (mViewInfo[k - 1] != null) getInfo(k, mViewInfo[k - 1].getId() + 1); + } + } else { + for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { + if (mViewInfo[k] != null) { + removeView(mViewInfo[k].getView()); + } + } + for (int k = BUFFER_SIZE - 1; k + adjust >= 0; k--) { + mViewInfo[k] = mViewInfo[k + adjust]; + } + for (int k = -1 - adjust; k >= 0; k--) { + mViewInfo[k] = null; + if (mViewInfo[k + 1] != null) getInfo(k, mViewInfo[k + 1].getId() - 1); + } + } + } + + private void stopScroll() { + mScroller.forceFinished(true); + mIsScrolling = false; + } + + private void adjustCenterPosition() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return; + + if (curr.getId() == 0 && mCenterPosition < curr.getCenterPosition()) { + mCenterPosition = curr.getCenterPosition(); + if (mIsScrolling) stopScroll(); + } + if (curr.getId() == mDataAdapter.getTotalNumber() - 1 + && mCenterPosition > curr.getCenterPosition()) { + mCenterPosition = curr.getCenterPosition(); + if (mIsScrolling) stopScroll(); + } + } + + private void layoutChildren() { + mIsScrolling = mScroller.computeScrollOffset(); + + if (mIsScrolling) mCenterPosition = mScroller.getCurrX(); + + adjustCenterPosition(); + + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition); + + // images on the left + for (int infoID = mCurrentInfo - 1; infoID >= 0; infoID--) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo next = mViewInfo[infoID + 1]; + curr.setLeftPosition(next.getLeftPosition() - curr.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition); + } + } + + // images on the right + for (int infoID = mCurrentInfo + 1; infoID < BUFFER_SIZE; infoID++) { + ViewInfo curr = mViewInfo[infoID]; + if (curr != null) { + ViewInfo prev = mViewInfo[infoID - 1]; + curr.setLeftPosition(prev.getLeftPosition() + prev.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition); + } + } + + stepIfNeeded(); + invalidate(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mViewInfo[mCurrentInfo] == null) return; + + mDrawArea.left = l; + mDrawArea.top = t; + mDrawArea.right = r; + mDrawArea.bottom = b; + + layoutChildren(); + } + + public void setDataAdapter( + DataAdapter adapter, ContentResolver resolver) { + mDataAdapter = adapter; + mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight()); + mDataAdapter.setDataListener(this); + mDataAdapter.requestLoad(resolver); + } + + private void getInfo(int infoID, int dataID) { + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) return; + v.setPadding(H_PADDING, 0, H_PADDING, 0); + addView(v); + ViewInfo info = new ViewInfo(dataID, v); + mViewInfo[infoID] = info; + } + + public void onDataChanged() { + removeAllViews(); + int dataNumber = mDataAdapter.getTotalNumber(); + if (dataNumber == 0) return; + + int currentData = 0; + int currentLeft = 0; + // previous data exists. + if (mViewInfo[mCurrentInfo] != null) { + currentLeft = mViewInfo[mCurrentInfo].getLeftPosition(); + currentData = mViewInfo[mCurrentInfo].getId(); + } + getInfo(mCurrentInfo, currentData); + mViewInfo[mCurrentInfo].setLeftPosition(currentLeft); + for (int i = 1; mCurrentInfo + i < BUFFER_SIZE || mCurrentInfo - i >= 0; i++) { + int infoID = mCurrentInfo + i; + if (infoID < BUFFER_SIZE && mViewInfo[infoID - 1] != null) { + getInfo(infoID, mViewInfo[infoID - 1].getId() + 1); + } + infoID = mCurrentInfo - i; + if (infoID >= 0 && mViewInfo[infoID + 1] != null) { + getInfo(infoID, mViewInfo[infoID + 1].getId() - 1); + } + } + layoutChildren(); + } + + private void movePositionTo(int position) { + mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, + 0, BACK_SCROLL_DURATION); + layoutChildren(); + } + + public void goToFirst() { + movePositionTo(0); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mGestureDetector.onTouchEvent(ev); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + + @Override + public boolean onDoubleTap(MotionEvent e) { + float x = (float) e.getX(); + float y = (float) e.getY(); + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) continue; + View v = mViewInfo[i].getView(); + if (x >= v.getLeft() && x < v.getRight() + && y >= v.getTop() && y < v.getBottom()) { + Log.v(TAG, "l, r, t, b " + v.getLeft() + ',' + v.getRight() + + ',' + v.getTop() + ',' + v.getBottom()); + movePositionTo(mViewInfo[i].getCenterPosition()); + break; + } + } + return true; + } + + @Override + public boolean onDown(MotionEvent ev) { + if (mIsScrolling) stopScroll(); + return true; + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + stopScroll(); + mCenterPosition += dx; + layoutChildren(); + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + ViewInfo info = mViewInfo[mCurrentInfo]; + int w = getWidth(); + if (info == null) return true; + mScroller.fling(mCenterPosition, 0, (int) -velocityX, (int) velocityY, + // estimation of possible length on the left + info.getLeftPosition() - info.getId() * w * 2, + // estimation of possible length on the right + info.getLeftPosition() + + (mDataAdapter.getTotalNumber() - info.getId()) * w * 2, + 0, 0); + layoutChildren(); + return true; + } + } +} -- cgit v1.2.3