diff options
Diffstat (limited to 'src')
38 files changed, 2563 insertions, 378 deletions
diff --git a/src/com/android/camera/CameraManager.java b/src/com/android/camera/CameraManager.java index b354654b6..af4b13f29 100644 --- a/src/com/android/camera/CameraManager.java +++ b/src/com/android/camera/CameraManager.java @@ -72,6 +72,7 @@ public class CameraManager { private static final int SET_PREVIEW_DISPLAY_ASYNC = 22; private static final int SET_PREVIEW_CALLBACK = 23; private static final int ENABLE_SHUTTER_SOUND = 24; + private static final int REFRESH_PARAMETERS = 25; private Handler mCameraHandler; private android.hardware.Camera mCamera; @@ -247,6 +248,10 @@ public class CameraManager { enableShutterSound((msg.arg1 == 1) ? true : false); return; + case REFRESH_PARAMETERS: + mParametersIsDirty = true; + return; + default: throw new RuntimeException("Invalid CameraProxy message=" + msg.what); } @@ -446,6 +451,10 @@ public class CameraManager { return mParameters; } + public void refreshParameters() { + mCameraHandler.sendEmptyMessage(REFRESH_PARAMETERS); + } + public void enableShutterSound(boolean enable) { mCameraHandler.obtainMessage( ENABLE_SHUTTER_SOUND, (enable ? 1 : 0), 0).sendToTarget(); diff --git a/src/com/android/camera/NewPhotoModule.java b/src/com/android/camera/NewPhotoModule.java index dfa1e0cc4..e9e60d8a9 100644 --- a/src/com/android/camera/NewPhotoModule.java +++ b/src/com/android/camera/NewPhotoModule.java @@ -64,7 +64,7 @@ import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.exif.Rational; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.filtershow.FilterShowActivity; import com.android.gallery3d.util.UsageStatistics; diff --git a/src/com/android/camera/NewVideoModule.java b/src/com/android/camera/NewVideoModule.java index 3fc748593..f8c36c526 100644 --- a/src/com/android/camera/NewVideoModule.java +++ b/src/com/android/camera/NewVideoModule.java @@ -1507,6 +1507,9 @@ public class NewVideoModule implements NewCameraModule, AccessibilityUtils.makeAnnouncement(mUI.getShutterButton(), mActivity.getString(R.string.video_recording_started)); + // The parameters might have been altered by MediaRecorder already. + // We need to force mCameraDevice to refresh before getting it. + mCameraDevice.refreshParameters(); // The parameters may have been changed by MediaRecorder upon starting // recording. We need to alter the parameters if we support camcorder // zoom. To reduce latency when setting the parameters during zoom, we diff --git a/src/com/android/camera/PhotoModule.java b/src/com/android/camera/PhotoModule.java index 22bd6503f..8759c19d9 100644 --- a/src/com/android/camera/PhotoModule.java +++ b/src/com/android/camera/PhotoModule.java @@ -64,8 +64,8 @@ import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.exif.ExifInterface; import com.android.gallery3d.exif.ExifTag; import com.android.gallery3d.exif.Rational; -import com.android.gallery3d.filtershow.CropExtras; import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.util.UsageStatistics; import java.io.File; @@ -590,6 +590,7 @@ public class PhotoModule if (mFocusManager != null) mFocusManager.setPreviewSize(width, height); ((CameraScreenNail) mActivity.mCameraScreenNail).setPreviewFrameLayoutSize( previewWidth, previewHeight); + mActivity.notifyScreenNailChanged(); } private void resetExposureCompensation() { diff --git a/src/com/android/camera/PhotoUI.java b/src/com/android/camera/PhotoUI.java index 29ebd6a70..53fdc969e 100644 --- a/src/com/android/camera/PhotoUI.java +++ b/src/com/android/camera/PhotoUI.java @@ -144,7 +144,6 @@ public class PhotoUI implements PieListener, } } - mRootView.addOnLayoutChangeListener(mLayoutListener); } public View getRootView() { @@ -259,6 +258,7 @@ public class PhotoUI implements PieListener, mShutterButton.setImageResource(R.drawable.btn_new_shutter); mShutterButton.setOnShutterButtonListener(mController); mShutterButton.setVisibility(View.VISIBLE); + mRootView.addOnLayoutChangeListener(mLayoutListener); } // called from onResume every other time @@ -270,6 +270,7 @@ public class PhotoUI implements PieListener, if (mMenu != null) { mMenu.reloadPreferences(); } + mRootView.addOnLayoutChangeListener(mLayoutListener); } public void initializeZoom(Camera.Parameters params) { diff --git a/src/com/android/camera/VideoModule.java b/src/com/android/camera/VideoModule.java index 09a406ce5..2b30ba48f 100644 --- a/src/com/android/camera/VideoModule.java +++ b/src/com/android/camera/VideoModule.java @@ -1532,6 +1532,9 @@ public class VideoModule implements CameraModule, AccessibilityUtils.makeAnnouncement(mActivity.getShutterButton(), mActivity.getString(R.string.video_recording_started)); + // The parameters might have been altered by MediaRecorder already. + // We need to force mCameraDevice to refresh before getting it. + mActivity.mCameraDevice.refreshParameters(); // The parameters may have been changed by MediaRecorder upon starting // recording. We need to alter the parameters if we support camcorder // zoom. To reduce latency when setting the parameters during zoom, we diff --git a/src/com/android/camera/data/CameraDataAdapter.java b/src/com/android/camera/data/CameraDataAdapter.java index 5d10953f8..a55cc9d52 100644 --- a/src/com/android/camera/data/CameraDataAdapter.java +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Matrix; import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.provider.MediaStore; import android.provider.MediaStore.Images; @@ -33,6 +34,7 @@ import android.widget.ImageView; import com.android.camera.Storage; import com.android.camera.ui.FilmStripView; +import com.android.camera.ui.FilmStripView.ImageData; import java.util.ArrayList; import java.util.List; @@ -47,39 +49,11 @@ 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<LocalImageData> mImages; - - private FilmStripView mFilmStripView; + + private List<LocalData> mImages; + + private Listener mListener; private View mCameraPreviewView; private ColorDrawable mPlaceHolder; @@ -91,8 +65,8 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { mPlaceHolder = new ColorDrawable(placeHolderColor); } - public void setCameraPreviewInfo(int width, int height, int orientation) { - addOrReplaceCameraData(buildCameraImageData(width, height, orientation)); + public void setCameraPreviewInfo(int width, int height) { + addOrReplaceCameraData(buildCameraImageData(width, height)); } @Override @@ -101,7 +75,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override - public FilmStripView.ImageData getImageData(int id) { + public ImageData getImageData(int id) { if (id >= mImages.size()) return null; return mImages.get(id); } @@ -116,7 +90,6 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } } - @Override public void requestLoad(ContentResolver resolver) { QueryTask qtask = new QueryTask(); qtask.execute(resolver); @@ -128,107 +101,87 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { 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; + return mImages.get(dataID).getView( + c, mSuggestedWidth, mSuggestedHeight, mPlaceHolder); } @Override - public void setDataListener(FilmStripView v) { - mFilmStripView = v; + public void setListener(Listener listener) { + mListener = listener; } - 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; + private LocalData buildCameraImageData(int width, int height) { + LocalData d = new CameraPreviewData(width, height); return d; } - private void addOrReplaceCameraData(LocalImageData data) { - if (mImages == null) mImages = new ArrayList<LocalImageData>(); + private void addOrReplaceCameraData(LocalData data) { + if (mImages == null) mImages = new ArrayList<LocalData>(); if (mImages.size() == 0) { mImages.add(0, data); return; } - LocalImageData first = mImages.get(0); - if (first.isCameraData) { + LocalData first = mImages.get(0); + if (first.getType() == ImageData.TYPE_CAMERA_PREVIEW) { 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(); + private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalData>> { + @Override + protected List<LocalData> doInBackground(ContentResolver... resolver) { + List<LocalData> l = null; + Cursor c = resolver[0].query( + Images.Media.EXTERNAL_CONTENT_URI, + LocalPhotoData.QUERY_PROJECTION, + MediaStore.Images.Media.DATA + " like ? ", CAMERA_PATH, + LocalPhotoData.QUERY_ORDER); + if (c == null) return null; + + // build up the list. + l = new ArrayList<LocalData>(); + c.moveToFirst(); + while (!c.isLast()) { + LocalData data = LocalPhotoData.buildFromCursor(c); + if (data != null) { + l.add(data); + } else { + Log.e(TAG, "Error decoding file:" + + c.getString(LocalPhotoData.COL_DATA)); + } + c.moveToNext(); } + c.close(); + return l; } - 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; + @Override + protected void onPostExecute(List<LocalData> l) { + boolean changed = (l != mImages); + LocalData first = null; + if (mImages != null && mImages.size() > 0) { + first = mImages.get(0); + if (first.getType() != ImageData.TYPE_CAMERA_PREVIEW) first = null; + } + mImages = l; + if (first != null) addOrReplaceCameraData(first); + // both might be null. + if (changed) mListener.onDataLoaded(); + } } - private class LocalImageData implements FilmStripView.ImageData { - public boolean isCameraData; + private abstract static class LocalData implements FilmStripView.ImageData { 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; + public int supportedAction; @Override public int getWidth() { @@ -241,90 +194,194 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override - public String toString() { - return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType - + "," + width + "x" + height + ",orientation=" + orientation; + public boolean isActionSupported(int action) { + return false; } + + @Override + abstract public int getType(); + + abstract View getView(Context c, int width, int height, Drawable placeHolder); } - private class QueryTask extends AsyncTask<ContentResolver, Void, List<LocalImageData>> { - private ContentResolver mResolver; - private LocalImageData mCameraImageData; + private class CameraPreviewData extends LocalData { + private int mWidth; + private int mHeight; + + CameraPreviewData(int w, int h) { + mWidth = w; + mHeight = h; + } @Override - protected List<LocalImageData> doInBackground(ContentResolver... resolver) { - List<LocalImageData> 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<LocalImageData>(); - 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; + public int getWidth() { + return mWidth; } @Override - protected void onPostExecute(List<LocalImageData> 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(); + public int getHeight() { + return mHeight; + } + + @Override + public int getType() { + return ImageData.TYPE_CAMERA_PREVIEW; + } + + @Override + View getView(Context c, int width, int height, Drawable placeHolder) { + return mCameraPreviewView; } } - private class LoadBitmapTask extends AsyncTask<Void, Void, Bitmap> { - private LocalImageData mData; - private ImageView mView; + private static class LocalPhotoData extends LocalData { + static final String QUERY_ORDER = ImageColumns.DATE_TAKEN + " DESC, " + + ImageColumns._ID + " DESC"; + static final String[] QUERY_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 + }; + + 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; + + + // 32K buffer. + private static final byte[] DECODE_TEMP_STORAGE = new byte[32 * 1024]; + + // from MediaStore, can only be 0, 90, 180, 270; + public int orientation; + + static LocalPhotoData buildFromCursor(Cursor c) { + LocalPhotoData d = new LocalPhotoData(); + 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; + } - public LoadBitmapTask( - LocalImageData d, ImageView v) { - mData = d; - mView = v; + @Override + View getView(Context c, + int decodeWidth, int decodeHeight, Drawable placeHolder) { + ImageView v = new ImageView(c); + v.setImageDrawable(placeHolder); + + v.setScaleType(ImageView.ScaleType.FIT_XY); + LoadBitmapTask task = new LoadBitmapTask(v, decodeWidth, decodeHeight); + task.execute(); + return v; + } + + + @Override + public String toString() { + return "LocalPhotoData:" + ",data=" + path + ",mimeType=" + mimeType + + "," + width + "x" + height + ",orientation=" + orientation; } @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; + public int getType() { + return TYPE_PHOTO; + } + + private static 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 static class Dimension { + public int width; + public int height; + } + + private class LoadBitmapTask extends AsyncTask<Void, Void, Bitmap> { + private ImageView mView; + private int mDecodeWidth; + private int mDecodeHeight; + + public LoadBitmapTask(ImageView v, int decodeWidth, int decodeHeight) { + mView = v; + mDecodeWidth = decodeWidth; + mDecodeHeight = decodeHeight; } - 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) { + + @Override + protected Bitmap doInBackground(Void... v) { + BitmapFactory.Options opts = null; + Bitmap b; + int sample = 1; + while (mDecodeWidth * sample < width + || mDecodeHeight * sample < height) { + sample *= 2; + } + opts = new BitmapFactory.Options(); + opts.inSampleSize = sample; + opts.inTempStorage = DECODE_TEMP_STORAGE; 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); + b = BitmapFactory.decodeFile(path, opts); + if (orientation != 0) { + if (isCancelled()) return null; + Matrix m = new Matrix(); + m.setRotate((float) orientation); + b = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, false); + } + return b; } - return b; - } - @Override - protected void onPostExecute(Bitmap bitmap) { - if (bitmap == null) { - Log.e(TAG, "Cannot decode bitmap file:" + mData.path); - return; + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap == null) { + Log.e(TAG, "Cannot decode bitmap file:" + path); + return; + } + mView.setImageBitmap(bitmap); } - mView.setImageBitmap(bitmap); } } } diff --git a/src/com/android/camera/ui/FilmStripGestureRecognizer.java b/src/com/android/camera/ui/FilmStripGestureRecognizer.java new file mode 100644 index 000000000..f0e2534d3 --- /dev/null +++ b/src/com/android/camera/ui/FilmStripGestureRecognizer.java @@ -0,0 +1,107 @@ +/* + * 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.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; + +// This class aggregates three gesture detectors: GestureDetector, +// ScaleGestureDetector. +public class FilmStripGestureRecognizer { + @SuppressWarnings("unused") + private static final String TAG = "FilmStripGestureRecognizer"; + + public interface Listener { + boolean onSingleTapUp(float x, float y); + boolean onDoubleTap(float x, float y); + boolean onScroll(float x, float y, float dx, float dy); + boolean onFling(float velocityX, float velocityY); + boolean onScaleBegin(float focusX, float focusY); + boolean onScale(float focusX, float focusY, float scale); + boolean onDown(float x, float y); + void onScaleEnd(); + } + + private final GestureDetector mGestureDetector; + private final ScaleGestureDetector mScaleDetector; + private final Listener mListener; + + public FilmStripGestureRecognizer(Context context, Listener listener) { + mListener = listener; + mGestureDetector = new GestureDetector(context, new MyGestureListener(), + null, true /* ignoreMultitouch */); + mScaleDetector = new ScaleGestureDetector( + context, new MyScaleListener()); + } + + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event) || mScaleDetector.onTouchEvent(event); + } + + private class MyGestureListener + extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapUp(MotionEvent e) { + return mListener.onSingleTapUp(e.getX(), e.getY()); + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + return mListener.onDoubleTap(e.getX(), e.getY()); + } + + @Override + public boolean onScroll( + MotionEvent e1, MotionEvent e2, float dx, float dy) { + return mListener.onScroll(e2.getX(), e2.getY(), dx, dy); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return mListener.onFling(velocityX, velocityY); + } + + @Override + public boolean onDown(MotionEvent e) { + mListener.onDown(e.getX(), e.getY()); + return super.onDown(e); + } + } + + private class MyScaleListener + extends ScaleGestureDetector.SimpleOnScaleGestureListener { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return mListener.onScaleBegin( + detector.getFocusX(), detector.getFocusY()); + } + + @Override + public boolean onScale(ScaleGestureDetector detector) { + return mListener.onScale(detector.getFocusX(), + detector.getFocusY(), detector.getScaleFactor()); + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + mListener.onScaleEnd(); + } + } +} diff --git a/src/com/android/camera/ui/FilmStripView.java b/src/com/android/camera/ui/FilmStripView.java index 326e969b2..eea9f0155 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -16,69 +16,99 @@ package com.android.camera.ui; +import android.animation.Animator; +import android.animation.ValueAnimator; 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.view.animation.DecelerateInterpolator; 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 static final int DURATION_BACK_ANIM = 500; + private static final int DURATION_SCROLL_TO_FILMSTRIP = 350; + private static final int DURATION_GEOMETRY_ADJUST = 200; + private static final float FILM_STRIP_SCALE = 0.6f; + private static final float MAX_SCALE = 1f; private Context mContext; - private GestureDetector mGestureDetector; + private FilmStripGestureRecognizer mGestureRecognizer; private DataAdapter mDataAdapter; private final Rect mDrawArea = new Rect(); private int mCurrentInfo; - private Scroller mScroller; - private boolean mIsScrolling; + private float mScale; + private GeometryAnimator mGeometryAnimator; private int mCenterPosition = -1; private ViewInfo[] mViewInfo = new ViewInfo[BUFFER_SIZE]; public interface ImageData { + public static final int TYPE_NONE = 0; + public static final int TYPE_CAMERA_PREVIEW = 1; + public static final int TYPE_PHOTO = 2; + public static final int TYPE_VIDEO = 3; + public static final int TYPE_PHOTOSPHERE = 4; + + // The actions are defined bit-wise so we can use bit operations like + // | and &. + public static final int ACTION_NONE = 0; + public static final int ACTION_PROMOTE = 1; + public static final int ACTION_DEMOTE = 2; + + // SIZE_FULL means disgard the width or height when deciding the view size + // of this ImageData, just use full screen size. + public static final int SIZE_FULL = -2; + // The values returned by getWidth() and getHeight() will be used for layout. public int getWidth(); public int getHeight(); + public int getType(); + public boolean isActionSupported(int action); } public interface DataAdapter { + public interface Listener { + public void onDataLoaded(); + public void onDataInserted(int dataID); + public void onDataRemoved(int dataID); + } 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); + public void setListener(Listener listener); } + // A helper class to tract and calculate the view coordination. 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; + private View mView; + private float mOffsetY; public ViewInfo(int id, View v) { + v.setPivotX(0f); + v.setPivotY(0f); mDataID = id; mView = v; mLeftPosition = -1; + mOffsetY = 0; } - public int getId() { + public int getID() { return mDataID; } @@ -90,7 +120,15 @@ public class FilmStripView extends ViewGroup { return mLeftPosition; } - public int getCenterPosition() { + public float getOffsetY() { + return mOffsetY; + } + + public void setOffsetY(float offset) { + mOffsetY = offset; + } + + public int getCenterX() { return mLeftPosition + mView.getWidth() / 2; } @@ -98,15 +136,20 @@ public class FilmStripView extends ViewGroup { return mView; } - private void layoutAt(int l, int t) { - mView.layout(l, t, l + mView.getMeasuredWidth(), t + mView.getMeasuredHeight()); + private void layoutAt(int left, int top) { + mView.layout(left, top, left + mView.getMeasuredWidth(), + top + mView.getMeasuredHeight()); } - public void layoutIn(Rect drawArea, int refCenter) { + public void layoutIn(Rect drawArea, int refCenter, float scale) { // 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); + int left = (int) (drawArea.centerX() + (mLeftPosition - refCenter) * scale); + int top = (int) (drawArea.centerY() - (mView.getMeasuredHeight() / 2) * scale + + mOffsetY); + layoutAt(left, top); + mView.setScaleX(scale); + mView.setScaleY(scale); } } @@ -129,15 +172,33 @@ public class FilmStripView extends ViewGroup { mCurrentInfo = (BUFFER_SIZE - 1) / 2; setWillNotDraw(false); mContext = context; - mScroller = new Scroller(context); - mGestureDetector = - new GestureDetector(context, new MyGestureListener(), - null, true /* ignoreMultitouch */); + mScale = 1.0f; + mGeometryAnimator = new GeometryAnimator(context); + mGestureRecognizer = + new FilmStripGestureRecognizer(context, new MyGestureReceiver()); + } + + public float getScale() { + return mScale; + } + + public boolean isAnchoredTo(int id) { + if (mViewInfo[mCurrentInfo].getID() == id + && mViewInfo[mCurrentInfo].getCenterX() == mCenterPosition) { + return true; + } + return false; + } + + public int getCurrentType() { + ViewInfo curr = mViewInfo[mCurrentInfo]; + if (curr == null) return ImageData.TYPE_NONE; + return mDataAdapter.getImageData(curr.getID()).getType(); } @Override public void onDraw(Canvas c) { - if (mIsScrolling) { + if (mGeometryAnimator.hasNewGeometry()) { layoutChildren(); } } @@ -146,13 +207,11 @@ public class FilmStripView extends ViewGroup { 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 boundWidth = MeasureSpec.getSize(widthMeasureSpec); + int boundHeight = MeasureSpec.getSize(heightMeasureSpec); + if (mDataAdapter != null) { + mDataAdapter.suggestSize(boundWidth / 2, boundHeight / 2); + } int wMode = View.MeasureSpec.EXACTLY; int hMode = View.MeasureSpec.EXACTLY; @@ -161,22 +220,25 @@ public class FilmStripView extends ViewGroup { ViewInfo info = mViewInfo[i]; if (mViewInfo[i] == null) continue; - int imageWidth = mDataAdapter.getImageData(info.getId()).getWidth(); - int imageHeight = mDataAdapter.getImageData(info.getId()).getHeight(); + int imageWidth = mDataAdapter.getImageData(info.getID()).getWidth(); + int imageHeight = mDataAdapter.getImageData(info.getID()).getHeight(); + if (imageWidth == ImageData.SIZE_FULL) imageWidth = boundWidth; + if (imageHeight == ImageData.SIZE_FULL) imageHeight = boundHeight; 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; + scaledWidth += H_PADDING * 2; mViewInfo[i].getView().measure( View.MeasureSpec.makeMeasureSpec(scaledWidth, wMode) , View.MeasureSpec.makeMeasureSpec(scaledHeight, hMode)); } - setMeasuredDimension(w, h); + setMeasuredDimension(boundWidth, boundHeight); } private int findTheNearestView(int pointX) { @@ -188,14 +250,14 @@ public class FilmStripView extends ViewGroup { nearest++); // no existing available ViewInfo if (nearest == BUFFER_SIZE) return -1; - int min = Math.abs(pointX - mViewInfo[nearest].getCenterPosition()); + int min = Math.abs(pointX - mViewInfo[nearest].getCenterX()); 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 c = mViewInfo[infoID].getCenterX(); int dist = Math.abs(pointX - c); if (dist < min) { min = dist; @@ -205,6 +267,14 @@ public class FilmStripView extends ViewGroup { return nearest; } + private ViewInfo buildInfoFromData(int dataID) { + View v = mDataAdapter.getView(mContext, dataID); + if (v == null) return null; + v.setPadding(H_PADDING, 0, H_PADDING, 0); + addView(v); + return new ViewInfo(dataID, v); + } + // We try to keep the one closest to the center of the screen at position mCurrentInfo. private void stepIfNeeded() { int nearest = findTheNearestView(mCenterPosition); @@ -223,7 +293,8 @@ public class FilmStripView extends ViewGroup { } 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); + if (mViewInfo[k - 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k - 1].getID() + 1); } } else { for (int k = BUFFER_SIZE - 1; k >= BUFFER_SIZE + adjust; k--) { @@ -236,47 +307,46 @@ public class FilmStripView extends ViewGroup { } for (int k = -1 - adjust; k >= 0; k--) { mViewInfo[k] = null; - if (mViewInfo[k + 1] != null) getInfo(k, mViewInfo[k + 1].getId() - 1); + if (mViewInfo[k + 1] != null) + mViewInfo[k] = buildInfoFromData(mViewInfo[k + 1].getID() - 1); } } } - private void stopScroll() { - mScroller.forceFinished(true); - mIsScrolling = false; - } - + // Don't go out of bound. 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() == 0 && mCenterPosition < curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); } - if (curr.getId() == mDataAdapter.getTotalNumber() - 1 - && mCenterPosition > curr.getCenterPosition()) { - mCenterPosition = curr.getCenterPosition(); - if (mIsScrolling) stopScroll(); + if (curr.getID() == mDataAdapter.getTotalNumber() - 1 + && mCenterPosition > curr.getCenterX()) { + mCenterPosition = curr.getCenterX(); + mGeometryAnimator.stopScroll(); } } private void layoutChildren() { - mIsScrolling = mScroller.computeScrollOffset(); - - if (mIsScrolling) mCenterPosition = mScroller.getCurrX(); + if (mGeometryAnimator.hasNewGeometry()) { + mCenterPosition = mGeometryAnimator.getNewPosition(); + mScale = mGeometryAnimator.getNewScale(); + } adjustCenterPosition(); - mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition); + mViewInfo[mCurrentInfo].layoutIn(mDrawArea, mCenterPosition, mScale); // 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); + curr.setLeftPosition( + next.getLeftPosition() - curr.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); } } @@ -285,8 +355,9 @@ public class FilmStripView extends ViewGroup { ViewInfo curr = mViewInfo[infoID]; if (curr != null) { ViewInfo prev = mViewInfo[infoID - 1]; - curr.setLeftPosition(prev.getLeftPosition() + prev.getView().getMeasuredWidth()); - curr.layoutIn(mDrawArea, mCenterPosition); + curr.setLeftPosition( + prev.getLeftPosition() + prev.getView().getMeasuredWidth()); + curr.layoutIn(mDrawArea, mCenterPosition, mScale); } } @@ -306,24 +377,43 @@ public class FilmStripView extends ViewGroup { layoutChildren(); } - public void setDataAdapter( - DataAdapter adapter, ContentResolver resolver) { + public void setDataAdapter(DataAdapter adapter) { mDataAdapter = adapter; mDataAdapter.suggestSize(getMeasuredWidth(), getMeasuredHeight()); - mDataAdapter.setDataListener(this); - mDataAdapter.requestLoad(resolver); + mDataAdapter.setListener(new DataAdapter.Listener() { + @Override + public void onDataLoaded() { + reload(); + } + + @Override + public void onDataInserted(int dataID) { + } + + @Override + public void onDataRemoved(int dataID) { + } + }); } - 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 boolean isInCameraFullscreen() { + return (isAnchoredTo(0) && mScale == 1f + && getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW); } - public void onDataChanged() { + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + if (isInCameraFullscreen()) return false; + return true; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mGestureRecognizer.onTouchEvent(ev); + return true; + } + + public void reload() { removeAllViews(); int dataNumber = mDataAdapter.getTotalNumber(); if (dataNumber == 0) return; @@ -333,89 +423,315 @@ public class FilmStripView extends ViewGroup { // previous data exists. if (mViewInfo[mCurrentInfo] != null) { currentLeft = mViewInfo[mCurrentInfo].getLeftPosition(); - currentData = mViewInfo[mCurrentInfo].getId(); + currentData = mViewInfo[mCurrentInfo].getID(); } - getInfo(mCurrentInfo, currentData); + mViewInfo[mCurrentInfo] = buildInfoFromData(currentData); mViewInfo[mCurrentInfo].setLeftPosition(currentLeft); + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW + && currentLeft == 0) { + // we are in camera mode by default. + mGeometryAnimator.lockPosition(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); + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID - 1].getID() + 1); } infoID = mCurrentInfo - i; if (infoID >= 0 && mViewInfo[infoID + 1] != null) { - getInfo(infoID, mViewInfo[infoID + 1].getId() - 1); + mViewInfo[infoID] = buildInfoFromData(mViewInfo[infoID + 1].getID() - 1); } } layoutChildren(); } - private void movePositionTo(int position) { - mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, - 0, BACK_SCROLL_DURATION); - layoutChildren(); - } + // GeometryAnimator controls all the geometry animations. It passively + // tells the geometry information on demand. + private class GeometryAnimator implements + ValueAnimator.AnimatorUpdateListener, + Animator.AnimatorListener { + + private ValueAnimator mScaleAnimator; + private boolean mHasNewScale; + private float mNewScale; + + private Scroller mScroller; + private boolean mHasNewPosition; + private DecelerateInterpolator mDecelerateInterpolator; + + private boolean mCanStopScroll; + private boolean mCanStopScale; + + private boolean mIsPositionLocked; + private int mLockedPosition; + + private Runnable mPostAction; + + GeometryAnimator(Context context) { + mScroller = new Scroller(context); + mHasNewPosition = false; + mScaleAnimator = new ValueAnimator(); + mScaleAnimator.addUpdateListener(GeometryAnimator.this); + mScaleAnimator.addListener(GeometryAnimator.this); + mDecelerateInterpolator = new DecelerateInterpolator(); + mCanStopScroll = true; + mCanStopScale = true; + mHasNewScale = false; + } - public void goToFirst() { - movePositionTo(0); - } + boolean hasNewGeometry() { + mHasNewPosition = mScroller.computeScrollOffset(); + if (!mHasNewPosition) { + mCanStopScroll = true; + } + // If the position is locked, then we always return true to force + // the position value to use the locked value. + return (mHasNewPosition || mHasNewScale || mIsPositionLocked); + } - @Override - public boolean onTouchEvent(MotionEvent ev) { - return mGestureDetector.onTouchEvent(ev); - } + // Always call hasNewGeometry() before getting the new scale value. + float getNewScale() { + if (!mHasNewScale) return mScale; + mHasNewScale = false; + return mNewScale; + } + + // Always call hasNewGeometry() before getting the new position value. + int getNewPosition() { + if (mIsPositionLocked) return mLockedPosition; + if (!mHasNewPosition) return mCenterPosition; + return mScroller.getCurrX(); + } - private class MyGestureListener - extends GestureDetector.SimpleOnGestureListener { + void lockPosition(int pos) { + mIsPositionLocked = true; + mLockedPosition = pos; + } - @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; - } + void unlockPosition() { + if (mIsPositionLocked) { + // only when the position is previously locked we set the current + // position to make it consistent. + mCenterPosition = mLockedPosition; + mIsPositionLocked = false; } + } + + void fling(int velocityX, int minX, int maxX) { + if (!stopScroll() || mIsPositionLocked) return; + mScroller.fling(mCenterPosition, 0, velocityX, 0, minX, maxX, 0, 0); + } + + boolean stopScroll() { + if (!mCanStopScroll) return false; + mScroller.forceFinished(true); + mHasNewPosition = false; return true; } + boolean stopScale() { + if (!mCanStopScale) return false; + mScaleAnimator.cancel(); + mHasNewScale = false; + return true; + } + + void stop() { + stopScroll(); + stopScale(); + } + + void scrollTo(int position, int duration, boolean interruptible) { + if (!stopScroll() || mIsPositionLocked) return; + mCanStopScroll = interruptible; + stopScroll(); + mScroller.startScroll(mCenterPosition, 0, position - mCenterPosition, + 0, duration); + } + + void scrollTo(int position, int duration) { + scrollTo(position, duration, true); + } + + void scaleTo(float scale, int duration, boolean interruptible) { + if (!stopScale()) return; + mCanStopScale = interruptible; + mScaleAnimator.setDuration(duration); + mScaleAnimator.setFloatValues(mScale, scale); + mScaleAnimator.setInterpolator(mDecelerateInterpolator); + mScaleAnimator.start(); + mHasNewScale = true; + } + + void scaleTo(float scale, int duration) { + scaleTo(scale, duration, true); + } + + void setPostAction(Runnable act) { + mPostAction = act; + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mHasNewScale = true; + mNewScale = (Float) animation.getAnimatedValue(); + layoutChildren(); + } + + @Override + public void onAnimationStart(Animator anim) { + } + + @Override + public void onAnimationEnd(Animator anim) { + if (mPostAction != null) { + mPostAction.run(); + mPostAction = null; + } + mCanStopScale = true; + } + + @Override + public void onAnimationCancel(Animator anim) { + mPostAction = null; + } + + @Override + public void onAnimationRepeat(Animator anim) { + } + } + + private class MyGestureReceiver implements FilmStripGestureRecognizer.Listener { + // Indicating the current trend of scaling is up (>1) or down (<1). + private float mScaleTrend; + + @Override + public boolean onSingleTapUp(float x, float y) { + return false; + } + + @Override + public boolean onDoubleTap(float x, float y) { + return false; + } + @Override - public boolean onDown(MotionEvent ev) { - if (mIsScrolling) stopScroll(); + public boolean onDown(float x, float y) { + mGeometryAnimator.stop(); return true; } @Override - public boolean onScroll( - MotionEvent e1, MotionEvent e2, float dx, float dy) { - stopScroll(); - mCenterPosition += dx; + public boolean onScroll(float x, float y, float dx, float dy) { + int deltaX = (int) (dx / mScale); + if (deltaX > 0 && isInCameraFullscreen() ) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } + + mCenterPosition += deltaX; + + // Vertical part. Promote or demote. + int scaledDeltaY = (int) (dy / mScale); + + for (int i = 0; i < BUFFER_SIZE; i++) { + if (mViewInfo[i] == null) continue; + Rect hitRect = new Rect(); + View v = mViewInfo[i].getView(); + v.getHitRect(hitRect); + if (hitRect.contains((int) x, (int) y)) { + ImageData data = mDataAdapter.getImageData(mViewInfo[i].getID()); + if ((data.isActionSupported(ImageData.ACTION_DEMOTE) && dy > 0) + || (data.isActionSupported(ImageData.ACTION_PROMOTE) && dy < 0)) { + mViewInfo[i].setOffsetY(mViewInfo[i].getOffsetY() - dy); + } + break; + } + } + layoutChildren(); return true; } @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, - float velocityY) { + public boolean onFling(float velocityX, float velocityY) { + float scaledVelocityX = velocityX / mScale; + if (isInCameraFullscreen() && scaledVelocityX < 0) { + mGeometryAnimator.unlockPosition(); + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + } ViewInfo info = mViewInfo[mCurrentInfo]; int w = getWidth(); if (info == null) return true; - mScroller.fling(mCenterPosition, 0, (int) -velocityX, (int) velocityY, + mGeometryAnimator.fling((int) -scaledVelocityX, // estimation of possible length on the left - info.getLeftPosition() - info.getId() * w * 2, + info.getLeftPosition() - info.getID() * w * 2, // estimation of possible length on the right info.getLeftPosition() - + (mDataAdapter.getTotalNumber() - info.getId()) * w * 2, - 0, 0); + + (mDataAdapter.getTotalNumber() - info.getID()) * w * 2); + layoutChildren(); + return true; + } + + @Override + public boolean onScaleBegin(float focusX, float focusY) { + if (isInCameraFullscreen()) return false; + mScaleTrend = 1f; + return true; + } + + @Override + public boolean onScale(float focusX, float focusY, float scale) { + if (isInCameraFullscreen()) return false; + + mScaleTrend = mScaleTrend * 0.5f + scale * 0.5f; + mScale *= scale; + if (mScale <= FILM_STRIP_SCALE) mScale = FILM_STRIP_SCALE; + if (mScale >= MAX_SCALE) mScale = MAX_SCALE; layoutChildren(); return true; } + + @Override + public void onScaleEnd() { + if (mScaleTrend >= 1f) { + if (mScale != 1f) { + mGeometryAnimator.scaleTo(1f, DURATION_GEOMETRY_ADJUST, false); + } + + if (getCurrentType() == ImageData.TYPE_CAMERA_PREVIEW) { + if (isAnchoredTo(0)) { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } else { + mGeometryAnimator.scrollTo( + mViewInfo[mCurrentInfo].getCenterX(), + DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mLockPositionRunnable); + } + } + } else { + // Scale down to film strip mode. + if (mScale == FILM_STRIP_SCALE) { + mGeometryAnimator.unlockPosition(); + return; + } + mGeometryAnimator.scaleTo(FILM_STRIP_SCALE, DURATION_GEOMETRY_ADJUST, false); + mGeometryAnimator.setPostAction(mUnlockPositionRunnable); + } + } + + private Runnable mLockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.lockPosition(mViewInfo[mCurrentInfo].getCenterX()); + } + }; + + private Runnable mUnlockPositionRunnable = new Runnable() { + @Override + public void run() { + mGeometryAnimator.unlockPosition(); + } + }; } } diff --git a/src/com/android/gallery3d/app/AlbumPage.java b/src/com/android/gallery3d/app/AlbumPage.java index 51591cc51..001ce87b7 100644 --- a/src/com/android/gallery3d/app/AlbumPage.java +++ b/src/com/android/gallery3d/app/AlbumPage.java @@ -39,8 +39,8 @@ import com.android.gallery3d.data.MediaItem; import com.android.gallery3d.data.MediaObject; import com.android.gallery3d.data.MediaSet; import com.android.gallery3d.data.Path; -import com.android.gallery3d.filtershow.CropExtras; import com.android.gallery3d.filtershow.FilterShowActivity; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.glrenderer.FadeTexture; import com.android.gallery3d.glrenderer.GLCanvas; import com.android.gallery3d.ui.ActionModeHandler; diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java index baab88990..1e7728162 100644 --- a/src/com/android/gallery3d/app/TrimVideo.java +++ b/src/com/android/gallery3d/app/TrimVideo.java @@ -237,7 +237,7 @@ public class TrimVideo extends Activity implements public void run() { try { VideoUtils.startTrim(mSrcFile, mDstFileInfo.mFile, - mTrimStartTime, mTrimEndTime, mVideoView.getDuration()); + mTrimStartTime, mTrimEndTime); // Update the database for adding a new video file. SaveVideoFileUtils.insertContent(mDstFileInfo, getContentResolver(), mUri); diff --git a/src/com/android/gallery3d/app/VideoUtils.java b/src/com/android/gallery3d/app/VideoUtils.java index 8ffc3d5eb..a3c3ef273 100644 --- a/src/com/android/gallery3d/app/VideoUtils.java +++ b/src/com/android/gallery3d/app/VideoUtils.java @@ -19,6 +19,14 @@ package com.android.gallery3d.app; +import android.media.MediaCodec.BufferInfo; +import android.media.MediaExtractor; +import android.media.MediaFormat; +import android.media.MediaMetadataRetriever; +import android.media.MediaMuxer; +import android.util.Log; + +import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.util.SaveVideoFileInfo; import com.coremedia.iso.IsoFile; import com.coremedia.iso.boxes.TimeToSampleBox; @@ -29,17 +37,49 @@ import com.googlecode.mp4parser.authoring.container.mp4.MovieCreator; import com.googlecode.mp4parser.authoring.tracks.CroppedTrack; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; public class VideoUtils { + private static final String LOGTAG = "VideoUtils"; + private static final int DEFAULT_BUFFER_SIZE = 1 * 1024 * 1024; + + /** + * Remove the sound track. + */ + public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(filePath, dstFileInfo.mFile.getPath(), -1, -1, + false, true); + } else { + startMuteUsingMp4Parser(filePath, dstFileInfo); + } + } - public static void startMute(String filePath, SaveVideoFileInfo dstFileInfo) throws IOException { + /** + * Shortens/Crops tracks + */ + public static void startTrim(File src, File dst, int startMs, int endMs) + throws IOException { + if (ApiHelper.HAS_MEDIA_MUXER) { + genVideoUsingMuxer(src.getPath(), dst.getPath(), startMs, endMs, + true, true); + } else { + trimUsingMp4Parser(src, dst, startMs, endMs); + } + } + + private static void startMuteUsingMp4Parser(String filePath, + SaveVideoFileInfo dstFileInfo) throws FileNotFoundException, IOException { File dst = dstFileInfo.mFile; File src = new File(filePath); RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); @@ -67,16 +107,123 @@ public class VideoUtils { IsoFile out = new DefaultMp4Builder().build(movie); FileOutputStream fos = new FileOutputStream(dst); FileChannel fc = fos.getChannel(); - out.getBox(fc); // This one build up the memory. + out.getBox(fc); // This one build up the memory. fc.close(); fos.close(); } /** - * Shortens/Crops a track + * @param srcPath the path of source video file. + * @param dstPath the path of destination video file. + * @param startMs starting time in milliseconds for trimming. Set to + * negative if starting from beginning. + * @param endMs end time for trimming in milliseconds. Set to negative if + * no trimming at the end. + * @param useAudio true if keep the audio track from the source. + * @param useVideo true if keep the video track from the source. + * @throws IOException */ - public static void startTrim(File src, File dst, int startMs, int endMs, int totalMs) throws IOException { + private static void genVideoUsingMuxer(String srcPath, String dstPath, + int startMs, int endMs, boolean useAudio, boolean useVideo) + throws IOException { + // Set up MediaExtractor to read from the source. + MediaExtractor extractor = new MediaExtractor(); + extractor.setDataSource(srcPath); + + int trackCount = extractor.getTrackCount(); + + // Set up MediaMuxer for the destination. + MediaMuxer muxer; + muxer = new MediaMuxer(dstPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); + + // Set up the tracks and retrieve the max buffer size for selected + // tracks. + HashMap<Integer, Integer> indexMap = new HashMap<Integer, + Integer>(trackCount); + int bufferSize = -1; + for (int i = 0; i < trackCount; i++) { + MediaFormat format = extractor.getTrackFormat(i); + String mime = format.getString(MediaFormat.KEY_MIME); + + boolean selectCurrentTrack = false; + + if (mime.startsWith("audio/") && useAudio) { + selectCurrentTrack = true; + } else if (mime.startsWith("video/") && useVideo) { + selectCurrentTrack = true; + } + + if (selectCurrentTrack) { + extractor.selectTrack(i); + int dstIndex = muxer.addTrack(format); + indexMap.put(i, dstIndex); + if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { + int newSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); + bufferSize = newSize > bufferSize ? newSize : bufferSize; + } + } + } + + if (bufferSize < 0) { + bufferSize = DEFAULT_BUFFER_SIZE; + } + + // Set up the orientation and starting time for extractor. + MediaMetadataRetriever retrieverSrc = new MediaMetadataRetriever(); + retrieverSrc.setDataSource(srcPath); + String degreesString = retrieverSrc.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION); + if (degreesString != null) { + int degrees = Integer.parseInt(degreesString); + if (degrees >= 0) { + muxer.setOrientationHint(degrees); + } + } + + if (startMs > 0) { + extractor.seekTo(startMs * 1000, MediaExtractor.SEEK_TO_CLOSEST_SYNC); + } + + // Copy the samples from MediaExtractor to MediaMuxer. We will loop + // for copying each sample and stop when we get to the end of the source + // file or exceed the end time of the trimming. + int offset = 0; + int trackIndex = -1; + ByteBuffer dstBuf = ByteBuffer.allocate(bufferSize); + BufferInfo bufferInfo = new BufferInfo(); + + muxer.start(); + while (true) { + bufferInfo.offset = offset; + bufferInfo.size = extractor.readSampleData(dstBuf, offset); + if (bufferInfo.size < 0) { + Log.d(LOGTAG, "Saw input EOS."); + bufferInfo.size = 0; + break; + } else { + bufferInfo.presentationTimeUs = extractor.getSampleTime(); + if (endMs > 0 && bufferInfo.presentationTimeUs > (endMs * 1000)) { + Log.d(LOGTAG, "The current sample is over the trim end time."); + break; + } else { + bufferInfo.flags = extractor.getSampleFlags(); + trackIndex = extractor.getSampleTrackIndex(); + + muxer.writeSampleData(indexMap.get(trackIndex), dstBuf, + bufferInfo); + extractor.advance(); + } + } + } + + muxer.stop(); + muxer.release(); + return; + } + + private static void trimUsingMp4Parser(File src, File dst, int startMs, int endMs) + throws FileNotFoundException, IOException { RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r"); Movie movie = MovieCreator.build(randomAccessFile.getChannel()); @@ -84,22 +231,25 @@ public class VideoUtils { List<Track> tracks = movie.getTracks(); movie.setTracks(new LinkedList<Track>()); - double startTime = startMs/1000; - double endTime = endMs/1000; + double startTime = startMs / 1000; + double endTime = endMs / 1000; boolean timeCorrected = false; - // Here we try to find a track that has sync samples. Since we can only start decoding - // at such a sample we SHOULD make sure that the start of the new fragment is exactly - // such a frame + // Here we try to find a track that has sync samples. Since we can only + // start decoding at such a sample we SHOULD make sure that the start of + // the new fragment is exactly such a frame. for (Track track : tracks) { if (track.getSyncSamples() != null && track.getSyncSamples().length > 0) { if (timeCorrected) { - // This exception here could be a false positive in case we have multiple tracks - // with sync samples at exactly the same positions. E.g. a single movie containing - // multiple qualities of the same video (Microsoft Smooth Streaming file) - - throw new RuntimeException("The startTime has already been corrected by another track with SyncSample. Not Supported."); + // This exception here could be a false positive in case we + // have multiple tracks with sync samples at exactly the + // same positions. E.g. a single movie containing multiple + // qualities of the same video (Microsoft Smooth Streaming + // file) + throw new RuntimeException( + "The startTime has already been corrected by" + + " another track with SyncSample. Not Supported."); } startTime = correctTimeToSyncSample(track, startTime, false); endTime = correctTimeToSyncSample(track, endTime, true); @@ -116,20 +266,23 @@ public class VideoUtils { for (int i = 0; i < track.getDecodingTimeEntries().size(); i++) { TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); for (int j = 0; j < entry.getCount(); j++) { - // entry.getDelta() is the amount of time the current sample covers. + // entry.getDelta() is the amount of time the current sample + // covers. if (currentTime <= startTime) { // current sample is still before the new starttime startSample = currentSample; } if (currentTime <= endTime) { - // current sample is after the new start time and still before the new endtime + // current sample is after the new start time and still + // before the new endtime endSample = currentSample; } else { // current sample is after the end of the cropped video break; } - currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale(); + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); currentSample++; } } @@ -139,15 +292,8 @@ public class VideoUtils { randomAccessFile.close(); } - protected static long getDuration(Track track) { - long duration = 0; - for (TimeToSampleBox.Entry entry : track.getDecodingTimeEntries()) { - duration += entry.getCount() * entry.getDelta(); - } - return duration; - } - - private static double correctTimeToSyncSample(Track track, double cutHere, boolean next) { + private static double correctTimeToSyncSample(Track track, double cutHere, + boolean next) { double[] timeOfSyncSamples = new double[track.getSyncSamples().length]; long currentSample = 0; double currentTime = 0; @@ -155,10 +301,13 @@ public class VideoUtils { TimeToSampleBox.Entry entry = track.getDecodingTimeEntries().get(i); for (int j = 0; j < entry.getCount(); j++) { if (Arrays.binarySearch(track.getSyncSamples(), currentSample + 1) >= 0) { - // samples always start with 1 but we start with zero therefore +1 - timeOfSyncSamples[Arrays.binarySearch(track.getSyncSamples(), currentSample + 1)] = currentTime; + // samples always start with 1 but we start with zero + // therefore +1 + timeOfSyncSamples[Arrays.binarySearch( + track.getSyncSamples(), currentSample + 1)] = currentTime; } - currentTime += (double) entry.getDelta() / (double) track.getTrackMetaData().getTimescale(); + currentTime += (double) entry.getDelta() + / (double) track.getTrackMetaData().getTimescale(); currentSample++; } } @@ -176,5 +325,4 @@ public class VideoUtils { return timeOfSyncSamples[timeOfSyncSamples.length - 1]; } - } diff --git a/src/com/android/gallery3d/app/Wallpaper.java b/src/com/android/gallery3d/app/Wallpaper.java index 1bbe8d2c6..91bc77271 100644 --- a/src/com/android/gallery3d/app/Wallpaper.java +++ b/src/com/android/gallery3d/app/Wallpaper.java @@ -27,7 +27,7 @@ import android.view.Display; import com.android.gallery3d.common.ApiHelper; import com.android.gallery3d.filtershow.FilterShowActivity; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.CropExtras; /** * Wallpaper picker for the gallery application. This just redirects to the diff --git a/src/com/android/gallery3d/filtershow/FilterShowActivity.java b/src/com/android/gallery3d/filtershow/FilterShowActivity.java index 81c171758..1a7d200f1 100644 --- a/src/com/android/gallery3d/filtershow/FilterShowActivity.java +++ b/src/com/android/gallery3d/filtershow/FilterShowActivity.java @@ -55,6 +55,7 @@ import com.android.gallery3d.data.LocalAlbum; import com.android.gallery3d.filtershow.cache.CachingPipeline; import com.android.gallery3d.filtershow.cache.FilteringPipeline; import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.filtershow.editors.BasicEditor; import com.android.gallery3d.filtershow.editors.EditorCrop; import com.android.gallery3d.filtershow.editors.EditorDraw; @@ -133,6 +134,7 @@ public class FilterShowActivity extends Activity implements OnItemClickListener, private FilterIconButton mNullBorderFilter; private int mIconSeedSize = 140; + private View mImageCategoryPanel = null; @Override public void onCreate(Bundle savedInstanceState) { @@ -219,6 +221,16 @@ public class FilterShowActivity extends Activity implements OnItemClickListener, setupHistoryPanel(); setupStatePanel(); + + mImageCategoryPanel = findViewById(R.id.imageCategoryPanel); + } + + public void hideCategoryPanel() { + mImageCategoryPanel.setVisibility(View.GONE); + } + + public void showCategoryPanel() { + mImageCategoryPanel.setVisibility(View.VISIBLE); } public void setupHistoryPanel() { @@ -348,15 +360,7 @@ public class FilterShowActivity extends Activity implements OnItemClickListener, ImageFilter.setActivityForMemoryToasts(this); Resources res = getResources(); - // TODO: add a mechanism to set the resources in FiltersManagmer - ImageFilterBorder filterBorder = (ImageFilterBorder) FiltersManager.getManager().getFilter(ImageFilterBorder.class); - filterBorder.setResources(res); - filterBorder = (ImageFilterBorder) FiltersManager.getPreviewManager().getFilter(ImageFilterBorder.class); - filterBorder.setResources(res); - ImageFilterFx filterFx = (ImageFilterFx) FiltersManager.getManager().getFilter(ImageFilterFx.class); - filterFx.setResources(res); - filterFx = (ImageFilterFx) FiltersManager.getPreviewManager().getFilter(ImageFilterFx.class); - filterFx.setResources(res); + FiltersManager.setResources(res); ImageShow.setDefaultBackgroundColor(res.getColor(R.color.background_screen)); // TODO: get those values from XML. @@ -542,6 +546,7 @@ public class FilterShowActivity extends Activity implements OnItemClickListener, ImageFilter.resetStatics(); FiltersManager.getPreviewManager().freeRSFilterScripts(); FiltersManager.getManager().freeRSFilterScripts(); + FiltersManager.getHighresManager().freeRSFilterScripts(); FiltersManager.reset(); CachingPipeline.destroyRenderScriptContext(); super.onDestroy(); diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java index 6b20fe180..1d0118f85 100644 --- a/src/com/android/gallery3d/filtershow/PanelController.java +++ b/src/com/android/gallery3d/filtershow/PanelController.java @@ -31,6 +31,7 @@ import android.util.Log; import com.android.gallery3d.R; import com.android.gallery3d.filtershow.editors.Editor; import com.android.gallery3d.filtershow.editors.EditorTinyPlanet; +import com.android.gallery3d.filtershow.editors.ImageOnlyEditor; import com.android.gallery3d.filtershow.filters.FilterRepresentation; import com.android.gallery3d.filtershow.filters.ImageFilter; import com.android.gallery3d.filtershow.filters.ImageFilterTinyPlanet; @@ -358,7 +359,6 @@ public class PanelController implements OnClickListener { if (mCurrentEditor != null) { mCurrentEditor.reflectCurrentFilter(); } - } } @@ -369,6 +369,7 @@ public class PanelController implements OnClickListener { HistoryAdapter adapter = MasterImage.getImage().getHistory(); int position = adapter.undo(); MasterImage.getImage().onHistoryItemClick(position); + mActivity.showCategoryPanel(); showPanel(mCurrentPanel); mCurrentImage.select(); if (mCurrentEditor != null) { @@ -586,7 +587,11 @@ public class PanelController implements OnClickListener { } mUtilityPanel.hideAccessoryViews(); mUtilityPanel.showMenu(false); + if (view instanceof FilterIconButton) { + if (mCurrentEditor != null) { + mCurrentEditor.detach(); + } mCurrentEditor = null; FilterIconButton component = (FilterIconButton) view; FilterRepresentation representation = component.getFilterRepresentation(); @@ -595,6 +600,9 @@ public class PanelController implements OnClickListener { mUtilityPanel.setShowParameter(representation.showParameterValue()); if (representation.getEditorId() != 0) { + if (representation.getEditorId() != ImageOnlyEditor.ID) { + mActivity.hideCategoryPanel(); + } if (mEditorPlaceHolder.contains(representation.getEditorId())) { mCurrentEditor = mEditorPlaceHolder.showEditor( representation.getEditorId()); @@ -625,6 +633,7 @@ public class PanelController implements OnClickListener { return; } + mActivity.showCategoryPanel(); int id = view.getId(); if (id == EditorTinyPlanet.ID) { mCurrentImage = showImageView(R.id.imageTinyPlanet); diff --git a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java index 055309f22..32d4ab094 100644 --- a/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java +++ b/src/com/android/gallery3d/filtershow/cache/CachingPipeline.java @@ -282,10 +282,13 @@ public class CachingPipeline { || request.getType() == RenderingRequest.FILTERS_RENDERING || request.getType() == RenderingRequest.ICON_RENDERING || request.getType() == RenderingRequest.PARTIAL_RENDERING - || request.getType() == RenderingRequest.HIGHRES_RENDERING) { + || request.getType() == RenderingRequest.HIGHRES_RENDERING + || request.getType() == RenderingRequest.STYLE_ICON_RENDERING) { if (request.getType() == RenderingRequest.ICON_RENDERING) { mEnvironment.setQuality(ImagePreset.QUALITY_ICON); + } else if (request.getType() == RenderingRequest.STYLE_ICON_RENDERING) { + mEnvironment.setQuality(ImagePreset.STYLE_ICON); } else { mEnvironment.setQuality(ImagePreset.QUALITY_PREVIEW); } diff --git a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java index a8c8d7b47..e63323b92 100644 --- a/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java +++ b/src/com/android/gallery3d/filtershow/cache/FilteringPipeline.java @@ -34,12 +34,13 @@ public class FilteringPipeline implements Handler.Callback { private static final String LOGTAG = "FilteringPipeline"; private boolean DEBUG = false; - private static long HIRES_DELAY = 100; // in ms + private static long HIRES_DELAY = 300; // in ms private volatile boolean mPipelineIsOn = false; private CachingPipeline mAccessoryPipeline = null; private CachingPipeline mPreviewPipeline = null; + private CachingPipeline mHighresPreviewPipeline = null; private HandlerThread mHandlerThread = null; private final static int NEW_PRESET = 0; @@ -111,7 +112,11 @@ public class FilteringPipeline implements Handler.Callback { } RenderingRequest request = (RenderingRequest) msg.obj; - mAccessoryPipeline.render(request); + if (msg.what == COMPUTE_HIGHRES_RENDERING_REQUEST) { + mHighresPreviewPipeline.render(request); + } else { + mAccessoryPipeline.render(request); + } Message uimsg = mUIHandler.obtainMessage(NEW_RENDERING_REQUEST); uimsg.obj = request; mUIHandler.sendMessage(uimsg); @@ -126,8 +131,12 @@ public class FilteringPipeline implements Handler.Callback { Process.THREAD_PRIORITY_FOREGROUND); mHandlerThread.start(); mProcessingHandler = new Handler(mHandlerThread.getLooper(), this); - mAccessoryPipeline = new CachingPipeline(FiltersManager.getManager(), "Accessory"); - mPreviewPipeline = new CachingPipeline(FiltersManager.getPreviewManager(), "Preview"); + mAccessoryPipeline = new CachingPipeline( + FiltersManager.getManager(), "Accessory"); + mPreviewPipeline = new CachingPipeline( + FiltersManager.getPreviewManager(), "Preview"); + mHighresPreviewPipeline = new CachingPipeline( + FiltersManager.getHighresManager(), "Highres"); } public synchronized static FilteringPipeline getPipeline() { @@ -145,6 +154,7 @@ public class FilteringPipeline implements Handler.Callback { Log.v(LOGTAG,"setOriginal, size " + bitmap.getWidth() + " x " + bitmap.getHeight()); mAccessoryPipeline.setOriginal(bitmap); mPreviewPipeline.setOriginal(bitmap); + mHighresPreviewPipeline.setOriginal(bitmap); } public void postRenderingRequest(RenderingRequest request) { @@ -194,16 +204,19 @@ public class FilteringPipeline implements Handler.Callback { public void setPreviewScaleFactor(float previewScaleFactor) { mAccessoryPipeline.setPreviewScaleFactor(previewScaleFactor); mPreviewPipeline.setPreviewScaleFactor(previewScaleFactor); + mHighresPreviewPipeline.setPreviewScaleFactor(previewScaleFactor); } public void setHighResPreviewScaleFactor(float highResPreviewScaleFactor) { mAccessoryPipeline.setHighResPreviewScaleFactor(highResPreviewScaleFactor); mPreviewPipeline.setHighResPreviewScaleFactor(highResPreviewScaleFactor); + mHighresPreviewPipeline.setHighResPreviewScaleFactor(highResPreviewScaleFactor); } public static synchronized void reset() { sPipeline.mAccessoryPipeline.reset(); sPipeline.mPreviewPipeline.reset(); + sPipeline.mHighresPreviewPipeline.reset(); sPipeline.mHandlerThread.quit(); sPipeline = null; } @@ -213,6 +226,7 @@ public class FilteringPipeline implements Handler.Callback { if (mPipelineIsOn) { assert(mPreviewPipeline.isInitialized()); assert(mAccessoryPipeline.isInitialized()); + assert(mHighresPreviewPipeline.isInitialized()); updatePreviewBuffer(); } } diff --git a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java index 3416ddfc2..e5bc411de 100644 --- a/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java +++ b/src/com/android/gallery3d/filtershow/cache/RenderingRequest.java @@ -41,6 +41,8 @@ public class RenderingRequest { public static final int ICON_RENDERING = 3; public static final int PARTIAL_RENDERING = 4; public static final int HIGHRES_RENDERING = 5; + public static final int STYLE_ICON_RENDERING = 6; + private static final Bitmap.Config mConfig = Bitmap.Config.ARGB_8888; public static void post(Bitmap source, ImagePreset preset, int type, RenderingRequestCaller caller) { @@ -59,7 +61,8 @@ public class RenderingRequest { Bitmap bitmap = null; if (type == FULL_RENDERING || type == GEOMETRY_RENDERING - || type == ICON_RENDERING) { + || type == ICON_RENDERING + || type == STYLE_ICON_RENDERING) { CachingPipeline pipeline = new CachingPipeline( FiltersManager.getManager(), "Icon"); bitmap = pipeline.renderGeometryIcon(source, preset); diff --git a/src/com/android/gallery3d/filtershow/controller/StyleChooser.java b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java index 3f75839ba..b3d0de715 100644 --- a/src/com/android/gallery3d/filtershow/controller/StyleChooser.java +++ b/src/com/android/gallery3d/filtershow/controller/StyleChooser.java @@ -17,13 +17,12 @@ import com.android.gallery3d.filtershow.editors.Editor; import java.util.Vector; -public class StyleChooser implements Control, RenderingRequestCaller { +public class StyleChooser implements Control { private final String LOGTAG = "StyleChooser"; protected ParameterStyles mParameter; protected LinearLayout mLinearLayout; protected Editor mEditor; private View mTopView; - private int mProcessingButton = 0; private Vector<ImageButton> mIconButton = new Vector<ImageButton>(); protected int mLayoutID = R.layout.filtershow_control_style_chooser; @@ -42,7 +41,7 @@ public class StyleChooser implements Control, RenderingRequestCaller { mIconButton.clear(); LayoutParams lp = new LayoutParams(120, 120); for (int i = 0; i < n; i++) { - ImageButton button = new ImageButton(context); + final ImageButton button = new ImageButton(context); button.setScaleType(ScaleType.CENTER_CROP); button.setLayoutParams(lp); button.setBackgroundResource(android.R.color.transparent); @@ -55,9 +54,17 @@ public class StyleChooser implements Control, RenderingRequestCaller { } }); mLinearLayout.addView(button); + mParameter.getIcon(i, new RenderingRequestCaller() { + @Override + public void available(RenderingRequest request) { + Bitmap bmap = request.getBitmap(); + if (bmap == null) { + return; + } + button.setImageBitmap(bmap); + } + }); } - mProcessingButton = 0; - mParameter.getIcon(mProcessingButton, this); } @Override @@ -78,23 +85,4 @@ public class StyleChooser implements Control, RenderingRequestCaller { } } - @Override - public void available(RenderingRequest request) { - Bitmap bmap = request.getBitmap(); - if (bmap == null) { - return; - } - - try { - ImageButton button = mIconButton.get(mProcessingButton); - button.setImageBitmap(bmap); - } catch (Exception e) { - return; - } - - mProcessingButton++; - if (mProcessingButton < mParameter.getNumberOfStyles()) - mParameter.getIcon(mProcessingButton, this); - } - } diff --git a/src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java index e94d1ed9e..74ce7cdd5 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java +++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java @@ -13,11 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.gallery3d.filtershow.imageshow; +package com.android.gallery3d.filtershow.crop; import android.graphics.Matrix; +import android.graphics.Rect; import android.graphics.RectF; +import com.android.gallery3d.filtershow.imageshow.GeometryMath; + import java.util.Arrays; /** @@ -30,11 +33,14 @@ public class BoundedRect { private RectF inner; private float[] innerRotated; - public BoundedRect() { - rot = 0; - outer = new RectF(); - inner = new RectF(); - innerRotated = new float[8]; + public BoundedRect(float rotation, Rect outerRect, Rect innerRect) { + rot = rotation; + outer = new RectF(outerRect); + inner = new RectF(innerRect); + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); } public BoundedRect(float rotation, RectF outerRect, RectF innerRect) { @@ -47,6 +53,16 @@ public class BoundedRect { reconstrain(); } + public void resetTo(float rotation, RectF outerRect, RectF innerRect) { + rot = rotation; + outer.set(outerRect); + inner.set(innerRect); + innerRotated = CropMath.getCornersFromRect(inner); + rotateInner(); + if (!isConstrained()) + reconstrain(); + } + /** * Sets inner, and re-constrains it to fit within the rotated bounding rect. */ @@ -73,10 +89,22 @@ public class BoundedRect { reconstrain(); } + public void setToInner(RectF r) { + r.set(inner); + } + + public void setToOuter(RectF r) { + r.set(outer); + } + public RectF getInner() { return new RectF(inner); } + public RectF getOuter() { + return new RectF(outer); + } + /** * Tries to move the inner rectangle by (dx, dy). If this would cause it to leave * the bounding rectangle, snaps the inner rectangle to the edge of the bounding diff --git a/src/com/android/gallery3d/filtershow/crop/CropActivity.java b/src/com/android/gallery3d/filtershow/crop/CropActivity.java new file mode 100644 index 000000000..26659a600 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java @@ -0,0 +1,422 @@ +/* + * 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.gallery3d.filtershow.crop; + +import android.app.ActionBar; +import android.app.Activity; +import android.app.WallpaperManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.provider.MediaStore; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.Display; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.widget.Toast; + +import com.android.gallery3d.R; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Activity for cropping an image. + */ +public class CropActivity extends Activity { + private static final String LOGTAG = "CropActivity"; + private CropExtras mCropExtras = null; + private LoadBitmapTask mLoadBitmapTask = null; + private SaveBitmapTask mSaveBitmapTask = null; + private SetWallpaperTask mSetWallpaperTask = null; + private Bitmap mOriginalBitmap = null; + private CropView mCropView = null; + private int mActiveBackgroundIO = 0; + private Intent mResultIntent = null; + private static final int SELECT_PICTURE = 1; // request code for picker + private static final int DEFAULT_DENSITY = 133; + private static final int DEFAULT_COMPRESS_QUALITY = 90; + public static final int MAX_BMAP_IN_INTENT = 990000; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + mResultIntent = new Intent(); + setResult(RESULT_CANCELED, mResultIntent); + mCropExtras = getExtrasFromIntent(intent); + if (mCropExtras != null && mCropExtras.getShowWhenLocked()) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); + } + + setContentView(R.layout.crop_activity); + mCropView = (CropView) findViewById(R.id.cropView); + + if (intent.getData() != null) { + startLoadBitmap(intent.getData()); + } else { + pickImage(); + } + ActionBar actionBar = getActionBar(); + actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + actionBar.setCustomView(R.layout.filtershow_actionbar); + + View saveButton = actionBar.getCustomView(); + saveButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + startFinishOutput(); + } + }); + } + + @Override + protected void onDestroy() { + if (mLoadBitmapTask != null) { + mLoadBitmapTask.cancel(false); + } + super.onDestroy(); + } + + /** + * Opens a selector in Gallery to chose an image for use when none was given + * in the CROP intent. + */ + public void pickImage() { + Intent intent = new Intent(); + intent.setType("image/*"); + intent.setAction(Intent.ACTION_GET_CONTENT); + startActivityForResult(Intent.createChooser(intent, getString(R.string.select_image)), + SELECT_PICTURE); + } + + /** + * Callback for pickImage(). + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode == RESULT_OK && requestCode == SELECT_PICTURE) { + Uri selectedImageUri = data.getData(); + startLoadBitmap(selectedImageUri); + } + } + + /** + * Gets the crop extras from the intent, or null if none exist. + */ + public static CropExtras getExtrasFromIntent(Intent intent) { + Bundle extras = intent.getExtras(); + if (extras != null) { + return new CropExtras(extras.getInt(CropExtras.KEY_OUTPUT_X, 0), + extras.getInt(CropExtras.KEY_OUTPUT_Y, 0), + extras.getBoolean(CropExtras.KEY_SCALE, true) && + extras.getBoolean(CropExtras.KEY_SCALE_UP_IF_NEEDED, false), + extras.getInt(CropExtras.KEY_ASPECT_X, 0), + extras.getInt(CropExtras.KEY_ASPECT_Y, 0), + extras.getBoolean(CropExtras.KEY_SET_AS_WALLPAPER, false), + extras.getBoolean(CropExtras.KEY_RETURN_DATA, false), + (Uri) extras.getParcelable(MediaStore.EXTRA_OUTPUT), + extras.getString(CropExtras.KEY_OUTPUT_FORMAT), + extras.getBoolean(CropExtras.KEY_SHOW_WHEN_LOCKED, false), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_X), + extras.getFloat(CropExtras.KEY_SPOTLIGHT_Y)); + } + return null; + } + + /** + * Gets screen size metric. + */ + private int getScreenImageSize() { + DisplayMetrics metrics = new DisplayMetrics(); + Display display = getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + display.getMetrics(metrics); + int msize = Math.min(size.x, size.y); + // TODO: WTF + return (DEFAULT_DENSITY * msize) / metrics.densityDpi + 512; + } + + /** + * Method that loads a bitmap in an async task. + */ + private void startLoadBitmap(Uri uri) { + mActiveBackgroundIO++; + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + mLoadBitmapTask = new LoadBitmapTask(); + mLoadBitmapTask.execute(uri); + } + + /** + * Method called on UI thread with loaded bitmap. + */ + private void doneLoadBitmap(Bitmap bitmap) { + mActiveBackgroundIO--; + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + mOriginalBitmap = bitmap; + // TODO: move these to dimens folder + if (bitmap != null) { + mCropView.setup(bitmap, (int) getPixelsFromDip(55), (int) getPixelsFromDip(25)); + } else { + Log.w(LOGTAG, "could not load image for cropping"); + cannotLoadImage(); + setResult(RESULT_CANCELED, mResultIntent); + done(); + } + } + + /** + * Display toast for image loading failure. + */ + private void cannotLoadImage() { + CharSequence text = getString(R.string.cannot_load_image); + Toast toast = Toast.makeText(this, text, Toast.LENGTH_SHORT); + toast.show(); + } + + /** + * AsyncTask for loading a bitmap into memory. + * + * @see #startLoadBitmap(Uri) + * @see #doneLoadBitmap(Bitmap) + */ + private class LoadBitmapTask extends AsyncTask<Uri, Void, Bitmap> { + int mBitmapSize; + Context mContext; + Rect mOriginalBounds; + + public LoadBitmapTask() { + mBitmapSize = getScreenImageSize(); + Log.v(LOGTAG, "bitmap size: " + mBitmapSize); + mContext = getApplicationContext(); + mOriginalBounds = new Rect(); + } + + @Override + protected Bitmap doInBackground(Uri... params) { + Bitmap bmap = CropLoader.getConstrainedBitmap(params[0], mContext, mBitmapSize, + mOriginalBounds); + return bmap; + } + + @Override + protected void onPostExecute(Bitmap result) { + doneLoadBitmap(result); + // super.onPostExecute(result); + } + } + + private void startSaveBitmap(Bitmap bmap, Uri uri, String format) { + if (bmap == null || uri == null) { + throw new IllegalArgumentException("bad argument to startSaveBitmap"); + } + mActiveBackgroundIO++; + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.VISIBLE); + mSaveBitmapTask = new SaveBitmapTask(uri, format); + mSaveBitmapTask.execute(bmap); + } + + private void doneSaveBitmap(Uri uri) { + mActiveBackgroundIO--; + final View loading = findViewById(R.id.loading); + loading.setVisibility(View.GONE); + if (uri == null) { + Log.w(LOGTAG, "failed to save bitmap"); + setResult(RESULT_CANCELED, mResultIntent); + done(); + return; + } + done(); + } + + private class SaveBitmapTask extends AsyncTask<Bitmap, Void, Boolean> { + + OutputStream mOutStream = null; + String mOutputFormat = null; + Uri mOutUri = null; + + public SaveBitmapTask(Uri uri, String outputFormat) { + mOutputFormat = outputFormat; + mOutStream = null; + mOutUri = uri; + try { + mOutStream = getContentResolver().openOutputStream(uri); + } catch (FileNotFoundException e) { + Log.w(LOGTAG, "cannot write output: " + mOutUri.toString(), e); + } + } + + @Override + protected Boolean doInBackground(Bitmap... params) { + if (mOutStream == null) { + return false; + } + CompressFormat cf = convertExtensionToCompressFormat(getFileExtension(mOutputFormat)); + return params[0].compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream); + } + + @Override + protected void onPostExecute(Boolean result) { + if (result.booleanValue() == false) { + Log.w(LOGTAG, "could not compress to output: " + mOutUri.toString()); + doneSaveBitmap(null); + } + doneSaveBitmap(mOutUri); + } + } + + private void startSetWallpaper(Bitmap bmap) { + if (bmap == null) { + throw new IllegalArgumentException("bad argument to startSetWallpaper"); + } + mActiveBackgroundIO++; + Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show(); + mSetWallpaperTask = new SetWallpaperTask(); + mSetWallpaperTask.execute(bmap); + + } + + private void doneSetWallpaper() { + mActiveBackgroundIO--; + done(); + } + + private class SetWallpaperTask extends AsyncTask<Bitmap, Void, Boolean> { + private final WallpaperManager mWPManager; + + public SetWallpaperTask() { + mWPManager = WallpaperManager.getInstance(getApplicationContext()); + } + + @Override + protected Boolean doInBackground(Bitmap... params) { + try { + mWPManager.setBitmap(params[0]); + } catch (IOException e) { + Log.w(LOGTAG, "fail to set wall paper", e); + } + return true; + } + + @Override + protected void onPostExecute(Boolean result) { + doneSetWallpaper(); + } + } + + private void startFinishOutput() { + if (mOriginalBitmap != null && mCropExtras != null) { + Bitmap cropped = null; + if (mCropExtras.getExtraOutput() != null) { + if (cropped == null) { + cropped = getCroppedImage(mOriginalBitmap); + } + startSaveBitmap(cropped, mCropExtras.getExtraOutput(), + mCropExtras.getOutputFormat()); + } + if (mCropExtras.getSetAsWallpaper()) { + if (cropped == null) { + cropped = getCroppedImage(mOriginalBitmap); + } + startSetWallpaper(cropped); + } + if (mCropExtras.getReturnData()) { + if (cropped == null) { + cropped = getCroppedImage(mOriginalBitmap); + } + int bmapSize = cropped.getRowBytes() * cropped.getHeight(); + if (bmapSize > MAX_BMAP_IN_INTENT) { + Log.w(LOGTAG, "Bitmap too large to be returned via intent"); + } else { + mResultIntent.putExtra(CropExtras.KEY_DATA, cropped); + } + } + setResult(RESULT_OK, mResultIntent); + } else { + setResult(RESULT_CANCELED, mResultIntent); + } + done(); + } + + private void done() { + if (mActiveBackgroundIO == 0) { + finish(); + } + } + + private Bitmap getCroppedImage(Bitmap image) { + RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight()); + RectF crop = getBitmapCrop(imageBounds); + if (crop == null) { + return image; + } + Rect intCrop = new Rect(); + crop.roundOut(intCrop); + return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(), + intCrop.height()); + } + + private RectF getBitmapCrop(RectF imageBounds) { + RectF crop = new RectF(); + if (!mCropView.getCropBounds(crop, imageBounds)) { + Log.w(LOGTAG, "could not get crop"); + return null; + } + return crop; + } + + /** + * Helper method for unit conversions. + */ + public float getPixelsFromDip(float value) { + Resources r = getResources(); + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, + r.getDisplayMetrics()); + } + + private static CompressFormat convertExtensionToCompressFormat(String extension) { + return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG; + } + + private static String getFileExtension(String requestFormat) { + String outputFormat = (requestFormat == null) + ? "jpg" + : requestFormat; + outputFormat = outputFormat.toLowerCase(); + return (outputFormat.equals("png") || outputFormat.equals("gif")) + ? "png" // We don't support gif compression. + : "jpg"; + } + +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java new file mode 100644 index 000000000..749ba0e29 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java @@ -0,0 +1,106 @@ +/* + * 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.gallery3d.filtershow.crop; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; + +public abstract class CropDrawingUtils { + + public static void drawRuleOfThird(Canvas canvas, RectF bounds) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setColor(Color.argb(128, 255, 255, 255)); + p.setStrokeWidth(2); + float stepX = bounds.width() / 3.0f; + float stepY = bounds.height() / 3.0f; + float x = bounds.left + stepX; + float y = bounds.top + stepY; + for (int i = 0; i < 2; i++) { + canvas.drawLine(x, bounds.top, x, bounds.bottom, p); + x += stepX; + } + for (int j = 0; j < 2; j++) { + canvas.drawLine(bounds.left, y, bounds.right, y, p); + y += stepY; + } + } + + public static void drawCropRect(Canvas canvas, RectF bounds) { + Paint p = new Paint(); + p.setStyle(Paint.Style.STROKE); + p.setColor(Color.WHITE); + p.setStrokeWidth(3); + canvas.drawRect(bounds, p); + } + + public static void drawIndicator(Canvas canvas, Drawable indicator, int indicatorSize, + float centerX, float centerY) { + int left = (int) centerX - indicatorSize / 2; + int top = (int) centerY - indicatorSize / 2; + indicator.setBounds(left, top, left + indicatorSize, top + indicatorSize); + indicator.draw(canvas); + } + + public static void drawIndicators(Canvas canvas, Drawable cropIndicator, int indicatorSize, + RectF bounds, boolean fixedAspect, int selection) { + boolean notMoving = (selection == CropObject.MOVE_NONE); + if (fixedAspect) { + if ((selection == CropObject.TOP_LEFT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.top); + } + if ((selection == CropObject.TOP_RIGHT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.top); + } + if ((selection == CropObject.BOTTOM_LEFT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.bottom); + } + if ((selection == CropObject.BOTTOM_RIGHT) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.bottom); + } + } else { + if (((selection & CropObject.MOVE_TOP) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.top); + } + if (((selection & CropObject.MOVE_BOTTOM) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.centerX(), bounds.bottom); + } + if (((selection & CropObject.MOVE_LEFT) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.left, bounds.centerY()); + } + if (((selection & CropObject.MOVE_RIGHT) != 0) || notMoving) { + drawIndicator(canvas, cropIndicator, indicatorSize, bounds.right, bounds.centerY()); + } + } + } + + public static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) { + Matrix m = new Matrix(); + CropDrawingUtils.setBitmapToDisplayMatrix(m, imageBounds, displayBounds); + return m; + } + + public static boolean setBitmapToDisplayMatrix(Matrix m, RectF imageBounds, + RectF displayBounds) { + m.reset(); + return m.setRectToRect(imageBounds, displayBounds, Matrix.ScaleToFit.CENTER); + } + +} diff --git a/src/com/android/gallery3d/filtershow/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java index 7ed8f1eb5..60fe9af53 100644 --- a/src/com/android/gallery3d/filtershow/CropExtras.java +++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.gallery3d.filtershow; +package com.android.gallery3d.filtershow.crop; import android.net.Uri; diff --git a/src/com/android/gallery3d/filtershow/crop/CropLoader.java b/src/com/android/gallery3d/filtershow/crop/CropLoader.java new file mode 100644 index 000000000..132d6c1dc --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropLoader.java @@ -0,0 +1,202 @@ +/* + * 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.gallery3d.filtershow.crop; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteException; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.MediaStore; +import android.util.Log; + +import com.android.gallery3d.common.Utils; +import com.android.gallery3d.exif.ExifInterface; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * This class contains static methods for loading a bitmap and + * mantains no instance state. + */ +public abstract class CropLoader { + public static final String LOGTAG = "CropLoader"; + public static final String JPEG_MIME_TYPE = "image/jpeg"; + public static final int ORI_NORMAL = ExifInterface.Orientation.TOP_LEFT; + public static final int ORI_ROTATE_90 = ExifInterface.Orientation.RIGHT_TOP; + public static final int ORI_ROTATE_180 = ExifInterface.Orientation.BOTTOM_LEFT; + public static final int ORI_ROTATE_270 = ExifInterface.Orientation.RIGHT_BOTTOM; + + /** + * Returns the orientation of image at the given URI as one of 0, 90, 180, + * 270. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @return the orientation of the image. Defaults to 0. + */ + public static int getMetadataOrientation(Uri uri, Context context) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + String mimeType = context.getContentResolver().getType(uri); + if (mimeType != JPEG_MIME_TYPE) { + return 0; + } + String path = uri.getPath(); + int orientation = 0; + ExifInterface exif = new ExifInterface(); + try { + exif.readExif(path); + orientation = ExifInterface.getRotationForOrientationValue( + exif.getTagIntValue(ExifInterface.TAG_ORIENTATION).shortValue()); + } catch (IOException e) { + Log.w(LOGTAG, "Failed to read EXIF orientation", e); + } + return orientation; + } + Cursor cursor = null; + try { + cursor = context.getContentResolver().query(uri, + new String[] { + MediaStore.Images.ImageColumns.ORIENTATION + }, + null, null, null); + if (cursor.moveToNext()) { + int ori = cursor.getInt(0); + + switch (ori) { + case 0: + return ORI_NORMAL; + case 90: + return ORI_ROTATE_90; + case 270: + return ORI_ROTATE_270; + case 180: + return ORI_ROTATE_180; + default: + return 0; + } + } + } catch (SQLiteException e) { + return 0; + } catch (IllegalArgumentException e) { + return 0; + } finally { + Utils.closeSilently(cursor); + } + return 0; + } + + /** + * Gets a bitmap at a given URI that is downsampled so that both sides are + * smaller than maxSideLength. The Bitmap's original dimensions are stored + * in the rect originalBounds. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param maxSideLength max side length of returned bitmap. + * @param originalBounds set to the actual bounds of the stored bitmap. + * @return downsampled bitmap or null if this operation failed. + */ + public static Bitmap getConstrainedBitmap(Uri uri, Context context, int maxSideLength, + Rect originalBounds) { + if (maxSideLength <= 0 || originalBounds == null || uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + InputStream is = null; + try { + // Get width and height of stored bitmap + is = context.getContentResolver().openInputStream(uri); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(is, null, options); + int w = options.outWidth; + int h = options.outHeight; + originalBounds.set(0, 0, w, h); + + // If bitmap cannot be decoded, return null + if (w <= 0 || h <= 0) { + return null; + } + + options = new BitmapFactory.Options(); + + // Find best downsampling size + int imageSide = Math.max(w, h); + options.inSampleSize = 1; + if (imageSide > maxSideLength) { + int shifts = 1 + Integer.numberOfLeadingZeros(maxSideLength) + - Integer.numberOfLeadingZeros(imageSide); + options.inSampleSize <<= shifts; + } + + // Make sure sample size is reasonable + if (options.inSampleSize <= 0 || + 0 >= (int) (Math.min(w, h) / options.inSampleSize)) { + return null; + } + + // Decode actual bitmap. + options.inMutable = true; + is.close(); + is = context.getContentResolver().openInputStream(uri); + return BitmapFactory.decodeStream(is, null, options); + } catch (FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException: " + uri, e); + } catch (IOException e) { + Log.e(LOGTAG, "IOException: " + uri, e); + } finally { + Utils.closeSilently(is); + } + return null; + } + + /** + * Gets a bitmap that has been downsampled using sampleSize. + * + * @param uri URI of image to open. + * @param context context whose ContentResolver to use. + * @param sampleSize downsampling amount. + * @return downsampled bitmap. + */ + public static Bitmap getBitmap(Uri uri, Context context, int sampleSize) { + if (uri == null || context == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + InputStream is = null; + try { + is = context.getContentResolver().openInputStream(uri); + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inMutable = true; + options.inSampleSize = sampleSize; + return BitmapFactory.decodeStream(is, null, options); + } catch (FileNotFoundException e) { + Log.e(LOGTAG, "FileNotFoundException: " + uri, e); + } finally { + Utils.closeSilently(is); + } + return null; + } + +} diff --git a/src/com/android/gallery3d/filtershow/imageshow/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java index 9037ca043..5914f1cb8 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/CropMath.java +++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java @@ -14,11 +14,13 @@ * limitations under the License. */ -package com.android.gallery3d.filtershow.imageshow; +package com.android.gallery3d.filtershow.crop; import android.graphics.Matrix; import android.graphics.RectF; +import com.android.gallery3d.filtershow.imageshow.GeometryMath; + import java.util.Arrays; public class CropMath { @@ -176,6 +178,33 @@ public class CropMath { r.set(centX - hw, centY - hh, centX + hw, centY + hh); } + /** + * Resizes rectangle to have a certain aspect ratio (center remains + * stationary) while constraining it to remain within the original rect. + * + * @param r rectangle to resize + * @param w new width aspect + * @param h new height aspect + */ + public static void fixAspectRatioContained(RectF r, float w, float h) { + float origW = r.width(); + float origH = r.height(); + float origA = origW / origH; + float a = w / h; + float finalW = origW; + float finalH = origH; + if (origA < a) { + finalH = origH / a; + } else { + finalW = origW * a; + } + float centX = r.centerX(); + float centY = r.centerY(); + float hw = finalW / 2; + float hh = finalH / 2; + r.set(centX - hw, centY - hh, centX + hw, centY + hh); + } + private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) { float dy = rotatedRect[1] - rotatedRect[3]; float dx = rotatedRect[0] - rotatedRect[2]; diff --git a/src/com/android/gallery3d/filtershow/crop/CropObject.java b/src/com/android/gallery3d/filtershow/crop/CropObject.java new file mode 100644 index 000000000..08cf158e1 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java @@ -0,0 +1,331 @@ +/* + * 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.gallery3d.filtershow.crop; + +import android.graphics.Rect; +import android.graphics.RectF; + +import com.android.gallery3d.filtershow.imageshow.GeometryMath; + +public class CropObject { + + private BoundedRect mBoundedRect; + private float mAspectWidth = 1; + private float mAspectHeight = 1; + private boolean mFixAspectRatio = false; + private float mRotation = 0; + private float mTouchTolerance = 45; + private float mMinSideSize = 20; + + public static final int MOVE_NONE = 0; + // Sides + public static final int MOVE_LEFT = 1; + public static final int MOVE_TOP = 2; + public static final int MOVE_RIGHT = 4; + public static final int MOVE_BOTTOM = 8; + public static final int MOVE_BLOCK = 16; + + // Corners + public static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT; + public static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT; + public static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT; + public static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT; + + private int mMovingEdges = MOVE_NONE; + + public CropObject(Rect outerBound, Rect innerBound, int outerAngle) { + mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound); + } + + public CropObject(RectF outerBound, RectF innerBound, int outerAngle) { + mBoundedRect = new BoundedRect(outerAngle % 360, outerBound, innerBound); + } + + public void resetBoundsTo(RectF inner, RectF outer) { + mBoundedRect.resetTo(0, outer, inner); + } + + public void getInnerBounds(RectF r) { + mBoundedRect.setToInner(r); + } + + public void getOuterBounds(RectF r) { + mBoundedRect.setToOuter(r); + } + + public RectF getInnerBounds() { + return mBoundedRect.getInner(); + } + + public RectF getOuterBounds() { + return mBoundedRect.getOuter(); + } + + public int getSelectState() { + return mMovingEdges; + } + + public boolean isFixedAspect() { + return mFixAspectRatio; + } + + public void rotateOuter(int angle) { + mRotation = angle % 360; + mBoundedRect.setRotation(mRotation); + clearSelectState(); + } + + public boolean setInnerAspectRatio(int width, int height) { + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Width and Height must be greater than zero"); + } + RectF inner = mBoundedRect.getInner(); + CropMath.fixAspectRatioContained(inner, width, height); + if (inner.width() < mMinSideSize || inner.height() < mMinSideSize) { + return false; + } + mAspectWidth = width; + mAspectHeight = height; + mFixAspectRatio = true; + mBoundedRect.setInner(inner); + clearSelectState(); + return true; + } + + public void setTouchTolerance(float tolerance) { + if (tolerance <= 0) { + throw new IllegalArgumentException("Tolerance must be greater than zero"); + } + mTouchTolerance = tolerance; + } + + public void setMinInnerSideSize(float minSide) { + if (minSide <= 0) { + throw new IllegalArgumentException("Min dide must be greater than zero"); + } + mMinSideSize = minSide; + } + + public void unsetAspectRatio() { + mFixAspectRatio = false; + clearSelectState(); + } + + public boolean hasSelectedEdge() { + return mMovingEdges != MOVE_NONE; + } + + public static boolean checkCorner(int selected) { + return selected == TOP_LEFT || selected == TOP_RIGHT || selected == BOTTOM_RIGHT + || selected == BOTTOM_LEFT; + } + + public static boolean checkEdge(int selected) { + return selected == MOVE_LEFT || selected == MOVE_TOP || selected == MOVE_RIGHT + || selected == MOVE_BOTTOM; + } + + public static boolean checkBlock(int selected) { + return selected == MOVE_BLOCK; + } + + public static boolean checkValid(int selected) { + return selected == MOVE_NONE || checkBlock(selected) || checkEdge(selected) + || checkCorner(selected); + } + + public void clearSelectState() { + mMovingEdges = MOVE_NONE; + } + + public int wouldSelectEdge(float x, float y) { + int edgeSelected = calculateSelectedEdge(x, y); + if (edgeSelected != MOVE_NONE && edgeSelected != MOVE_BLOCK) { + return edgeSelected; + } + return MOVE_NONE; + } + + public boolean selectEdge(int edge) { + if (!checkValid(edge)) { + // temporary + throw new IllegalArgumentException("bad edge selected"); + // return false; + } + if ((mFixAspectRatio && !checkCorner(edge)) && !checkBlock(edge)) { + // temporary + throw new IllegalArgumentException("bad corner selected"); + // return false; + } + mMovingEdges = edge; + return true; + } + + public boolean selectEdge(float x, float y) { + int edgeSelected = calculateSelectedEdge(x, y); + if (mFixAspectRatio) { + edgeSelected = fixEdgeToCorner(edgeSelected); + } + if (edgeSelected == MOVE_NONE) { + return false; + } + return selectEdge(edgeSelected); + } + + public boolean moveCurrentSelection(float dX, float dY) { + if (mMovingEdges == MOVE_NONE) { + return false; + } + RectF crop = mBoundedRect.getInner(); + + float minWidthHeight = mMinSideSize; + + int movingEdges = mMovingEdges; + if (movingEdges == MOVE_BLOCK) { + mBoundedRect.moveInner(dX, dY); + return true; + } else { + float dx = 0; + float dy = 0; + + if ((movingEdges & MOVE_LEFT) != 0) { + dx = Math.min(crop.left + dX, crop.right - minWidthHeight) - crop.left; + } + if ((movingEdges & MOVE_TOP) != 0) { + dy = Math.min(crop.top + dY, crop.bottom - minWidthHeight) - crop.top; + } + if ((movingEdges & MOVE_RIGHT) != 0) { + dx = Math.max(crop.right + dX, crop.left + minWidthHeight) + - crop.right; + } + if ((movingEdges & MOVE_BOTTOM) != 0) { + dy = Math.max(crop.bottom + dY, crop.top + minWidthHeight) + - crop.bottom; + } + + if (mFixAspectRatio) { + float[] l1 = { + crop.left, crop.bottom + }; + float[] l2 = { + crop.right, crop.top + }; + if (movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT) { + l1[1] = crop.top; + l2[1] = crop.bottom; + } + float[] b = { + l1[0] - l2[0], l1[1] - l2[1] + }; + float[] disp = { + dx, dy + }; + float[] bUnit = GeometryMath.normalize(b); + float sp = GeometryMath.scalarProjection(disp, bUnit); + dx = sp * bUnit[0]; + dy = sp * bUnit[1]; + RectF newCrop = fixedCornerResize(crop, movingEdges, dx, dy); + + mBoundedRect.fixedAspectResizeInner(newCrop); + } else { + if ((movingEdges & MOVE_LEFT) != 0) { + crop.left += dx; + } + if ((movingEdges & MOVE_TOP) != 0) { + crop.top += dy; + } + if ((movingEdges & MOVE_RIGHT) != 0) { + crop.right += dx; + } + if ((movingEdges & MOVE_BOTTOM) != 0) { + crop.bottom += dy; + } + mBoundedRect.resizeInner(crop); + } + } + return true; + } + + // Helper methods + + private int calculateSelectedEdge(float x, float y) { + RectF cropped = mBoundedRect.getInner(); + + float left = Math.abs(x - cropped.left); + float right = Math.abs(x - cropped.right); + float top = Math.abs(y - cropped.top); + float bottom = Math.abs(y - cropped.bottom); + + int edgeSelected = MOVE_NONE; + // Check left or right. + if ((left <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top) + && ((y - mTouchTolerance) <= cropped.bottom) && (left < right)) { + edgeSelected |= MOVE_LEFT; + } + else if ((right <= mTouchTolerance) && ((y + mTouchTolerance) >= cropped.top) + && ((y - mTouchTolerance) <= cropped.bottom)) { + edgeSelected |= MOVE_RIGHT; + } + + // Check top or bottom. + if ((top <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left) + && ((x - mTouchTolerance) <= cropped.right) && (top < bottom)) { + edgeSelected |= MOVE_TOP; + } + else if ((bottom <= mTouchTolerance) && ((x + mTouchTolerance) >= cropped.left) + && ((x - mTouchTolerance) <= cropped.right)) { + edgeSelected |= MOVE_BOTTOM; + } + return edgeSelected; + } + + private static RectF fixedCornerResize(RectF r, int moving_corner, float dx, float dy) { + RectF newCrop = null; + // Fix opposite corner in place and move sides + if (moving_corner == BOTTOM_RIGHT) { + newCrop = new RectF(r.left, r.top, r.left + r.width() + dx, r.top + r.height() + + dy); + } else if (moving_corner == BOTTOM_LEFT) { + newCrop = new RectF(r.right - r.width() + dx, r.top, r.right, r.top + r.height() + + dy); + } else if (moving_corner == TOP_LEFT) { + newCrop = new RectF(r.right - r.width() + dx, r.bottom - r.height() + dy, + r.right, r.bottom); + } else if (moving_corner == TOP_RIGHT) { + newCrop = new RectF(r.left, r.bottom - r.height() + dy, r.left + + r.width() + dx, r.bottom); + } + return newCrop; + } + + private static int fixEdgeToCorner(int moving_edges) { + if (moving_edges == MOVE_LEFT) { + moving_edges |= MOVE_TOP; + } + if (moving_edges == MOVE_TOP) { + moving_edges |= MOVE_LEFT; + } + if (moving_edges == MOVE_RIGHT) { + moving_edges |= MOVE_BOTTOM; + } + if (moving_edges == MOVE_BOTTOM) { + moving_edges |= MOVE_RIGHT; + } + return moving_edges; + } + +} diff --git a/src/com/android/gallery3d/filtershow/crop/CropView.java b/src/com/android/gallery3d/filtershow/crop/CropView.java new file mode 100644 index 000000000..d762ad459 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropView.java @@ -0,0 +1,195 @@ +/* + * 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.gallery3d.filtershow.crop; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +import com.android.gallery3d.R; + +public class CropView extends View { + private static final String LOGTAG = "CropView"; + + Bitmap mImage = null; + CropObject mCropObj = null; + private final Drawable mCropIndicator; + private final int mIndicatorSize; + + private float mPrevX = 0; + private float mPrevY = 0; + + private int mMinSideSize = 45; + private int mTouchTolerance = 20; + private boolean mMovingBlock = false; + + private Matrix mDisplayMatrix = null; + private Matrix mDisplayMatrixInverse = null; + + private enum Mode { + NONE, MOVE + } + + private Mode mState = Mode.NONE; + + public CropView(Context context, AttributeSet attrs) { + super(context, attrs); + Resources resources = context.getResources(); + mCropIndicator = resources.getDrawable(R.drawable.camera_crop); + mIndicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size); + } + + // For unchanging parameters + public void setup(Bitmap image, int minSideSize, int touchTolerance) { + mImage = image; + mMinSideSize = minSideSize; + mTouchTolerance = touchTolerance; + reset(); + } + + @Override + public void onDraw(Canvas canvas) { + if (mImage == null) { + return; + } + int displayWidth = getWidth(); + int displayHeight = getHeight(); + Rect imageBoundsOriginal = new Rect(0, 0, mImage.getWidth(), mImage.getHeight()); + Rect displayBoundsOriginal = new Rect(0, 0, displayWidth, displayHeight); + if (mCropObj == null) { + reset(); + mCropObj = new CropObject(imageBoundsOriginal, imageBoundsOriginal, 0); + } + + RectF imageBounds = mCropObj.getInnerBounds(); + RectF displayBounds = mCropObj.getOuterBounds(); + + // If display matrix doesn't exist, create it and its dependencies + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + mDisplayMatrix = CropDrawingUtils.getBitmapToDisplayMatrix(displayBounds, new RectF( + displayBoundsOriginal)); + mDisplayMatrixInverse = new Matrix(); + mDisplayMatrixInverse.reset(); + if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) { + Log.w(LOGTAG, "could not invert display matrix"); + } + // Scale min side and tolerance by display matrix scale factor + mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize)); + mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance)); + } + canvas.drawBitmap(mImage, mDisplayMatrix, new Paint()); + + if (mDisplayMatrix.mapRect(imageBounds)) { + CropDrawingUtils.drawCropRect(canvas, imageBounds); + CropDrawingUtils.drawRuleOfThird(canvas, imageBounds); + CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize, imageBounds, + mCropObj.isFixedAspect(), mCropObj.getSelectState()); + } + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + float x = event.getX(); + float y = event.getY(); + if (mDisplayMatrix == null || mDisplayMatrixInverse == null) { + return true; + } + float[] touchPoint = { + x, y + }; + mDisplayMatrixInverse.mapPoints(touchPoint); + x = touchPoint[0]; + y = touchPoint[1]; + switch (event.getActionMasked()) { + case (MotionEvent.ACTION_DOWN): + if (mState == Mode.NONE) { + if (!mCropObj.selectEdge(x, y)) { + mMovingBlock = mCropObj.selectEdge(CropObject.MOVE_BLOCK); + } + mPrevX = x; + mPrevY = y; + mState = Mode.MOVE; + } else { + reset(); + } + break; + case (MotionEvent.ACTION_UP): + if (mState == Mode.MOVE) { + mCropObj.selectEdge(CropObject.MOVE_NONE); + mMovingBlock = false; + mPrevX = x; + mPrevY = y; + mState = Mode.NONE; + } else { + reset(); + } + break; + case (MotionEvent.ACTION_MOVE): + if (mState == Mode.MOVE) { + float dx = x - mPrevX; + float dy = y - mPrevY; + mCropObj.moveCurrentSelection(dx, dy); + mPrevX = x; + mPrevY = y; + } else { + reset(); + } + break; + default: + reset(); + break; + } + invalidate(); + return true; + } + + public void reset() { + Log.w(LOGTAG, "reset called"); + mState = Mode.NONE; + mCropObj = null; + mDisplayMatrix = null; + mDisplayMatrixInverse = null; + mMovingBlock = false; + invalidate(); + } + + public boolean getCropBounds(RectF out_crop, RectF in_newContaining) { + Matrix m = new Matrix(); + RectF inner = mCropObj.getInnerBounds(); + RectF outer = mCropObj.getOuterBounds(); + if (!m.setRectToRect(outer, in_newContaining, Matrix.ScaleToFit.FILL)) { + Log.w(LOGTAG, "failed to make transform matrix"); + return false; + } + if (!m.mapRect(inner)) { + Log.w(LOGTAG, "failed to transform crop bounds"); + return false; + } + out_crop.set(inner); + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/editors/Editor.java b/src/com/android/gallery3d/filtershow/editors/Editor.java index 013590c6d..036745d3c 100644 --- a/src/com/android/gallery3d/filtershow/editors/Editor.java +++ b/src/com/android/gallery3d/filtershow/editors/Editor.java @@ -18,8 +18,10 @@ package com.android.gallery3d.filtershow.editors; import android.content.Context; import android.util.AttributeSet; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -41,7 +43,7 @@ import com.android.gallery3d.filtershow.presets.ImagePreset; /** * Base class for Editors Must contain a mImageShow and a top level view */ -public class Editor implements OnSeekBarChangeListener { +public class Editor implements OnSeekBarChangeListener, SwapButton.SwapButtonListener { protected Context mContext; protected View mView; protected ImageShow mImageShow; @@ -79,7 +81,6 @@ public class Editor implements OnSeekBarChangeListener { return mID; } - public byte showParameterValue() { return mShowParameter; } @@ -260,4 +261,17 @@ public class Editor implements OnSeekBarChangeListener { } + @Override + public void swapLeft(MenuItem item) { + + } + + @Override + public void swapRight(MenuItem item) { + + } + + public void detach() { + + } } diff --git a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java index df922f10a..e2173ad77 100644 --- a/src/com/android/gallery3d/filtershow/editors/EditorCrop.java +++ b/src/com/android/gallery3d/filtershow/editors/EditorCrop.java @@ -21,7 +21,7 @@ import android.view.View; import android.widget.FrameLayout; import com.android.gallery3d.R; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.filtershow.imageshow.ImageCrop; import com.android.gallery3d.filtershow.imageshow.MasterImage; diff --git a/src/com/android/gallery3d/filtershow/editors/SwapButton.java b/src/com/android/gallery3d/filtershow/editors/SwapButton.java new file mode 100644 index 000000000..d8f3fc3a7 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/editors/SwapButton.java @@ -0,0 +1,119 @@ +/* + * 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.gallery3d.filtershow.editors; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.widget.Button; + +public class SwapButton extends Button implements GestureDetector.OnGestureListener { + + public static int ANIM_DURATION = 200; + + public interface SwapButtonListener { + public void swapLeft(MenuItem item); + public void swapRight(MenuItem item); + } + + private GestureDetector mDetector; + private SwapButtonListener mListener; + private Menu mMenu; + private int mCurrentMenuIndex; + + public SwapButton(Context context, AttributeSet attrs) { + super(context, attrs); + mDetector = new GestureDetector(context, this); + } + + public SwapButtonListener getListener() { + return mListener; + } + + public void setListener(SwapButtonListener listener) { + mListener = listener; + } + + public Menu getMenu() { + return mMenu; + } + + public void setMenu(Menu menu) { + mMenu = menu; + } + + public boolean onTouchEvent(MotionEvent me) { + if (!mDetector.onTouchEvent(me)) { + return super.onTouchEvent(me); + } + return true; + } + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public void onShowPress(MotionEvent e) { + } + + @Override + public boolean onSingleTapUp(MotionEvent e) { + callOnClick(); + return true; + } + + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { + return false; + } + + @Override + public void onLongPress(MotionEvent e) { + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (mMenu == null) { + return false; + } + if (e1.getX() - e2.getX() > 0) { + // right to left + mCurrentMenuIndex++; + if (mCurrentMenuIndex == mMenu.size()) { + mCurrentMenuIndex = 0; + } + if (mListener != null) { + mListener.swapRight(mMenu.getItem(mCurrentMenuIndex)); + } + } else { + // left to right + mCurrentMenuIndex--; + if (mCurrentMenuIndex < 0) { + mCurrentMenuIndex = mMenu.size() - 1; + } + if (mListener != null) { + mListener.swapLeft(mMenu.getItem(mCurrentMenuIndex)); + } + } + return true; + } +} diff --git a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java index 232de6460..66ad10640 100644 --- a/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java +++ b/src/com/android/gallery3d/filtershow/filters/BaseFiltersManager.java @@ -15,6 +15,7 @@ */ package com.android.gallery3d.filtershow.filters; +import android.content.res.Resources; import com.android.gallery3d.filtershow.presets.ImagePreset; import java.util.HashMap; @@ -140,4 +141,10 @@ public abstract class BaseFiltersManager { representations.add(getRepresentation(ImageFilterDraw.class)); } + public void setFilterResources(Resources resources) { + ImageFilterBorder filterBorder = (ImageFilterBorder) getFilter(ImageFilterBorder.class); + filterBorder.setResources(resources); + ImageFilterFx filterFx = (ImageFilterFx) getFilter(ImageFilterFx.class); + filterFx.setResources(resources); + } } diff --git a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java index 7e0e25d61..5bb0e5733 100644 --- a/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java +++ b/src/com/android/gallery3d/filtershow/filters/FilterRepresentation.java @@ -42,7 +42,7 @@ public class FilterRepresentation implements Cloneable { public static final byte TYPE_NORMAL = 5; public static final byte TYPE_TINYPLANET = 6; - public FilterRepresentation mTempRepresentation = null; + private FilterRepresentation mTempRepresentation = null; public FilterRepresentation(String name) { mName = name; @@ -127,6 +127,10 @@ public class FilterRepresentation implements Cloneable { public void useParametersFrom(FilterRepresentation a) { } + public void clearTempRepresentation() { + mTempRepresentation = null; + } + public synchronized void updateTempParametersFrom(FilterRepresentation representation) { if (mTempRepresentation == null) { try { diff --git a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java index 5a33cc823..4f46eed82 100644 --- a/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java +++ b/src/com/android/gallery3d/filtershow/filters/ImageFilterGeometry.java @@ -24,7 +24,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.util.Log; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.filtershow.imageshow.GeometryMath; import com.android.gallery3d.filtershow.imageshow.GeometryMetadata; diff --git a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java index ad2152ab1..898fdf021 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java +++ b/src/com/android/gallery3d/filtershow/imageshow/GeometryMetadata.java @@ -21,8 +21,8 @@ import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; -import com.android.gallery3d.filtershow.CropExtras; import com.android.gallery3d.filtershow.cache.ImageLoader; +import com.android.gallery3d.filtershow.crop.CropExtras; import com.android.gallery3d.filtershow.editors.EditorCrop; import com.android.gallery3d.filtershow.editors.EditorFlip; import com.android.gallery3d.filtershow.editors.EditorRotate; diff --git a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java index 2ea6f6a42..6d62bbd1d 100644 --- a/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java +++ b/src/com/android/gallery3d/filtershow/imageshow/ImageCrop.java @@ -33,7 +33,9 @@ import android.widget.LinearLayout; import android.widget.PopupMenu; import com.android.gallery3d.R; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.BoundedRect; +import com.android.gallery3d.filtershow.crop.CropExtras; +import com.android.gallery3d.filtershow.crop.CropMath; import com.android.gallery3d.filtershow.editors.EditorCrop; import com.android.gallery3d.filtershow.ui.FramedTextButton; diff --git a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java index a35a18a7d..2858ea6e5 100644 --- a/src/com/android/gallery3d/filtershow/presets/ImagePreset.java +++ b/src/com/android/gallery3d/filtershow/presets/ImagePreset.java @@ -39,6 +39,8 @@ public class ImagePreset { public static final int QUALITY_ICON = 0; public static final int QUALITY_PREVIEW = 1; public static final int QUALITY_FINAL = 2; + public static final int STYLE_ICON = 3; + private ImageLoader mImageLoader = null; private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>(); diff --git a/src/com/android/gallery3d/gadget/WidgetConfigure.java b/src/com/android/gallery3d/gadget/WidgetConfigure.java index 4818d261b..eb81b6ef3 100644 --- a/src/com/android/gallery3d/gadget/WidgetConfigure.java +++ b/src/com/android/gallery3d/gadget/WidgetConfigure.java @@ -36,7 +36,7 @@ import com.android.gallery3d.data.LocalAlbum; import com.android.gallery3d.data.MediaSet; import com.android.gallery3d.data.Path; import com.android.gallery3d.filtershow.FilterShowActivity; -import com.android.gallery3d.filtershow.CropExtras; +import com.android.gallery3d.filtershow.crop.CropExtras; public class WidgetConfigure extends Activity { @SuppressWarnings("unused") diff --git a/src/com/android/gallery3d/ingest/ui/MtpImageView.java b/src/com/android/gallery3d/ingest/ui/MtpImageView.java index 5664d8a34..9ab78628d 100644 --- a/src/com/android/gallery3d/ingest/ui/MtpImageView.java +++ b/src/com/android/gallery3d/ingest/ui/MtpImageView.java @@ -17,6 +17,7 @@ package com.android.gallery3d.ingest.ui; import android.content.Context; +import android.graphics.Matrix; import android.mtp.MtpDevice; import android.mtp.MtpObjectInfo; import android.os.Handler; @@ -91,10 +92,67 @@ public class MtpImageView extends ImageView { return MtpBitmapFetch.getFullsize(device, info); } + private float mLastBitmapWidth; + private float mLastBitmapHeight; + private int mLastRotationDegrees; + private Matrix mDrawMatrix = new Matrix(); + + private void updateDrawMatrix() { + mDrawMatrix.reset(); + float dwidth; + float dheight; + float vheight = getHeight(); + float vwidth = getWidth(); + float scale; + boolean rotated90 = (mLastRotationDegrees % 180 != 0); + if (rotated90) { + dwidth = mLastBitmapHeight; + dheight = mLastBitmapWidth; + } else { + dwidth = mLastBitmapWidth; + dheight = mLastBitmapHeight; + } + if (dwidth <= vwidth && dheight <= vheight) { + scale = 1.0f; + } else { + scale = Math.min(vwidth / dwidth, vheight / dheight); + } + mDrawMatrix.setScale(scale, scale); + if (rotated90) { + mDrawMatrix.postTranslate(-dheight * scale * 0.5f, + -dwidth * scale * 0.5f); + mDrawMatrix.postRotate(mLastRotationDegrees); + mDrawMatrix.postTranslate(dwidth * scale * 0.5f, + dheight * scale * 0.5f); + } + mDrawMatrix.postTranslate((vwidth - dwidth * scale) * 0.5f, + (vheight - dheight * scale) * 0.5f); + if (!rotated90 && mLastRotationDegrees > 0) { + // rotated by a multiple of 180 + mDrawMatrix.postRotate(mLastRotationDegrees, vwidth / 2, vheight / 2); + } + setImageMatrix(mDrawMatrix); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (changed && getScaleType() == ScaleType.MATRIX) { + updateDrawMatrix(); + } + } + protected void onMtpImageDataFetchedFromDevice(Object result) { BitmapWithMetadata bitmapWithMetadata = (BitmapWithMetadata)result; + if (getScaleType() == ScaleType.MATRIX) { + mLastBitmapHeight = bitmapWithMetadata.bitmap.getHeight(); + mLastBitmapWidth = bitmapWithMetadata.bitmap.getWidth(); + mLastRotationDegrees = bitmapWithMetadata.rotationDegrees; + updateDrawMatrix(); + } else { + setRotation(bitmapWithMetadata.rotationDegrees); + } setImageBitmap(bitmapWithMetadata.bitmap); - setRotation(bitmapWithMetadata.rotationDegrees); } protected void cancelLoadingAndClear() { @@ -104,7 +162,6 @@ public class MtpImageView extends ImageView { mFetchResult = null; } setImageResource(android.R.color.transparent); - setRotation(0); } @Override |
