diff options
Diffstat (limited to 'src/com/android/gallery3d/filtershow/crop')
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)); + } + + } +} |