diff options
Diffstat (limited to 'src/com')
26 files changed, 2003 insertions, 138 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..7c0c15a82 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; 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..7a75a6d3d 100644 --- a/src/com/android/camera/data/CameraDataAdapter.java +++ b/src/com/android/camera/data/CameraDataAdapter.java @@ -33,6 +33,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; @@ -79,7 +80,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { private List<LocalImageData> mImages; - private FilmStripView mFilmStripView; + private Listener mListener; private View mCameraPreviewView; private ColorDrawable mPlaceHolder; @@ -101,7 +102,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); } @@ -142,8 +143,8 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override - public void setDataListener(FilmStripView v) { - mFilmStripView = v; + public void setListener(Listener listener) { + mListener = listener; } private LocalImageData buildCameraImageData(int width, int height, int orientation) { @@ -152,6 +153,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { d.height = height; d.orientation = orientation; d.isCameraData = true; + d.supportedAction = ImageData.ACTION_NONE; return d; } @@ -179,6 +181,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { d.orientation = c.getInt(COL_ORIENTATION); d.width = c.getInt(COL_WIDTH); d.height = c.getInt(COL_HEIGHT); + d.supportedAction = ImageData.ACTION_PROMOTE | ImageData.ACTION_DEMOTE; if (d.width <= 0 || d.height <= 0) { Log.v(TAG, "warning! zero dimension for " + d.path + ":" + d.width + "x" + d.height); @@ -229,6 +232,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { // width and height should be adjusted according to orientation. public int width; public int height; + public int supportedAction; @Override public int getWidth() { @@ -241,6 +245,17 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { } @Override + public int getType() { + if (isCameraData) return ImageData.TYPE_CAMERA_PREVIEW; + return ImageData.TYPE_PHOTO; + } + + @Override + public boolean isActionSupported(int action) { + return ((action & supportedAction) != 0); + } + + @Override public String toString() { return "LocalImageData:" + ",data=" + path + ",mimeType=" + mimeType + "," + width + "x" + height + ",orientation=" + orientation; @@ -281,7 +296,7 @@ public class CameraDataAdapter implements FilmStripView.DataAdapter { mImages = l; if (first != null) addOrReplaceCameraData(first); // both might be null. - if (changed) mFilmStripView.onDataChanged(); + if (changed && mListener != null) mListener.onDataLoaded(); } } 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..f2ffa8719 100644 --- a/src/com/android/camera/ui/FilmStripView.java +++ b/src/com/android/camera/ui/FilmStripView.java @@ -16,46 +16,72 @@ 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); @@ -63,22 +89,27 @@ public class FilmStripView extends ViewGroup { 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 +121,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 +137,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 +173,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 +208,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 +221,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 +251,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 +268,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 +294,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 +308,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 +356,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 +378,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 +424,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/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 b65943279..874a7c911 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; diff --git a/src/com/android/gallery3d/filtershow/PanelController.java b/src/com/android/gallery3d/filtershow/PanelController.java index f29765a35..1d0118f85 100644 --- a/src/com/android/gallery3d/filtershow/PanelController.java +++ b/src/com/android/gallery3d/filtershow/PanelController.java @@ -359,7 +359,6 @@ public class PanelController implements OnClickListener { if (mCurrentEditor != null) { mCurrentEditor.reflectCurrentFilter(); } - } } @@ -590,6 +589,9 @@ public class PanelController implements OnClickListener { mUtilityPanel.showMenu(false); if (view instanceof FilterIconButton) { + if (mCurrentEditor != null) { + mCurrentEditor.detach(); + } mCurrentEditor = null; FilterIconButton component = (FilterIconButton) view; FilterRepresentation representation = component.getFilterRepresentation(); diff --git a/src/com/android/gallery3d/filtershow/imageshow/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java index e94d1ed9e..c2c768eaf 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) { @@ -73,10 +79,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/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..40254931f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropLoader.java @@ -0,0 +1,199 @@ +/* + * 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 reentrant static methods for loading a bitmap. + */ +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); + if (imageSide > maxSideLength) { + int shifts = 1 + Integer.numberOfLeadingZeros(maxSideLength) + - Integer.numberOfLeadingZeros(imageSide); + options.inSampleSize = 1 << shifts; + } + + // Make sure sample size is reasonable + if (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..00baba980 --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java @@ -0,0 +1,327 @@ +/* + * 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 setToInnerBounds(RectF r) { + mBoundedRect.setToInner(r); + } + + public void setToOuterBounds(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..561f7ae7f --- /dev/null +++ b/src/com/android/gallery3d/filtershow/crop/CropView.java @@ -0,0 +1,278 @@ +/* + * 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.Color; +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 = 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)) { + drawCropRect(canvas, imageBounds); + drawRuleOfThird(canvas, imageBounds); + 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; + } + + // Helper methods + + private 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; + } + } + + private 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); + } + + private 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); + } + + private 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()); + } + } + } + + private static Matrix getBitmapToDisplayMatrix(RectF imageBounds, RectF displayBounds) { + Matrix m = new Matrix(); + setBitmapToDisplayMatrix(m, imageBounds, displayBounds); + return m; + } + + private 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/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/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/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") |