summaryrefslogtreecommitdiffstats
path: root/src/com/android/gallery3d/filtershow/crop
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/gallery3d/filtershow/crop')
-rw-r--r--src/com/android/gallery3d/filtershow/crop/BoundedRect.java368
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropActivity.java697
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java168
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropExtras.java121
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropMath.java260
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropObject.java330
-rw-r--r--src/com/android/gallery3d/filtershow/crop/CropView.java378
7 files changed, 2322 insertions, 0 deletions
diff --git a/src/com/android/gallery3d/filtershow/crop/BoundedRect.java b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
new file mode 100644
index 000000000..13b8d6de1
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/BoundedRect.java
@@ -0,0 +1,368 @@
+/*
+ * Copyright (C) 2012 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.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+/**
+ * Maintains invariant that inner rectangle is constrained to be within the
+ * outer, rotated rectangle.
+ */
+public class BoundedRect {
+ private float rot;
+ private RectF outer;
+ private RectF inner;
+ private float[] innerRotated;
+
+ 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) {
+ rot = rotation;
+ outer = new RectF(outerRect);
+ inner = new RectF(innerRect);
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ 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.
+ */
+ public void setInner(RectF newInner) {
+ if (inner.equals(newInner))
+ return;
+ inner = newInner;
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ reconstrain();
+ }
+
+ /**
+ * Sets rotation, and re-constrains inner to fit within the rotated bounding rect.
+ */
+ public void setRotation(float rotation) {
+ if (rotation == rot)
+ return;
+ rot = rotation;
+ innerRotated = CropMath.getCornersFromRect(inner);
+ rotateInner();
+ if (!isConstrained())
+ 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
+ * rectangle.
+ */
+ public void moveInner(float dx, float dy) {
+ Matrix m0 = getInverseRotMatrix();
+
+ RectF translatedInner = new RectF(inner);
+ translatedInner.offset(dx, dy);
+
+ float[] translatedInnerCorners = CropMath.getCornersFromRect(translatedInner);
+ float[] outerCorners = CropMath.getCornersFromRect(outer);
+
+ m0.mapPoints(translatedInnerCorners);
+ float[] correction = {
+ 0, 0
+ };
+
+ // find correction vectors for corners that have moved out of bounds
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+ float[] badCorner = {
+ correctedInnerX, correctedInnerY
+ };
+ float[] nearestSide = CropMath.closestSide(badCorner, outerCorners);
+ float[] correctionVec =
+ GeometryMathUtils.shortestVectorFromPointToLine(badCorner, nearestSide);
+ correction[0] += correctionVec[0];
+ correction[1] += correctionVec[1];
+ }
+ }
+
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ if (!CropMath.inclusiveContains(outer, correctedInnerX, correctedInnerY)) {
+ float[] correctionVec = {
+ correctedInnerX, correctedInnerY
+ };
+ CropMath.getEdgePoints(outer, correctionVec);
+ correctionVec[0] -= correctedInnerX;
+ correctionVec[1] -= correctedInnerY;
+ correction[0] += correctionVec[0];
+ correction[1] += correctionVec[1];
+ }
+ }
+
+ // Set correction
+ for (int i = 0; i < translatedInnerCorners.length; i += 2) {
+ float correctedInnerX = translatedInnerCorners[i] + correction[0];
+ float correctedInnerY = translatedInnerCorners[i + 1] + correction[1];
+ // update translated corners with correction vectors
+ translatedInnerCorners[i] = correctedInnerX;
+ translatedInnerCorners[i + 1] = correctedInnerY;
+ }
+
+ innerRotated = translatedInnerCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ /**
+ * Attempts to resize the inner rectangle. If this would cause it to leave
+ * the bounding rect, clips the inner rectangle to fit.
+ */
+ public void resizeInner(RectF newInner) {
+ Matrix m = getRotMatrix();
+ Matrix m0 = getInverseRotMatrix();
+
+ float[] outerCorners = CropMath.getCornersFromRect(outer);
+ m.mapPoints(outerCorners);
+ float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+ float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+ RectF ret = new RectF(newInner);
+
+ for (int i = 0; i < newInnerCorners.length; i += 2) {
+ float[] c = {
+ newInnerCorners[i], newInnerCorners[i + 1]
+ };
+ float[] c0 = Arrays.copyOf(c, 2);
+ m0.mapPoints(c0);
+ if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+ float[] outerSide = CropMath.closestSide(c, outerCorners);
+ float[] pathOfCorner = {
+ newInnerCorners[i], newInnerCorners[i + 1],
+ oldInnerCorners[i], oldInnerCorners[i + 1]
+ };
+ float[] p = GeometryMathUtils.lineIntersect(pathOfCorner, outerSide);
+ if (p == null) {
+ // lines are parallel or not well defined, so don't resize
+ p = new float[2];
+ p[0] = oldInnerCorners[i];
+ p[1] = oldInnerCorners[i + 1];
+ }
+ // relies on corners being in same order as method
+ // getCornersFromRect
+ switch (i) {
+ case 0:
+ case 1:
+ ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+ ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+ break;
+ case 2:
+ case 3:
+ ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+ ret.top = (p[1] > ret.top) ? p[1] : ret.top;
+ break;
+ case 4:
+ case 5:
+ ret.right = (p[0] < ret.right) ? p[0] : ret.right;
+ ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+ break;
+ case 6:
+ case 7:
+ ret.left = (p[0] > ret.left) ? p[0] : ret.left;
+ ret.bottom = (p[1] < ret.bottom) ? p[1] : ret.bottom;
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ float[] retCorners = CropMath.getCornersFromRect(ret);
+ m0.mapPoints(retCorners);
+ innerRotated = retCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ /**
+ * Attempts to resize the inner rectangle. If this would cause it to leave
+ * the bounding rect, clips the inner rectangle to fit while maintaining
+ * aspect ratio.
+ */
+ public void fixedAspectResizeInner(RectF newInner) {
+ Matrix m = getRotMatrix();
+ Matrix m0 = getInverseRotMatrix();
+
+ float aspectW = inner.width();
+ float aspectH = inner.height();
+ float aspRatio = aspectW / aspectH;
+ float[] corners = CropMath.getCornersFromRect(outer);
+
+ m.mapPoints(corners);
+ float[] oldInnerCorners = CropMath.getCornersFromRect(inner);
+ float[] newInnerCorners = CropMath.getCornersFromRect(newInner);
+
+ // find fixed corner
+ int fixed = -1;
+ if (inner.top == newInner.top) {
+ if (inner.left == newInner.left)
+ fixed = 0; // top left
+ else if (inner.right == newInner.right)
+ fixed = 2; // top right
+ } else if (inner.bottom == newInner.bottom) {
+ if (inner.right == newInner.right)
+ fixed = 4; // bottom right
+ else if (inner.left == newInner.left)
+ fixed = 6; // bottom left
+ }
+ // no fixed corner, return without update
+ if (fixed == -1)
+ return;
+ float widthSoFar = newInner.width();
+ int moved = -1;
+ for (int i = 0; i < newInnerCorners.length; i += 2) {
+ float[] c = {
+ newInnerCorners[i], newInnerCorners[i + 1]
+ };
+ float[] c0 = Arrays.copyOf(c, 2);
+ m0.mapPoints(c0);
+ if (!CropMath.inclusiveContains(outer, c0[0], c0[1])) {
+ moved = i;
+ if (moved == fixed)
+ continue;
+ float[] l2 = CropMath.closestSide(c, corners);
+ float[] l1 = {
+ newInnerCorners[i], newInnerCorners[i + 1],
+ oldInnerCorners[i], oldInnerCorners[i + 1]
+ };
+ float[] p = GeometryMathUtils.lineIntersect(l1, l2);
+ if (p == null) {
+ // lines are parallel or not well defined, so set to old
+ // corner
+ p = new float[2];
+ p[0] = oldInnerCorners[i];
+ p[1] = oldInnerCorners[i + 1];
+ }
+ // relies on corners being in same order as method
+ // getCornersFromRect
+ float fixed_x = oldInnerCorners[fixed];
+ float fixed_y = oldInnerCorners[fixed + 1];
+ float newWidth = Math.abs(fixed_x - p[0]);
+ float newHeight = Math.abs(fixed_y - p[1]);
+ newWidth = Math.max(newWidth, aspRatio * newHeight);
+ if (newWidth < widthSoFar)
+ widthSoFar = newWidth;
+ }
+ }
+
+ float heightSoFar = widthSoFar / aspRatio;
+ RectF ret = new RectF(inner);
+ if (fixed == 0) {
+ ret.right = ret.left + widthSoFar;
+ ret.bottom = ret.top + heightSoFar;
+ } else if (fixed == 2) {
+ ret.left = ret.right - widthSoFar;
+ ret.bottom = ret.top + heightSoFar;
+ } else if (fixed == 4) {
+ ret.left = ret.right - widthSoFar;
+ ret.top = ret.bottom - heightSoFar;
+ } else if (fixed == 6) {
+ ret.right = ret.left + widthSoFar;
+ ret.top = ret.bottom - heightSoFar;
+ }
+ float[] retCorners = CropMath.getCornersFromRect(ret);
+ m0.mapPoints(retCorners);
+ innerRotated = retCorners;
+ // reconstrain to update inner
+ reconstrain();
+ }
+
+ // internal methods
+
+ private boolean isConstrained() {
+ for (int i = 0; i < 8; i += 2) {
+ if (!CropMath.inclusiveContains(outer, innerRotated[i], innerRotated[i + 1]))
+ return false;
+ }
+ return true;
+ }
+
+ private void reconstrain() {
+ // innerRotated has been changed to have incorrect values
+ CropMath.getEdgePoints(outer, innerRotated);
+ Matrix m = getRotMatrix();
+ float[] unrotated = Arrays.copyOf(innerRotated, 8);
+ m.mapPoints(unrotated);
+ inner = CropMath.trapToRect(unrotated);
+ }
+
+ private void rotateInner() {
+ Matrix m = getInverseRotMatrix();
+ m.mapPoints(innerRotated);
+ }
+
+ private Matrix getRotMatrix() {
+ Matrix m = new Matrix();
+ m.setRotate(rot, outer.centerX(), outer.centerY());
+ return m;
+ }
+
+ private Matrix getInverseRotMatrix() {
+ Matrix m = new Matrix();
+ m.setRotate(-rot, outer.centerX(), outer.centerY());
+ return m;
+ }
+}
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..0a0c36703
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropActivity.java
@@ -0,0 +1,697 @@
+/*
+ * 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.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapRegionDecoder;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+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.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.gallery3d.R;
+import com.android.gallery3d.common.Utils;
+import com.android.gallery3d.filtershow.cache.ImageLoader;
+import com.android.gallery3d.filtershow.tools.SaveImage;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Activity for cropping an image.
+ */
+public class CropActivity extends Activity {
+ private static final String LOGTAG = "CropActivity";
+ public static final String CROP_ACTION = "com.android.camera.action.CROP";
+ private CropExtras mCropExtras = null;
+ private LoadBitmapTask mLoadBitmapTask = null;
+
+ private int mOutputX = 0;
+ private int mOutputY = 0;
+ private Bitmap mOriginalBitmap = null;
+ private RectF mOriginalBounds = null;
+ private int mOriginalRotation = 0;
+ private Uri mSourceUri = null;
+ private CropView mCropView = null;
+ private View mSaveButton = null;
+ private boolean finalIOGuard = false;
+
+ private static final int SELECT_PICTURE = 1; // request code for picker
+
+ private static final int DEFAULT_COMPRESS_QUALITY = 90;
+ /**
+ * The maximum bitmap size we allow to be returned through the intent.
+ * Intents have a maximum of 1MB in total size. However, the Bitmap seems to
+ * have some overhead to hit so that we go way below the limit here to make
+ * sure the intent stays below 1MB.We should consider just returning a byte
+ * array instead of a Bitmap instance to avoid overhead.
+ */
+ public static final int MAX_BMAP_IN_INTENT = 750000;
+
+ // Flags
+ private static final int DO_SET_WALLPAPER = 1;
+ private static final int DO_RETURN_DATA = 1 << 1;
+ private static final int DO_EXTRA_OUTPUT = 1 << 2;
+
+ private static final int FLAG_CHECK = DO_SET_WALLPAPER | DO_RETURN_DATA | DO_EXTRA_OUTPUT;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ setResult(RESULT_CANCELED, new Intent());
+ 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);
+
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+ actionBar.setCustomView(R.layout.filtershow_actionbar);
+
+ View mSaveButton = actionBar.getCustomView();
+ mSaveButton.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ startFinishOutput();
+ }
+ });
+
+ if (intent.getData() != null) {
+ mSourceUri = intent.getData();
+ startLoadBitmap(mSourceUri);
+ } else {
+ pickImage();
+ }
+ }
+
+ private void enableSave(boolean enable) {
+ if (mSaveButton != null) {
+ mSaveButton.setEnabled(enable);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ if (mLoadBitmapTask != null) {
+ mLoadBitmapTask.cancel(false);
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onConfigurationChanged (Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mCropView.configChanged();
+ }
+
+ /**
+ * Opens a selector in Gallery to chose an image for use when none was given
+ * in the CROP intent.
+ */
+ private 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) {
+ mSourceUri = data.getData();
+ startLoadBitmap(mSourceUri);
+ }
+ }
+
+ /**
+ * Gets screen size metric.
+ */
+ private int getScreenImageSize() {
+ DisplayMetrics outMetrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
+ return (int) Math.max(outMetrics.heightPixels, outMetrics.widthPixels);
+ }
+
+ /**
+ * Method that loads a bitmap in an async task.
+ */
+ private void startLoadBitmap(Uri uri) {
+ if (uri != null) {
+ enableSave(false);
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.VISIBLE);
+ mLoadBitmapTask = new LoadBitmapTask();
+ mLoadBitmapTask.execute(uri);
+ } else {
+ cannotLoadImage();
+ done();
+ }
+ }
+
+ /**
+ * Method called on UI thread with loaded bitmap.
+ */
+ private void doneLoadBitmap(Bitmap bitmap, RectF bounds, int orientation) {
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ mOriginalBitmap = bitmap;
+ mOriginalBounds = bounds;
+ mOriginalRotation = orientation;
+ if (bitmap != null && bitmap.getWidth() != 0 && bitmap.getHeight() != 0) {
+ RectF imgBounds = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ mCropView.initialize(bitmap, imgBounds, imgBounds, orientation);
+ if (mCropExtras != null) {
+ int aspectX = mCropExtras.getAspectX();
+ int aspectY = mCropExtras.getAspectY();
+ mOutputX = mCropExtras.getOutputX();
+ mOutputY = mCropExtras.getOutputY();
+ if (mOutputX > 0 && mOutputY > 0) {
+ mCropView.applyAspect(mOutputX, mOutputY);
+
+ }
+ float spotX = mCropExtras.getSpotlightX();
+ float spotY = mCropExtras.getSpotlightY();
+ if (spotX > 0 && spotY > 0) {
+ mCropView.setWallpaperSpotlight(spotX, spotY);
+ }
+ if (aspectX > 0 && aspectY > 0) {
+ mCropView.applyAspect(aspectX, aspectY);
+ }
+ }
+ enableSave(true);
+ } else {
+ Log.w(LOGTAG, "could not load image for cropping");
+ cannotLoadImage();
+ setResult(RESULT_CANCELED, new Intent());
+ 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;
+ int mOrientation;
+
+ public LoadBitmapTask() {
+ mBitmapSize = getScreenImageSize();
+ mContext = getApplicationContext();
+ mOriginalBounds = new Rect();
+ mOrientation = 0;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Uri... params) {
+ Uri uri = params[0];
+ Bitmap bmap = ImageLoader.loadConstrainedBitmap(uri, mContext, mBitmapSize,
+ mOriginalBounds, false);
+ mOrientation = ImageLoader.getMetadataRotation(mContext, uri);
+ return bmap;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap result) {
+ doneLoadBitmap(result, new RectF(mOriginalBounds), mOrientation);
+ }
+ }
+
+ private void startFinishOutput() {
+ if (finalIOGuard) {
+ return;
+ } else {
+ finalIOGuard = true;
+ }
+ enableSave(false);
+ Uri destinationUri = null;
+ int flags = 0;
+ if (mOriginalBitmap != null && mCropExtras != null) {
+ if (mCropExtras.getExtraOutput() != null) {
+ destinationUri = mCropExtras.getExtraOutput();
+ if (destinationUri != null) {
+ flags |= DO_EXTRA_OUTPUT;
+ }
+ }
+ if (mCropExtras.getSetAsWallpaper()) {
+ flags |= DO_SET_WALLPAPER;
+ }
+ if (mCropExtras.getReturnData()) {
+ flags |= DO_RETURN_DATA;
+ }
+ }
+ if (flags == 0) {
+ destinationUri = SaveImage.makeAndInsertUri(this, mSourceUri);
+ if (destinationUri != null) {
+ flags |= DO_EXTRA_OUTPUT;
+ }
+ }
+ if ((flags & FLAG_CHECK) != 0 && mOriginalBitmap != null) {
+ RectF photo = new RectF(0, 0, mOriginalBitmap.getWidth(), mOriginalBitmap.getHeight());
+ RectF crop = getBitmapCrop(photo);
+ startBitmapIO(flags, mOriginalBitmap, mSourceUri, destinationUri, crop,
+ photo, mOriginalBounds,
+ (mCropExtras == null) ? null : mCropExtras.getOutputFormat(), mOriginalRotation);
+ return;
+ }
+ setResult(RESULT_CANCELED, new Intent());
+ done();
+ return;
+ }
+
+ private void startBitmapIO(int flags, Bitmap currentBitmap, Uri sourceUri, Uri destUri,
+ RectF cropBounds, RectF photoBounds, RectF currentBitmapBounds, String format,
+ int rotation) {
+ if (cropBounds == null || photoBounds == null || currentBitmap == null
+ || currentBitmap.getWidth() == 0 || currentBitmap.getHeight() == 0
+ || cropBounds.width() == 0 || cropBounds.height() == 0 || photoBounds.width() == 0
+ || photoBounds.height() == 0) {
+ return; // fail fast
+ }
+ if ((flags & FLAG_CHECK) == 0) {
+ return; // no output options
+ }
+ if ((flags & DO_SET_WALLPAPER) != 0) {
+ Toast.makeText(this, R.string.setting_wallpaper, Toast.LENGTH_LONG).show();
+ }
+
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.VISIBLE);
+ BitmapIOTask ioTask = new BitmapIOTask(sourceUri, destUri, format, flags, cropBounds,
+ photoBounds, currentBitmapBounds, rotation, mOutputX, mOutputY);
+ ioTask.execute(currentBitmap);
+ }
+
+ private void doneBitmapIO(boolean success, Intent intent) {
+ final View loading = findViewById(R.id.loading);
+ loading.setVisibility(View.GONE);
+ if (success) {
+ setResult(RESULT_OK, intent);
+ } else {
+ setResult(RESULT_CANCELED, intent);
+ }
+ done();
+ }
+
+ private class BitmapIOTask extends AsyncTask<Bitmap, Void, Boolean> {
+
+ private final WallpaperManager mWPManager;
+ InputStream mInStream = null;
+ OutputStream mOutStream = null;
+ String mOutputFormat = null;
+ Uri mOutUri = null;
+ Uri mInUri = null;
+ int mFlags = 0;
+ RectF mCrop = null;
+ RectF mPhoto = null;
+ RectF mOrig = null;
+ Intent mResultIntent = null;
+ int mRotation = 0;
+
+ // Helper to setup input stream
+ private void regenerateInputStream() {
+ if (mInUri == null) {
+ Log.w(LOGTAG, "cannot read original file, no input URI given");
+ } else {
+ Utils.closeSilently(mInStream);
+ try {
+ mInStream = getContentResolver().openInputStream(mInUri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "cannot read file: " + mInUri.toString(), e);
+ }
+ }
+ }
+
+ public BitmapIOTask(Uri sourceUri, Uri destUri, String outputFormat, int flags,
+ RectF cropBounds, RectF photoBounds, RectF originalBitmapBounds, int rotation,
+ int outputX, int outputY) {
+ mOutputFormat = outputFormat;
+ mOutStream = null;
+ mOutUri = destUri;
+ mInUri = sourceUri;
+ mFlags = flags;
+ mCrop = cropBounds;
+ mPhoto = photoBounds;
+ mOrig = originalBitmapBounds;
+ mWPManager = WallpaperManager.getInstance(getApplicationContext());
+ mResultIntent = new Intent();
+ mRotation = (rotation < 0) ? -rotation : rotation;
+ mRotation %= 360;
+ mRotation = 90 * (int) (mRotation / 90); // now mRotation is a multiple of 90
+ mOutputX = outputX;
+ mOutputY = outputY;
+
+ if ((flags & DO_EXTRA_OUTPUT) != 0) {
+ if (mOutUri == null) {
+ Log.w(LOGTAG, "cannot write file, no output URI given");
+ } else {
+ try {
+ mOutStream = getContentResolver().openOutputStream(mOutUri);
+ } catch (FileNotFoundException e) {
+ Log.w(LOGTAG, "cannot write file: " + mOutUri.toString(), e);
+ }
+ }
+ }
+
+ if ((flags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0) {
+ regenerateInputStream();
+ }
+ }
+
+ @Override
+ protected Boolean doInBackground(Bitmap... params) {
+ boolean failure = false;
+ Bitmap img = params[0];
+
+ // Set extra for crop bounds
+ if (mCrop != null && mPhoto != null && mOrig != null) {
+ RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ m.mapRect(trueCrop);
+ if (trueCrop != null) {
+ Rect rounded = new Rect();
+ trueCrop.roundOut(rounded);
+ mResultIntent.putExtra(CropExtras.KEY_CROPPED_RECT, rounded);
+ }
+ }
+
+ // Find the small cropped bitmap that is returned in the intent
+ if ((mFlags & DO_RETURN_DATA) != 0) {
+ assert (img != null);
+ Bitmap ret = getCroppedImage(img, mCrop, mPhoto);
+ if (ret != null) {
+ ret = getDownsampledBitmap(ret, MAX_BMAP_IN_INTENT);
+ }
+ if (ret == null) {
+ Log.w(LOGTAG, "could not downsample bitmap to return in data");
+ failure = true;
+ } else {
+ if (mRotation > 0) {
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap(ret, 0, 0, ret.getWidth(),
+ ret.getHeight(), m, true);
+ if (tmp != null) {
+ ret = tmp;
+ }
+ }
+ mResultIntent.putExtra(CropExtras.KEY_DATA, ret);
+ }
+ }
+
+ // Do the large cropped bitmap and/or set the wallpaper
+ if ((mFlags & (DO_EXTRA_OUTPUT | DO_SET_WALLPAPER)) != 0 && mInStream != null) {
+ // Find crop bounds (scaled to original image size)
+ RectF trueCrop = CropMath.getScaledCropBounds(mCrop, mPhoto, mOrig);
+ if (trueCrop == null) {
+ Log.w(LOGTAG, "cannot find crop for full size image");
+ failure = true;
+ return false;
+ }
+ Rect roundedTrueCrop = new Rect();
+ trueCrop.roundOut(roundedTrueCrop);
+
+ if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
+ Log.w(LOGTAG, "crop has bad values for full size image");
+ failure = true;
+ return false;
+ }
+
+ // Attempt to open a region decoder
+ BitmapRegionDecoder decoder = null;
+ try {
+ decoder = BitmapRegionDecoder.newInstance(mInStream, true);
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot open region decoder for file: " + mInUri.toString(), e);
+ }
+
+ Bitmap crop = null;
+ if (decoder != null) {
+ // Do region decoding to get crop bitmap
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inMutable = true;
+ crop = decoder.decodeRegion(roundedTrueCrop, options);
+ decoder.recycle();
+ }
+
+ if (crop == null) {
+ // BitmapRegionDecoder has failed, try to crop in-memory
+ regenerateInputStream();
+ Bitmap fullSize = null;
+ if (mInStream != null) {
+ fullSize = BitmapFactory.decodeStream(mInStream);
+ }
+ if (fullSize != null) {
+ crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
+ roundedTrueCrop.top, roundedTrueCrop.width(),
+ roundedTrueCrop.height());
+ }
+ }
+
+ if (crop == null) {
+ Log.w(LOGTAG, "cannot decode file: " + mInUri.toString());
+ failure = true;
+ return false;
+ }
+ if (mOutputX > 0 && mOutputY > 0) {
+ Matrix m = new Matrix();
+ RectF cropRect = new RectF(0, 0, crop.getWidth(), crop.getHeight());
+ if (mRotation > 0) {
+ m.setRotate(mRotation);
+ m.mapRect(cropRect);
+ }
+ RectF returnRect = new RectF(0, 0, mOutputX, mOutputY);
+ m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
+ m.preRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
+ (int) returnRect.height(), Bitmap.Config.ARGB_8888);
+ if (tmp != null) {
+ Canvas c = new Canvas(tmp);
+ c.drawBitmap(crop, m, new Paint());
+ crop = tmp;
+ }
+ } else if (mRotation > 0) {
+ Matrix m = new Matrix();
+ m.setRotate(mRotation);
+ Bitmap tmp = Bitmap.createBitmap(crop, 0, 0, crop.getWidth(),
+ crop.getHeight(), m, true);
+ if (tmp != null) {
+ crop = tmp;
+ }
+ }
+ // Get output compression format
+ CompressFormat cf =
+ convertExtensionToCompressFormat(getFileExtension(mOutputFormat));
+
+ // If we only need to output to a URI, compress straight to file
+ if (mFlags == DO_EXTRA_OUTPUT) {
+ if (mOutStream == null
+ || !crop.compress(cf, DEFAULT_COMPRESS_QUALITY, mOutStream)) {
+ Log.w(LOGTAG, "failed to compress bitmap to file: " + mOutUri.toString());
+ failure = true;
+ } else {
+ mResultIntent.setData(mOutUri);
+ }
+ } else {
+ // Compress to byte array
+ ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
+ if (crop.compress(cf, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
+
+ // If we need to output to a Uri, write compressed
+ // bitmap out
+ if ((mFlags & DO_EXTRA_OUTPUT) != 0) {
+ if (mOutStream == null) {
+ Log.w(LOGTAG,
+ "failed to compress bitmap to file: " + mOutUri.toString());
+ failure = true;
+ } else {
+ try {
+ mOutStream.write(tmpOut.toByteArray());
+ mResultIntent.setData(mOutUri);
+ } catch (IOException e) {
+ Log.w(LOGTAG,
+ "failed to compress bitmap to file: "
+ + mOutUri.toString(), e);
+ failure = true;
+ }
+ }
+ }
+
+ // If we need to set to the wallpaper, set it
+ if ((mFlags & DO_SET_WALLPAPER) != 0 && mWPManager != null) {
+ if (mWPManager == null) {
+ Log.w(LOGTAG, "no wallpaper manager");
+ failure = true;
+ } else {
+ try {
+ mWPManager.setStream(new ByteArrayInputStream(tmpOut
+ .toByteArray()));
+ } catch (IOException e) {
+ Log.w(LOGTAG, "cannot write stream to wallpaper", e);
+ failure = true;
+ }
+ }
+ }
+ } else {
+ Log.w(LOGTAG, "cannot compress bitmap");
+ failure = true;
+ }
+ }
+ }
+ return !failure; // True if any of the operations failed
+ }
+
+ @Override
+ protected void onPostExecute(Boolean result) {
+ Utils.closeSilently(mOutStream);
+ Utils.closeSilently(mInStream);
+ doneBitmapIO(result.booleanValue(), mResultIntent);
+ }
+
+ }
+
+ private void done() {
+ finish();
+ }
+
+ protected static Bitmap getCroppedImage(Bitmap image, RectF cropBounds, RectF photoBounds) {
+ RectF imageBounds = new RectF(0, 0, image.getWidth(), image.getHeight());
+ RectF crop = CropMath.getScaledCropBounds(cropBounds, photoBounds, imageBounds);
+ if (crop == null) {
+ return null;
+ }
+ Rect intCrop = new Rect();
+ crop.roundOut(intCrop);
+ return Bitmap.createBitmap(image, intCrop.left, intCrop.top, intCrop.width(),
+ intCrop.height());
+ }
+
+ protected static Bitmap getDownsampledBitmap(Bitmap image, int max_size) {
+ if (image == null || image.getWidth() == 0 || image.getHeight() == 0 || max_size < 16) {
+ throw new IllegalArgumentException("Bad argument to getDownsampledBitmap()");
+ }
+ int shifts = 0;
+ int size = CropMath.getBitmapSize(image);
+ while (size > max_size) {
+ shifts++;
+ size /= 4;
+ }
+ Bitmap ret = Bitmap.createScaledBitmap(image, image.getWidth() >> shifts,
+ image.getHeight() >> shifts, true);
+ if (ret == null) {
+ return null;
+ }
+ // Handle edge case for rounding.
+ if (CropMath.getBitmapSize(ret) > max_size) {
+ return Bitmap.createScaledBitmap(ret, ret.getWidth() >> 1, ret.getHeight() >> 1, true);
+ }
+ return ret;
+ }
+
+ /**
+ * Gets the crop extras from the intent, or null if none exist.
+ */
+ protected 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;
+ }
+
+ protected static CompressFormat convertExtensionToCompressFormat(String extension) {
+ return extension.equals("png") ? CompressFormat.PNG : CompressFormat.JPEG;
+ }
+
+ protected 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";
+ }
+
+ private RectF getBitmapCrop(RectF imageBounds) {
+ RectF crop = mCropView.getCrop();
+ RectF photo = mCropView.getPhoto();
+ if (crop == null || photo == null) {
+ Log.w(LOGTAG, "could not get crop");
+ return null;
+ }
+ RectF scaledCrop = CropMath.getScaledCropBounds(crop, photo, imageBounds);
+ return scaledCrop;
+ }
+}
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..b0d324cbb
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropDrawingUtils.java
@@ -0,0 +1,168 @@
+/*
+ * 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.Path;
+import android.graphics.RectF;
+import android.graphics.Region;
+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 void drawWallpaperSelectionFrame(Canvas canvas, RectF cropBounds, float spotX,
+ float spotY, Paint p, Paint shadowPaint) {
+ float sx = cropBounds.width() * spotX;
+ float sy = cropBounds.height() * spotY;
+ float cx = cropBounds.centerX();
+ float cy = cropBounds.centerY();
+ RectF r1 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+ float temp = sx;
+ sx = sy;
+ sy = temp;
+ RectF r2 = new RectF(cx - sx / 2, cy - sy / 2, cx + sx / 2, cy + sy / 2);
+ canvas.save();
+ canvas.clipRect(cropBounds);
+ canvas.clipRect(r1, Region.Op.DIFFERENCE);
+ canvas.clipRect(r2, Region.Op.DIFFERENCE);
+ canvas.drawPaint(shadowPaint);
+ canvas.restore();
+ Path path = new Path();
+ path.moveTo(r1.left, r1.top);
+ path.lineTo(r1.right, r1.top);
+ path.moveTo(r1.left, r1.top);
+ path.lineTo(r1.left, r1.bottom);
+ path.moveTo(r1.left, r1.bottom);
+ path.lineTo(r1.right, r1.bottom);
+ path.moveTo(r1.right, r1.top);
+ path.lineTo(r1.right, r1.bottom);
+ path.moveTo(r2.left, r2.top);
+ path.lineTo(r2.right, r2.top);
+ path.moveTo(r2.right, r2.top);
+ path.lineTo(r2.right, r2.bottom);
+ path.moveTo(r2.left, r2.bottom);
+ path.lineTo(r2.right, r2.bottom);
+ path.moveTo(r2.left, r2.top);
+ path.lineTo(r2.left, r2.bottom);
+ canvas.drawPath(path, p);
+ }
+
+ public static void drawShadows(Canvas canvas, Paint p, RectF innerBounds, RectF outerBounds) {
+ canvas.drawRect(outerBounds.left, outerBounds.top, innerBounds.right, innerBounds.top, p);
+ canvas.drawRect(innerBounds.right, outerBounds.top, outerBounds.right, innerBounds.bottom,
+ p);
+ canvas.drawRect(innerBounds.left, innerBounds.bottom, outerBounds.right,
+ outerBounds.bottom, p);
+ canvas.drawRect(outerBounds.left, innerBounds.top, innerBounds.left, outerBounds.bottom, p);
+ }
+
+ 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);
+ }
+
+ public static boolean setImageToScreenMatrix(Matrix dst, RectF image,
+ RectF screen, int rotation) {
+ RectF rotatedImage = new RectF();
+ dst.setRotate(rotation, image.centerX(), image.centerY());
+ if (!dst.mapRect(rotatedImage, image)) {
+ return false; // fails for rotations that are not multiples of 90
+ // degrees
+ }
+ boolean rToR = dst.setRectToRect(rotatedImage, screen, Matrix.ScaleToFit.CENTER);
+ boolean rot = dst.preRotate(rotation, image.centerX(), image.centerY());
+ return rToR && rot;
+ }
+
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropExtras.java b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
new file mode 100644
index 000000000..60fe9af53
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropExtras.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2012 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.net.Uri;
+
+public class CropExtras {
+
+ public static final String KEY_CROPPED_RECT = "cropped-rect";
+ public static final String KEY_OUTPUT_X = "outputX";
+ public static final String KEY_OUTPUT_Y = "outputY";
+ public static final String KEY_SCALE = "scale";
+ public static final String KEY_SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
+ public static final String KEY_ASPECT_X = "aspectX";
+ public static final String KEY_ASPECT_Y = "aspectY";
+ public static final String KEY_SET_AS_WALLPAPER = "set-as-wallpaper";
+ public static final String KEY_RETURN_DATA = "return-data";
+ public static final String KEY_DATA = "data";
+ public static final String KEY_SPOTLIGHT_X = "spotlightX";
+ public static final String KEY_SPOTLIGHT_Y = "spotlightY";
+ public static final String KEY_SHOW_WHEN_LOCKED = "showWhenLocked";
+ public static final String KEY_OUTPUT_FORMAT = "outputFormat";
+
+ private int mOutputX = 0;
+ private int mOutputY = 0;
+ private boolean mScaleUp = true;
+ private int mAspectX = 0;
+ private int mAspectY = 0;
+ private boolean mSetAsWallpaper = false;
+ private boolean mReturnData = false;
+ private Uri mExtraOutput = null;
+ private String mOutputFormat = null;
+ private boolean mShowWhenLocked = false;
+ private float mSpotlightX = 0;
+ private float mSpotlightY = 0;
+
+ public CropExtras(int outputX, int outputY, boolean scaleUp, int aspectX, int aspectY,
+ boolean setAsWallpaper, boolean returnData, Uri extraOutput, String outputFormat,
+ boolean showWhenLocked, float spotlightX, float spotlightY) {
+ mOutputX = outputX;
+ mOutputY = outputY;
+ mScaleUp = scaleUp;
+ mAspectX = aspectX;
+ mAspectY = aspectY;
+ mSetAsWallpaper = setAsWallpaper;
+ mReturnData = returnData;
+ mExtraOutput = extraOutput;
+ mOutputFormat = outputFormat;
+ mShowWhenLocked = showWhenLocked;
+ mSpotlightX = spotlightX;
+ mSpotlightY = spotlightY;
+ }
+
+ public CropExtras(CropExtras c) {
+ this(c.mOutputX, c.mOutputY, c.mScaleUp, c.mAspectX, c.mAspectY, c.mSetAsWallpaper,
+ c.mReturnData, c.mExtraOutput, c.mOutputFormat, c.mShowWhenLocked,
+ c.mSpotlightX, c.mSpotlightY);
+ }
+
+ public int getOutputX() {
+ return mOutputX;
+ }
+
+ public int getOutputY() {
+ return mOutputY;
+ }
+
+ public boolean getScaleUp() {
+ return mScaleUp;
+ }
+
+ public int getAspectX() {
+ return mAspectX;
+ }
+
+ public int getAspectY() {
+ return mAspectY;
+ }
+
+ public boolean getSetAsWallpaper() {
+ return mSetAsWallpaper;
+ }
+
+ public boolean getReturnData() {
+ return mReturnData;
+ }
+
+ public Uri getExtraOutput() {
+ return mExtraOutput;
+ }
+
+ public String getOutputFormat() {
+ return mOutputFormat;
+ }
+
+ public boolean getShowWhenLocked() {
+ return mShowWhenLocked;
+ }
+
+ public float getSpotlightX() {
+ return mSpotlightX;
+ }
+
+ public float getSpotlightY() {
+ return mSpotlightY;
+ }
+}
diff --git a/src/com/android/gallery3d/filtershow/crop/CropMath.java b/src/com/android/gallery3d/filtershow/crop/CropMath.java
new file mode 100644
index 000000000..02c65310e
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropMath.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2012 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.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+import java.util.Arrays;
+
+public class CropMath {
+
+ /**
+ * Gets a float array of the 2D coordinates representing a rectangles
+ * corners.
+ * The order of the corners in the float array is:
+ * 0------->1
+ * ^ |
+ * | v
+ * 3<-------2
+ *
+ * @param r the rectangle to get the corners of
+ * @return the float array of corners (8 floats)
+ */
+
+ public static float[] getCornersFromRect(RectF r) {
+ float[] corners = {
+ r.left, r.top,
+ r.right, r.top,
+ r.right, r.bottom,
+ r.left, r.bottom
+ };
+ return corners;
+ }
+
+ /**
+ * Returns true iff point (x, y) is within or on the rectangle's bounds.
+ * RectF's "contains" function treats points on the bottom and right bound
+ * as not being contained.
+ *
+ * @param r the rectangle
+ * @param x the x value of the point
+ * @param y the y value of the point
+ * @return
+ */
+ public static boolean inclusiveContains(RectF r, float x, float y) {
+ return !(x > r.right || x < r.left || y > r.bottom || y < r.top);
+ }
+
+ /**
+ * Takes an array of 2D coordinates representing corners and returns the
+ * smallest rectangle containing those coordinates.
+ *
+ * @param array array of 2D coordinates
+ * @return smallest rectangle containing coordinates
+ */
+ public static RectF trapToRect(float[] array) {
+ RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY,
+ Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY);
+ for (int i = 1; i < array.length; i += 2) {
+ float x = array[i - 1];
+ float y = array[i];
+ r.left = (x < r.left) ? x : r.left;
+ r.top = (y < r.top) ? y : r.top;
+ r.right = (x > r.right) ? x : r.right;
+ r.bottom = (y > r.bottom) ? y : r.bottom;
+ }
+ r.sort();
+ return r;
+ }
+
+ /**
+ * If edge point [x, y] in array [x0, y0, x1, y1, ...] is outside of the
+ * image bound rectangle, clamps it to the edge of the rectangle.
+ *
+ * @param imageBound the rectangle to clamp edge points to.
+ * @param array an array of points to clamp to the rectangle, gets set to
+ * the clamped values.
+ */
+ public static void getEdgePoints(RectF imageBound, float[] array) {
+ if (array.length < 2)
+ return;
+ for (int x = 0; x < array.length; x += 2) {
+ array[x] = GeometryMathUtils.clamp(array[x], imageBound.left, imageBound.right);
+ array[x + 1] = GeometryMathUtils.clamp(array[x + 1], imageBound.top, imageBound.bottom);
+ }
+ }
+
+ /**
+ * Takes a point and the corners of a rectangle and returns the two corners
+ * representing the side of the rectangle closest to the point.
+ *
+ * @param point the point which is being checked
+ * @param corners the corners of the rectangle
+ * @return two corners representing the side of the rectangle
+ */
+ public static float[] closestSide(float[] point, float[] corners) {
+ int len = corners.length;
+ float oldMag = Float.POSITIVE_INFINITY;
+ float[] bestLine = null;
+ for (int i = 0; i < len; i += 2) {
+ float[] line = {
+ corners[i], corners[(i + 1) % len],
+ corners[(i + 2) % len], corners[(i + 3) % len]
+ };
+ float mag = GeometryMathUtils.vectorLength(
+ GeometryMathUtils.shortestVectorFromPointToLine(point, line));
+ if (mag < oldMag) {
+ oldMag = mag;
+ bestLine = line;
+ }
+ }
+ return bestLine;
+ }
+
+ /**
+ * Checks if a given point is within a rotated rectangle.
+ *
+ * @param point 2D point to check
+ * @param bound rectangle to rotate
+ * @param rot angle of rotation about rectangle center
+ * @return true if point is within rotated rectangle
+ */
+ public static boolean pointInRotatedRect(float[] point, RectF bound, float rot) {
+ Matrix m = new Matrix();
+ float[] p = Arrays.copyOf(point, 2);
+ m.setRotate(rot, bound.centerX(), bound.centerY());
+ Matrix m0 = new Matrix();
+ if (!m.invert(m0))
+ return false;
+ m0.mapPoints(p);
+ return inclusiveContains(bound, p[0], p[1]);
+ }
+
+ /**
+ * Checks if a given point is within a rotated rectangle.
+ *
+ * @param point 2D point to check
+ * @param rotatedRect corners of a rotated rectangle
+ * @param center center of the rotated rectangle
+ * @return true if point is within rotated rectangle
+ */
+ public static boolean pointInRotatedRect(float[] point, float[] rotatedRect, float[] center) {
+ RectF unrotated = new RectF();
+ float angle = getUnrotated(rotatedRect, center, unrotated);
+ return pointInRotatedRect(point, unrotated, angle);
+ }
+
+ /**
+ * Resizes rectangle to have a certain aspect ratio (center remains
+ * stationary).
+ *
+ * @param r rectangle to resize
+ * @param w new width aspect
+ * @param h new height aspect
+ */
+ public static void fixAspectRatio(RectF r, float w, float h) {
+ float scale = Math.min(r.width() / w, r.height() / h);
+ float centX = r.centerX();
+ float centY = r.centerY();
+ float hw = scale * w / 2;
+ float hh = scale * h / 2;
+ 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 = origW / a;
+ r.top = r.centerY() - finalH / 2;
+ r.bottom = r.top + finalH;
+ } else {
+ finalW = origH * a;
+ r.left = r.centerX() - finalW / 2;
+ r.right = r.left + finalW;
+ }
+ }
+
+ /**
+ * Stretches/Scales/Translates photoBounds to match displayBounds, and
+ * and returns an equivalent stretched/scaled/translated cropBounds or null
+ * if the mapping is invalid.
+ * @param cropBounds cropBounds to transform
+ * @param photoBounds original bounds containing crop bounds
+ * @param displayBounds final bounds for crop
+ * @return the stretched/scaled/translated crop bounds that fit within displayBounds
+ */
+ public static RectF getScaledCropBounds(RectF cropBounds, RectF photoBounds,
+ RectF displayBounds) {
+ Matrix m = new Matrix();
+ m.setRectToRect(photoBounds, displayBounds, Matrix.ScaleToFit.FILL);
+ RectF trueCrop = new RectF(cropBounds);
+ if (!m.mapRect(trueCrop)) {
+ return null;
+ }
+ return trueCrop;
+ }
+
+ /**
+ * Returns the size of a bitmap in bytes.
+ * @param bmap bitmap whose size to check
+ * @return bitmap size in bytes
+ */
+ public static int getBitmapSize(Bitmap bmap) {
+ return bmap.getRowBytes() * bmap.getHeight();
+ }
+
+ /**
+ * Constrains rotation to be in [0, 90, 180, 270] rounding down.
+ * @param rotation any rotation value, in degrees
+ * @return integer rotation in [0, 90, 180, 270]
+ */
+ public static int constrainedRotation(float rotation) {
+ int r = (int) ((rotation % 360) / 90);
+ r = (r < 0) ? (r + 4) : r;
+ return r * 90;
+ }
+
+ private static float getUnrotated(float[] rotatedRect, float[] center, RectF unrotated) {
+ float dy = rotatedRect[1] - rotatedRect[3];
+ float dx = rotatedRect[0] - rotatedRect[2];
+ float angle = (float) (Math.atan(dy / dx) * 180 / Math.PI);
+ Matrix m = new Matrix();
+ m.setRotate(-angle, center[0], center[1]);
+ float[] unrotatedRect = new float[rotatedRect.length];
+ m.mapPoints(unrotatedRect, rotatedRect);
+ unrotated.set(trapToRect(unrotatedRect));
+ return angle;
+ }
+
+}
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..b98ed1bfd
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropObject.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.gallery3d.filtershow.crop;
+
+import android.graphics.Rect;
+import android.graphics.RectF;
+
+import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
+
+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(float width, float 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) && edge != MOVE_NONE) {
+ // 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 = GeometryMathUtils.normalize(b);
+ float sp = GeometryMathUtils.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..bbb7cfd4c
--- /dev/null
+++ b/src/com/android/gallery3d/filtershow/crop/CropView.java
@@ -0,0 +1,378 @@
+/*
+ * 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.DashPathEffect;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.NinePatchDrawable;
+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";
+
+ private RectF mImageBounds = new RectF();
+ private RectF mScreenBounds = new RectF();
+ private RectF mScreenImageBounds = new RectF();
+ private RectF mScreenCropBounds = new RectF();
+ private Rect mShadowBounds = new Rect();
+
+ private Bitmap mBitmap;
+ private Paint mPaint = new Paint();
+
+ private NinePatchDrawable mShadow;
+ private CropObject mCropObj = null;
+ private Drawable mCropIndicator;
+ private int mIndicatorSize;
+ private int mRotation = 0;
+ private boolean mMovingBlock = false;
+ private Matrix mDisplayMatrix = null;
+ private Matrix mDisplayMatrixInverse = null;
+ private boolean mDirty = false;
+
+ private float mPrevX = 0;
+ private float mPrevY = 0;
+ private float mSpotX = 0;
+ private float mSpotY = 0;
+ private boolean mDoSpot = false;
+
+ private int mShadowMargin = 15;
+ private int mMargin = 32;
+ private int mOverlayShadowColor = 0xCF000000;
+ private int mOverlayWPShadowColor = 0x5F000000;
+ private int mWPMarkerColor = 0x7FFFFFFF;
+ private int mMinSideSize = 90;
+ private int mTouchTolerance = 40;
+ private float mDashOnLength = 20;
+ private float mDashOffLength = 10;
+
+ private enum Mode {
+ NONE, MOVE
+ }
+
+ private Mode mState = Mode.NONE;
+
+ public CropView(Context context) {
+ super(context);
+ setup(context);
+ }
+
+ public CropView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setup(context);
+ }
+
+ public CropView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setup(context);
+ }
+
+ private void setup(Context context) {
+ Resources rsc = context.getResources();
+ mShadow = (NinePatchDrawable) rsc.getDrawable(R.drawable.geometry_shadow);
+ mCropIndicator = rsc.getDrawable(R.drawable.camera_crop);
+ mIndicatorSize = (int) rsc.getDimension(R.dimen.crop_indicator_size);
+ mShadowMargin = (int) rsc.getDimension(R.dimen.shadow_margin);
+ mMargin = (int) rsc.getDimension(R.dimen.preview_margin);
+ mMinSideSize = (int) rsc.getDimension(R.dimen.crop_min_side);
+ mTouchTolerance = (int) rsc.getDimension(R.dimen.crop_touch_tolerance);
+ mOverlayShadowColor = (int) rsc.getColor(R.color.crop_shadow_color);
+ mOverlayWPShadowColor = (int) rsc.getColor(R.color.crop_shadow_wp_color);
+ mWPMarkerColor = (int) rsc.getColor(R.color.crop_wp_markers);
+ mDashOnLength = rsc.getDimension(R.dimen.wp_selector_dash_length);
+ mDashOffLength = rsc.getDimension(R.dimen.wp_selector_off_length);
+ }
+
+ public void initialize(Bitmap image, RectF newCropBounds, RectF newPhotoBounds, int rotation) {
+ mBitmap = image;
+ if (mCropObj != null) {
+ RectF crop = mCropObj.getInnerBounds();
+ RectF containing = mCropObj.getOuterBounds();
+ if (crop != newCropBounds || containing != newPhotoBounds
+ || mRotation != rotation) {
+ mRotation = rotation;
+ mCropObj.resetBoundsTo(newCropBounds, newPhotoBounds);
+ clearDisplay();
+ }
+ } else {
+ mRotation = rotation;
+ mCropObj = new CropObject(newPhotoBounds, newCropBounds, 0);
+ clearDisplay();
+ }
+ }
+
+ public RectF getCrop() {
+ return mCropObj.getInnerBounds();
+ }
+
+ public RectF getPhoto() {
+ return mCropObj.getOuterBounds();
+ }
+
+ @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;
+ }
+ break;
+ case (MotionEvent.ACTION_UP):
+ if (mState == Mode.MOVE) {
+ mCropObj.selectEdge(CropObject.MOVE_NONE);
+ mMovingBlock = false;
+ mPrevX = x;
+ mPrevY = y;
+ mState = Mode.NONE;
+ }
+ 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;
+ }
+ break;
+ default:
+ break;
+ }
+ invalidate();
+ return true;
+ }
+
+ private void reset() {
+ Log.w(LOGTAG, "crop reset called");
+ mState = Mode.NONE;
+ mCropObj = null;
+ mRotation = 0;
+ mMovingBlock = false;
+ clearDisplay();
+ }
+
+ private void clearDisplay() {
+ mDisplayMatrix = null;
+ mDisplayMatrixInverse = null;
+ invalidate();
+ }
+
+ protected void configChanged() {
+ mDirty = true;
+ }
+
+ public void applyFreeAspect() {
+ mCropObj.unsetAspectRatio();
+ invalidate();
+ }
+
+ public void applyOriginalAspect() {
+ RectF outer = mCropObj.getOuterBounds();
+ float w = outer.width();
+ float h = outer.height();
+ if (w > 0 && h > 0) {
+ applyAspect(w, h);
+ mCropObj.resetBoundsTo(outer, outer);
+ } else {
+ Log.w(LOGTAG, "failed to set aspect ratio original");
+ }
+ }
+
+ public void applySquareAspect() {
+ applyAspect(1, 1);
+ }
+
+ public void applyAspect(float x, float y) {
+ if (x <= 0 || y <= 0) {
+ throw new IllegalArgumentException("Bad arguments to applyAspect");
+ }
+ // If we are rotated by 90 degrees from horizontal, swap x and y
+ if (((mRotation < 0) ? -mRotation : mRotation) % 180 == 90) {
+ float tmp = x;
+ x = y;
+ y = tmp;
+ }
+ if (!mCropObj.setInnerAspectRatio(x, y)) {
+ Log.w(LOGTAG, "failed to set aspect ratio");
+ }
+ invalidate();
+ }
+
+ public void setWallpaperSpotlight(float spotlightX, float spotlightY) {
+ mSpotX = spotlightX;
+ mSpotY = spotlightY;
+ if (mSpotX > 0 && mSpotY > 0) {
+ mDoSpot = true;
+ }
+ }
+
+ public void unsetWallpaperSpotlight() {
+ mDoSpot = false;
+ }
+
+ /**
+ * Rotates first d bits in integer x to the left some number of times.
+ */
+ private int bitCycleLeft(int x, int times, int d) {
+ int mask = (1 << d) - 1;
+ int mout = x & mask;
+ times %= d;
+ int hi = mout >> (d - times);
+ int low = (mout << times) & mask;
+ int ret = x & ~mask;
+ ret |= low;
+ ret |= hi;
+ return ret;
+ }
+
+ /**
+ * Find the selected edge or corner in screen coordinates.
+ */
+ private int decode(int movingEdges, float rotation) {
+ int rot = CropMath.constrainedRotation(rotation);
+ switch (rot) {
+ case 90:
+ return bitCycleLeft(movingEdges, 1, 4);
+ case 180:
+ return bitCycleLeft(movingEdges, 2, 4);
+ case 270:
+ return bitCycleLeft(movingEdges, 3, 4);
+ default:
+ return movingEdges;
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ if (mBitmap == null) {
+ return;
+ }
+ if (mDirty) {
+ mDirty = false;
+ clearDisplay();
+ }
+
+ mImageBounds = new RectF(0, 0, mBitmap.getWidth(), mBitmap.getHeight());
+ mScreenBounds = new RectF(0, 0, canvas.getWidth(), canvas.getHeight());
+ mScreenBounds.inset(mMargin, mMargin);
+
+ // If crop object doesn't exist, create it and update it from master
+ // state
+ if (mCropObj == null) {
+ reset();
+ mCropObj = new CropObject(mImageBounds, mImageBounds, 0);
+ }
+
+ // If display matrix doesn't exist, create it and its dependencies
+ if (mDisplayMatrix == null || mDisplayMatrixInverse == null) {
+ mDisplayMatrix = new Matrix();
+ mDisplayMatrix.reset();
+ if (!CropDrawingUtils.setImageToScreenMatrix(mDisplayMatrix, mImageBounds, mScreenBounds,
+ mRotation)) {
+ Log.w(LOGTAG, "failed to get screen matrix");
+ mDisplayMatrix = null;
+ return;
+ }
+ mDisplayMatrixInverse = new Matrix();
+ mDisplayMatrixInverse.reset();
+ if (!mDisplayMatrix.invert(mDisplayMatrixInverse)) {
+ Log.w(LOGTAG, "could not invert display matrix");
+ mDisplayMatrixInverse = null;
+ return;
+ }
+ // Scale min side and tolerance by display matrix scale factor
+ mCropObj.setMinInnerSideSize(mDisplayMatrixInverse.mapRadius(mMinSideSize));
+ mCropObj.setTouchTolerance(mDisplayMatrixInverse.mapRadius(mTouchTolerance));
+ }
+
+ mScreenImageBounds.set(mImageBounds);
+
+ // Draw background shadow
+ if (mDisplayMatrix.mapRect(mScreenImageBounds)) {
+ int margin = (int) mDisplayMatrix.mapRadius(mShadowMargin);
+ mScreenImageBounds.roundOut(mShadowBounds);
+ mShadowBounds.set(mShadowBounds.left - margin, mShadowBounds.top -
+ margin, mShadowBounds.right + margin, mShadowBounds.bottom + margin);
+ mShadow.setBounds(mShadowBounds);
+ mShadow.draw(canvas);
+ }
+
+ mPaint.setAntiAlias(true);
+ mPaint.setFilterBitmap(true);
+ // Draw actual bitmap
+ canvas.drawBitmap(mBitmap, mDisplayMatrix, mPaint);
+
+ mCropObj.getInnerBounds(mScreenCropBounds);
+
+ if (mDisplayMatrix.mapRect(mScreenCropBounds)) {
+
+ // Draw overlay shadows
+ Paint p = new Paint();
+ p.setColor(mOverlayShadowColor);
+ p.setStyle(Paint.Style.FILL);
+ CropDrawingUtils.drawShadows(canvas, p, mScreenCropBounds, mScreenImageBounds);
+
+ // Draw crop rect and markers
+ CropDrawingUtils.drawCropRect(canvas, mScreenCropBounds);
+ if (!mDoSpot) {
+ CropDrawingUtils.drawRuleOfThird(canvas, mScreenCropBounds);
+ } else {
+ Paint wpPaint = new Paint();
+ wpPaint.setColor(mWPMarkerColor);
+ wpPaint.setStrokeWidth(3);
+ wpPaint.setStyle(Paint.Style.STROKE);
+ wpPaint.setPathEffect(new DashPathEffect(new float[]
+ {mDashOnLength, mDashOnLength + mDashOffLength}, 0));
+ p.setColor(mOverlayWPShadowColor);
+ CropDrawingUtils.drawWallpaperSelectionFrame(canvas, mScreenCropBounds,
+ mSpotX, mSpotY, wpPaint, p);
+ }
+ CropDrawingUtils.drawIndicators(canvas, mCropIndicator, mIndicatorSize,
+ mScreenCropBounds, mCropObj.isFixedAspect(), decode(mCropObj.getSelectState(), mRotation));
+ }
+
+ }
+}