blob: a031fa58c4a4f3e1f35ea0deac445e36c51094f0 [file] [log] [blame]
/*
* 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.imageshow;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import com.android.gallery3d.R;
public class ImageCrop extends ImageGeometry {
private static final boolean LOGV = false;
private static final int MOVE_LEFT = 1;
private static final int MOVE_TOP = 2;
private static final int MOVE_RIGHT = 4;
private static final int MOVE_BOTTOM = 8;
private static final int MOVE_BLOCK = 16;
//Corners
private static final int TOP_LEFT = MOVE_TOP | MOVE_LEFT;
private static final int TOP_RIGHT = MOVE_TOP | MOVE_RIGHT;
private static final int BOTTOM_RIGHT = MOVE_BOTTOM | MOVE_RIGHT;
private static final int BOTTOM_LEFT = MOVE_BOTTOM | MOVE_LEFT;
private static final float MIN_CROP_WIDTH_HEIGHT = 0.1f;
private static int mTouchTolerance = 45;
private boolean mFirstDraw = true;
private float mAspectWidth = 1;
private float mAspectHeight = 1;
private boolean mFixAspectRatio = false;
private float mLastRot = 0;
private final Paint borderPaint;
private int movingEdges;
private final Drawable cropIndicator;
private final int indicatorSize;
private final int mBorderColor = Color.argb(128, 255, 255, 255);
private static final String LOGTAG = "ImageCrop";
private static final Paint gPaint = new Paint();
public ImageCrop(Context context) {
super(context);
Resources resources = context.getResources();
cropIndicator = resources.getDrawable(R.drawable.camera_crop);
indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
borderPaint = new Paint();
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setColor(mBorderColor);
borderPaint.setStrokeWidth(2f);
}
public ImageCrop(Context context, AttributeSet attrs) {
super(context, attrs);
Resources resources = context.getResources();
cropIndicator = resources.getDrawable(R.drawable.camera_crop);
indicatorSize = (int) resources.getDimension(R.dimen.crop_indicator_size);
borderPaint = new Paint();
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setColor(mBorderColor);
borderPaint.setStrokeWidth(2f);
}
@Override
public String getName() {
return getContext().getString(R.string.crop);
}
private void swapAspect(){
float temp = mAspectWidth;
mAspectWidth = mAspectHeight;
mAspectHeight = temp;
}
public static void setTouchTolerance(int tolerance){
mTouchTolerance = tolerance;
}
private boolean switchCropBounds(int moving_corner, RectF dst) {
RectF crop = getCropBoundsDisplayed();
float dx1 = 0;
float dy1 = 0;
float dx2 = 0;
float dy2 = 0;
if ((moving_corner & MOVE_RIGHT) != 0) {
dx1 = mCurrentX - crop.right;
} else if ((moving_corner & MOVE_LEFT) != 0) {
dx1 = mCurrentX - crop.left;
}
if ((moving_corner & MOVE_BOTTOM) != 0) {
dy1 = mCurrentY - crop.bottom;
} else if ((moving_corner & MOVE_TOP) != 0) {
dy1 = mCurrentY - crop.top;
}
RectF newCrop = null;
//Fix opposite corner in place and move sides
if (moving_corner == BOTTOM_RIGHT) {
newCrop = new RectF(crop.left, crop.top, crop.left + crop.height(), crop.top
+ crop.width());
} else if (moving_corner == BOTTOM_LEFT) {
newCrop = new RectF(crop.right - crop.height(), crop.top, crop.right, crop.top
+ crop.width());
} else if (moving_corner == TOP_LEFT) {
newCrop = new RectF(crop.right - crop.height(), crop.bottom - crop.width(),
crop.right, crop.bottom);
} else if (moving_corner == TOP_RIGHT) {
newCrop = new RectF(crop.left, crop.bottom - crop.width(), crop.left
+ crop.height(), crop.bottom);
}
if ((moving_corner & MOVE_RIGHT) != 0) {
dx2 = mCurrentX - newCrop.right;
} else if ((moving_corner & MOVE_LEFT) != 0) {
dx2 = mCurrentX - newCrop.left;
}
if ((moving_corner & MOVE_BOTTOM) != 0) {
dy2 = mCurrentY - newCrop.bottom;
} else if ((moving_corner & MOVE_TOP) != 0) {
dy2 = mCurrentY - newCrop.top;
}
if (Math.sqrt(dx1*dx1 + dy1*dy1) > Math.sqrt(dx2*dx2 + dy2*dy2)){
Matrix m = getCropBoundDisplayMatrix();
Matrix m0 = new Matrix();
if (!m.invert(m0)){
if (LOGV)
Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
return false;
}
if (!m0.mapRect(newCrop)){
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
return false;
}
swapAspect();
dst.set(newCrop);
return true;
}
return false;
}
public void apply(float w, float h){
mFixAspectRatio = true;
mAspectWidth = w;
mAspectHeight = h;
setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
getLocalStraighten()));
cropSetup();
saveAndSetPreset();
invalidate();
}
public void applyOriginal() {
mFixAspectRatio = true;
RectF photobounds = getLocalPhotoBounds();
float w = photobounds.width();
float h = photobounds.height();
float scale = Math.min(w, h);
mAspectWidth = w / scale;
mAspectHeight = h / scale;
setLocalCropBounds(getUntranslatedStraightenCropBounds(photobounds,
getLocalStraighten()));
cropSetup();
saveAndSetPreset();
invalidate();
}
public void applyClear() {
mFixAspectRatio = false;
setLocalCropBounds(getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
getLocalStraighten()));
cropSetup();
saveAndSetPreset();
invalidate();
}
private float getScaledMinWidthHeight() {
RectF disp = new RectF(0, 0, getWidth(), getHeight());
float scaled = Math.min(disp.width(), disp.height()) * MIN_CROP_WIDTH_HEIGHT
/ computeScale(getWidth(), getHeight());
return scaled;
}
protected Matrix getCropRotationMatrix(float rotation, RectF localImage) {
Matrix m = getLocalGeoFlipMatrix(localImage.width(), localImage.height());
m.postRotate(rotation, localImage.centerX(), localImage.centerY());
if (!m.rectStaysRect()) {
return null;
}
return m;
}
protected Matrix getCropBoundDisplayMatrix(){
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
if (m == null) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
m = new Matrix();
}
float zoom = computeScale(getWidth(), getHeight());
m.postTranslate(mXOffset, mYOffset);
m.postScale(zoom, zoom, mCenterX, mCenterY);
return m;
}
protected RectF getCropBoundsDisplayed() {
RectF bounds = getLocalCropBounds();
RectF crop = new RectF(bounds);
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
if (m == null) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
m = new Matrix();
} else {
m.mapRect(crop);
}
m = new Matrix();
float zoom = computeScale(getWidth(), getHeight());
m.setScale(zoom, zoom, mCenterX, mCenterY);
m.preTranslate(mXOffset, mYOffset);
m.mapRect(crop);
return crop;
}
private RectF getRotatedCropBounds() {
RectF bounds = getLocalCropBounds();
RectF crop = new RectF(bounds);
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
if (m == null) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP CROP BOUNDS TO RECTANGLE");
return null;
} else {
m.mapRect(crop);
}
return crop;
}
private RectF getUnrotatedCropBounds(RectF cropBounds) {
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
if (m == null) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO GET ROTATION MATRIX");
return null;
}
Matrix m0 = new Matrix();
if (!m.invert(m0)) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX");
return null;
}
RectF crop = new RectF(cropBounds);
if (!m0.mapRect(crop)) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS");
return null;
}
return crop;
}
private RectF getRotatedStraightenBounds() {
RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
getLocalStraighten());
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
if (m == null) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP STRAIGHTEN BOUNDS TO RECTANGLE");
return null;
} else {
m.mapRect(straightenBounds);
}
return straightenBounds;
}
/**
* Sets cropped bounds; modifies the bounds if it's smaller than the allowed
* dimensions.
*/
public void setCropBounds(RectF bounds) {
// Avoid cropping smaller than minimum width or height.
RectF cbounds = new RectF(bounds);
float minWidthHeight = getScaledMinWidthHeight();
float aw = mAspectWidth;
float ah = mAspectHeight;
if (mFixAspectRatio) {
minWidthHeight /= aw * ah;
int r = (int) (getLocalRotation() / 90);
if (r % 2 != 0) {
float temp = aw;
aw = ah;
ah = temp;
}
}
float newWidth = cbounds.width();
float newHeight = cbounds.height();
if (mFixAspectRatio) {
if (newWidth < (minWidthHeight * aw) || newHeight < (minWidthHeight * ah)) {
newWidth = minWidthHeight * aw;
newHeight = minWidthHeight * ah;
}
} else {
if (newWidth < minWidthHeight) {
newWidth = minWidthHeight;
}
if (newHeight < minWidthHeight) {
newHeight = minWidthHeight;
}
}
RectF pbounds = getLocalPhotoBounds();
if (pbounds.width() < minWidthHeight) {
newWidth = pbounds.width();
}
if (pbounds.height() < minWidthHeight) {
newHeight = pbounds.height();
}
cbounds.set(cbounds.left, cbounds.top, cbounds.left + newWidth, cbounds.top + newHeight);
RectF straightenBounds = getUntranslatedStraightenCropBounds(getLocalPhotoBounds(),
getLocalStraighten());
cbounds.intersect(straightenBounds);
if (mFixAspectRatio) {
fixAspectRatio(cbounds, aw, ah);
}
setLocalCropBounds(cbounds);
invalidate();
}
private void detectMovingEdges(float x, float y) {
RectF cropped = getCropBoundsDisplayed();
movingEdges = 0;
// Check left or right.
float left = Math.abs(x - cropped.left);
float right = Math.abs(x - cropped.right);
if ((left <= mTouchTolerance) && (left < right)) {
movingEdges |= MOVE_LEFT;
}
else if (right <= mTouchTolerance) {
movingEdges |= MOVE_RIGHT;
}
// Check top or bottom.
float top = Math.abs(y - cropped.top);
float bottom = Math.abs(y - cropped.bottom);
if ((top <= mTouchTolerance) & (top < bottom)) {
movingEdges |= MOVE_TOP;
}
else if (bottom <= mTouchTolerance) {
movingEdges |= MOVE_BOTTOM;
}
// Check inside block.
if (cropped.contains(x, y) && (movingEdges == 0)) {
movingEdges = MOVE_BLOCK;
}
if (mFixAspectRatio && (movingEdges != MOVE_BLOCK)) {
movingEdges = fixEdgeToCorner(movingEdges);
}
invalidate();
}
private 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;
}
private 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 void moveEdges(float dX, float dY) {
RectF cropped = getRotatedCropBounds();
float minWidthHeight = getScaledMinWidthHeight();
float scale = computeScale(getWidth(), getHeight());
float deltaX = dX / scale;
float deltaY = dY / scale;
int select = movingEdges;
if (mFixAspectRatio && (select != MOVE_BLOCK)) {
// TODO: add in orientation change for fixed aspect
/*if (select == TOP_LEFT || select == TOP_RIGHT ||
select == BOTTOM_LEFT || select == BOTTOM_RIGHT){
RectF blank = new RectF();
if(switchCropBounds(select, blank)){
setCropBounds(blank);
return;
}
}*/
if (select == MOVE_LEFT) {
select |= MOVE_TOP;
}
if (select == MOVE_TOP) {
select |= MOVE_LEFT;
}
if (select == MOVE_RIGHT) {
select |= MOVE_BOTTOM;
}
if (select == MOVE_BOTTOM) {
select |= MOVE_RIGHT;
}
}
if (select == MOVE_BLOCK) {
RectF straight = getRotatedStraightenBounds();
// Move the whole cropped bounds within the photo display bounds.
deltaX = (deltaX > 0) ? Math.min(straight.right - cropped.right, deltaX)
: Math.max(straight.left - cropped.left, deltaX);
deltaY = (deltaY > 0) ? Math.min(straight.bottom - cropped.bottom, deltaY)
: Math.max(straight.top - cropped.top, deltaY);
cropped.offset(deltaX, deltaY);
} else {
float dx = 0;
float dy = 0;
if ((select & MOVE_LEFT) != 0) {
dx = Math.min(cropped.left + deltaX, cropped.right - minWidthHeight) - cropped.left;
}
if ((select & MOVE_TOP) != 0) {
dy = Math.min(cropped.top + deltaY, cropped.bottom - minWidthHeight) - cropped.top;
}
if ((select & MOVE_RIGHT) != 0) {
dx = Math.max(cropped.right + deltaX, cropped.left + minWidthHeight)
- cropped.right;
}
if ((select & MOVE_BOTTOM) != 0) {
dy = Math.max(cropped.bottom + deltaY, cropped.top + minWidthHeight)
- cropped.bottom;
}
if (mFixAspectRatio) {
RectF crop = getCropBoundsDisplayed();
float [] l1 = {crop.left, crop.bottom};
float [] l2 = {crop.right, crop.top};
if(movingEdges == TOP_LEFT || movingEdges == BOTTOM_RIGHT){
l1[1] = crop.top;
l2[1] = crop.bottom;
}
float[] b = { l1[0] - l2[0], l1[1] - l2[1] };
float[] disp = {dx, dy};
float[] bUnit = GeometryMath.normalize(b);
float sp = GeometryMath.scalarProjection(disp, bUnit);
dx = sp * bUnit[0];
dy = sp * bUnit[1];
RectF newCrop = fixedCornerResize(crop, select, dx * scale, dy * scale);
Matrix m = getCropBoundDisplayMatrix();
Matrix m0 = new Matrix();
if (!m.invert(m0)){
if (LOGV)
Log.v(LOGTAG, "FAILED TO INVERT CROP MATRIX");
return;
}
if (!m0.mapRect(newCrop)){
if (LOGV)
Log.v(LOGTAG, "FAILED TO MAP RECTANGLE TO RECTANGLE");
return;
}
setCropBounds(newCrop);
return;
} else {
if ((select & MOVE_LEFT) != 0) {
cropped.left += dx;
}
if ((select & MOVE_TOP) != 0) {
cropped.top += dy;
}
if ((select & MOVE_RIGHT) != 0) {
cropped.right += dx;
}
if ((select & MOVE_BOTTOM) != 0) {
cropped.bottom += dy;
}
}
}
movingEdges = select;
Matrix m = getCropRotationMatrix(getLocalRotation(), getLocalPhotoBounds());
Matrix m0 = new Matrix();
if (!m.invert(m0)) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO INVERT ROTATION MATRIX");
}
if (!m0.mapRect(cropped)) {
if (LOGV)
Log.v(LOGTAG, "FAILED TO UNROTATE CROPPING BOUNDS");
}
setCropBounds(cropped);
}
private void drawIndicator(Canvas canvas, Drawable indicator, 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);
}
@Override
protected void setActionDown(float x, float y) {
super.setActionDown(x, y);
detectMovingEdges(x, y);
}
@Override
protected void setActionUp() {
super.setActionUp();
movingEdges = 0;
}
@Override
protected void setActionMove(float x, float y) {
if (movingEdges != 0){
moveEdges(x - mCurrentX, y - mCurrentY);
}
super.setActionMove(x, y);
}
private void cropSetup() {
if (mFixAspectRatio) {
RectF cb = getRotatedCropBounds();
fixAspectRatio(cb, mAspectWidth, mAspectHeight);
RectF cb0 = getUnrotatedCropBounds(cb);
setCropBounds(cb0);
} else {
setCropBounds(getLocalCropBounds());
}
}
@Override
public void imageLoaded() {
super.imageLoaded();
syncLocalToMasterGeometry();
applyClear();
invalidate();
}
@Override
protected void gainedVisibility() {
float rot = getLocalRotation();
// if has changed orientation via rotate
if( ((int) ((rot - mLastRot) / 90)) % 2 != 0 ){
swapAspect();
}
cropSetup();
mFirstDraw = true;
}
@Override
public void resetParameter() {
super.resetParameter();
cropSetup();
}
@Override
protected void lostVisibility() {
mLastRot = getLocalRotation();
}
private void drawRuleOfThird(Canvas canvas, RectF bounds, Paint p) {
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;
}
}
@Override
protected void drawShape(Canvas canvas, Bitmap image) {
// TODO: move style to xml
gPaint.setAntiAlias(true);
gPaint.setFilterBitmap(true);
gPaint.setDither(true);
gPaint.setARGB(255, 255, 255, 255);
if (mFirstDraw) {
cropSetup();
mFirstDraw = false;
}
float rotation = getLocalRotation();
RectF crop = drawTransformed(canvas, image, gPaint);
gPaint.setColor(mBorderColor);
gPaint.setStrokeWidth(3);
gPaint.setStyle(Paint.Style.STROKE);
drawRuleOfThird(canvas, crop, gPaint);
gPaint.setColor(mBorderColor);
gPaint.setStrokeWidth(3);
gPaint.setStyle(Paint.Style.STROKE);
drawStraighten(canvas, gPaint);
int decoded_moving = decoder(movingEdges, rotation);
canvas.save();
canvas.rotate(rotation, mCenterX, mCenterY);
RectF scaledCrop = unrotatedCropBounds();
boolean notMoving = decoded_moving == 0;
if (((decoded_moving & MOVE_TOP) != 0) || notMoving) {
drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.top);
}
if (((decoded_moving & MOVE_BOTTOM) != 0) || notMoving) {
drawIndicator(canvas, cropIndicator, scaledCrop.centerX(), scaledCrop.bottom);
}
if (((decoded_moving & MOVE_LEFT) != 0) || notMoving) {
drawIndicator(canvas, cropIndicator, scaledCrop.left, scaledCrop.centerY());
}
if (((decoded_moving & MOVE_RIGHT) != 0) || notMoving) {
drawIndicator(canvas, cropIndicator, scaledCrop.right, scaledCrop.centerY());
}
canvas.restore();
}
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;
}
protected int decoder(int movingEdges, float rotation) {
int rot = constrainedRotation(rotation);
switch (rot) {
case 90:
return bitCycleLeft(movingEdges, 3, 4);
case 180:
return bitCycleLeft(movingEdges, 2, 4);
case 270:
return bitCycleLeft(movingEdges, 1, 4);
default:
return movingEdges;
}
}
}