/*
* Copyright 2013, Edmodo, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in compliance with the License.
* You may obtain a copy of the License in the LICENSE file, or 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.smartandroid.sa.cropper;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
/**
* A custom View representing the crop window and the shaded background outside
* the crop window.
*/
public class CropOverlayView extends View {
// Private Constants ///////////////////////////////////////////////////////
private static final int SNAP_RADIUS_DP = 6;
private static final float DEFAULT_SHOW_GUIDELINES_LIMIT = 100;
// Gets default values from PaintUtil, sets a bunch of values such that the
// corners will draw correctly
private static final float DEFAULT_CORNER_THICKNESS_DP = PaintUtil
.getCornerThickness();
private static final float DEFAULT_LINE_THICKNESS_DP = PaintUtil
.getLineThickness();
private static final float DEFAULT_CORNER_OFFSET_DP = (DEFAULT_CORNER_THICKNESS_DP / 2)
- (DEFAULT_LINE_THICKNESS_DP / 2);
private static final float DEFAULT_CORNER_EXTENSION_DP = DEFAULT_CORNER_THICKNESS_DP
/ 2 + DEFAULT_CORNER_OFFSET_DP;
private static final float DEFAULT_CORNER_LENGTH_DP = 20;
// mGuidelines enumerations
private static final int GUIDELINES_OFF = 0;
private static final int GUIDELINES_ON_TOUCH = 1;
private static final int GUIDELINES_ON = 2;
// Member Variables ////////////////////////////////////////////////////////
// The Paint used to draw the white rectangle around the crop area.
private Paint mBorderPaint;
// The Paint used to draw the guidelines within the crop area when pressed.
private Paint mGuidelinePaint;
// The Paint used to draw the corners of the Border
private Paint mCornerPaint;
// The Paint used to darken the surrounding areas outside the crop area.
private Paint mBackgroundPaint;
// The bounding box around the Bitmap that we are cropping.
private Rect mBitmapRect;
// The radius of the touch zone (in pixels) around a given Handle.
private float mHandleRadius;
// An edge of the crop window will snap to the corresponding edge of a
// specified bounding box when the crop window edge is less than or equal to
// this distance (in pixels) away from the bounding box edge.
private float mSnapRadius;
// Holds the x and y offset between the exact touch location and the exact
// handle location that is activated. There may be an offset because we
// allow for some leeway (specified by mHandleRadius) in activating a
// handle. However, we want to maintain these offset values while the handle
// is being dragged so that the handle doesn't jump.
private Pair<Float, Float> mTouchOffset;
// The Handle that is currently pressed; null if no Handle is pressed.
private Handle mPressedHandle;
// Flag indicating if the crop area should always be a certain aspect ratio
// (indicated by mTargetAspectRatio).
private boolean mFixAspectRatio = CropImageView.DEFAULT_FIXED_ASPECT_RATIO;
// Floats to save the current aspect ratio of the image
private int mAspectRatioX = CropImageView.DEFAULT_ASPECT_RATIO_X;
private int mAspectRatioY = CropImageView.DEFAULT_ASPECT_RATIO_Y;
// The aspect ratio that the crop area should maintain; this variable is
// only used when mMaintainAspectRatio is true.
private float mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
// Instance variables for customizable attributes
private int mGuidelines;
// Whether the Crop View has been initialized for the first time
private boolean initializedCropWindow = false;
// Instance variables for the corner values
private float mCornerExtension;
private float mCornerOffset;
private float mCornerLength;
// Constructors ////////////////////////////////////////////////////////////
public CropOverlayView(Context context) {
super(context);
init(context);
}
public CropOverlayView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
// View Methods ////////////////////////////////////////////////////////////
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// Initialize the crop window here because we need the size of the view
// to have been determined.
initCropWindow(mBitmapRect);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw translucent background for the cropped area.
drawBackground(canvas, mBitmapRect);
if (showGuidelines()) {
// Determines whether guidelines should be drawn or not
if (mGuidelines == GUIDELINES_ON) {
drawRuleOfThirdsGuidelines(canvas);
} else if (mGuidelines == GUIDELINES_ON_TOUCH) {
// Draw only when resizing
if (mPressedHandle != null)
drawRuleOfThirdsGuidelines(canvas);
} else if (mGuidelines == GUIDELINES_OFF) {
// Do nothing
}
}
// Draws the main crop window border.
canvas.drawRect(Edge.LEFT.getCoordinate(), Edge.TOP.getCoordinate(),
Edge.RIGHT.getCoordinate(), Edge.BOTTOM.getCoordinate(),
mBorderPaint);
drawCorners(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// If this View is not enabled, don't allow for touch interactions.
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
onActionDown(event.getX(), event.getY());
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
getParent().requestDisallowInterceptTouchEvent(false);
onActionUp();
return true;
case MotionEvent.ACTION_MOVE:
onActionMove(event.getX(), event.getY());
getParent().requestDisallowInterceptTouchEvent(true);
return true;
default:
return false;
}
}
// Public Methods //////////////////////////////////////////////////////////
/**
* Informs the CropOverlayView of the image's position relative to the
* ImageView. This is necessary to call in order to draw the crop window.
*
* @param bitmapRect
* the image's bounding box
*/
public void setBitmapRect(Rect bitmapRect) {
mBitmapRect = bitmapRect;
initCropWindow(mBitmapRect);
}
/**
* Resets the crop overlay view.
*
* @param bitmap
* the Bitmap to set
*/
public void resetCropOverlayView() {
if (initializedCropWindow) {
initCropWindow(mBitmapRect);
invalidate();
}
}
/**
* Sets the guidelines for the CropOverlayView to be either on, off, or to
* show when resizing the application.
*
* @param guidelines
* Integer that signals whether the guidelines should be on, off,
* or only showing when resizing.
*/
public void setGuidelines(int guidelines) {
if (guidelines < 0 || guidelines > 2)
throw new IllegalArgumentException(
"Guideline value must be set between 0 and 2. See documentation.");
else {
mGuidelines = guidelines;
if (initializedCropWindow) {
initCropWindow(mBitmapRect);
invalidate();
}
}
}
/**
* Sets whether the aspect ratio is fixed or not; true fixes the aspect
* ratio, while false allows it to be changed.
*
* @param fixAspectRatio
* Boolean that signals whether the aspect ratio should be
* maintained.
*/
public void setFixedAspectRatio(boolean fixAspectRatio) {
mFixAspectRatio = fixAspectRatio;
if (initializedCropWindow) {
initCropWindow(mBitmapRect);
invalidate();
}
}
/**
* Sets the X value of the aspect ratio; is defaulted to 1.
*
* @param aspectRatioX
* int that specifies the new X value of the aspect ratio
*/
public void setAspectRatioX(int aspectRatioX) {
if (aspectRatioX <= 0)
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
else {
mAspectRatioX = aspectRatioX;
mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
if (initializedCropWindow) {
initCropWindow(mBitmapRect);
invalidate();
}
}
}
/**
* Sets the Y value of the aspect ratio; is defaulted to 1.
*
* @param aspectRatioY
* int that specifies the new Y value of the aspect ratio
*/
public void setAspectRatioY(int aspectRatioY) {
if (aspectRatioY <= 0)
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
else {
mAspectRatioY = aspectRatioY;
mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
if (initializedCropWindow) {
initCropWindow(mBitmapRect);
invalidate();
}
}
}
/**
* Sets all initial values, but does not call initCropWindow to reset the
* views. Used once at the very start to initialize the attributes.
*
* @param guidelines
* Integer that signals whether the guidelines should be on, off,
* or only showing when resizing.
* @param fixAspectRatio
* Boolean that signals whether the aspect ratio should be
* maintained.
* @param aspectRatioX
* float that specifies the new X value of the aspect ratio
* @param aspectRatioY
* float that specifies the new Y value of the aspect ratio
*/
public void setInitialAttributeValues(int guidelines,
boolean fixAspectRatio, int aspectRatioX, int aspectRatioY) {
if (guidelines < 0 || guidelines > 2)
throw new IllegalArgumentException(
"Guideline value must be set between 0 and 2. See documentation.");
else
mGuidelines = guidelines;
mFixAspectRatio = fixAspectRatio;
if (aspectRatioX <= 0)
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
else {
mAspectRatioX = aspectRatioX;
mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
}
if (aspectRatioY <= 0)
throw new IllegalArgumentException(
"Cannot set aspect ratio value to a number less than or equal to 0.");
else {
mAspectRatioY = aspectRatioY;
mTargetAspectRatio = ((float) mAspectRatioX) / mAspectRatioY;
}
}
// Private Methods /////////////////////////////////////////////////////////
private void init(Context context) {
DisplayMetrics displayMetrics = context.getResources()
.getDisplayMetrics();
mHandleRadius = HandleUtil.getTargetRadius(context);
mSnapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
SNAP_RADIUS_DP, displayMetrics);
mBorderPaint = PaintUtil.newBorderPaint(context);
mGuidelinePaint = PaintUtil.newGuidelinePaint();
mBackgroundPaint = PaintUtil.newBackgroundPaint(context);
mCornerPaint = PaintUtil.newCornerPaint(context);
// Sets the values for the corner sizes
mCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
DEFAULT_CORNER_OFFSET_DP, displayMetrics);
mCornerExtension = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, DEFAULT_CORNER_EXTENSION_DP,
displayMetrics);
mCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
DEFAULT_CORNER_LENGTH_DP, displayMetrics);
// Sets guidelines to default until specified otherwise
mGuidelines = CropImageView.DEFAULT_GUIDELINES;
}
/**
* Set the initial crop window size and position. This is dependent on the
* size and position of the image being cropped.
*
* @param bitmapRect
* the bounding box around the image being cropped
*/
private void initCropWindow(Rect bitmapRect) {
// Tells the attribute functions the crop window has already been
// initialized
if (initializedCropWindow == false)
initializedCropWindow = true;
if (mFixAspectRatio) {
// If the image aspect ratio is wider than the crop aspect ratio,
// then the image height is the determining initial length. Else,
// vice-versa.
if (AspectRatioUtil.calculateAspectRatio(bitmapRect) > mTargetAspectRatio) {
Edge.TOP.setCoordinate(bitmapRect.top);
Edge.BOTTOM.setCoordinate(bitmapRect.bottom);
final float centerX = getWidth() / 2f;
// Limits the aspect ratio to no less than 40 wide or 40 tall
final float cropWidth = Math
.max(Edge.MIN_CROP_LENGTH_PX, AspectRatioUtil
.calculateWidth(Edge.TOP.getCoordinate(),
Edge.BOTTOM.getCoordinate(),
mTargetAspectRatio));
// Create new TargetAspectRatio if the original one does not fit
// the screen
if (cropWidth == Edge.MIN_CROP_LENGTH_PX)
mTargetAspectRatio = (Edge.MIN_CROP_LENGTH_PX)
/ (Edge.BOTTOM.getCoordinate() - Edge.TOP
.getCoordinate());
final float halfCropWidth = cropWidth / 2f;
Edge.LEFT.setCoordinate(centerX - halfCropWidth);
Edge.RIGHT.setCoordinate(centerX + halfCropWidth);
} else {
Edge.LEFT.setCoordinate(bitmapRect.left);
Edge.RIGHT.setCoordinate(bitmapRect.right);
final float centerY = getHeight() / 2f;
// Limits the aspect ratio to no less than 40 wide or 40 tall
final float cropHeight = Math
.max(Edge.MIN_CROP_LENGTH_PX, AspectRatioUtil
.calculateHeight(Edge.LEFT.getCoordinate(),
Edge.RIGHT.getCoordinate(),
mTargetAspectRatio));
// Create new TargetAspectRatio if the original one does not fit
// the screen
if (cropHeight == Edge.MIN_CROP_LENGTH_PX)
mTargetAspectRatio = (Edge.RIGHT.getCoordinate() - Edge.LEFT
.getCoordinate()) / Edge.MIN_CROP_LENGTH_PX;
final float halfCropHeight = cropHeight / 2f;
Edge.TOP.setCoordinate(centerY - halfCropHeight);
Edge.BOTTOM.setCoordinate(centerY + halfCropHeight);
}
} else { // ... do not fix aspect ratio...
// Initialize crop window to have 10% padding w/ respect to image.
final float horizontalPadding = 0.1f * bitmapRect.width();
final float verticalPadding = 0.1f * bitmapRect.height();
Edge.LEFT.setCoordinate(bitmapRect.left + horizontalPadding);
Edge.TOP.setCoordinate(bitmapRect.top + verticalPadding);
Edge.RIGHT.setCoordinate(bitmapRect.right - horizontalPadding);
Edge.BOTTOM.setCoordinate(bitmapRect.bottom - verticalPadding);
}
}
/**
* Indicates whether the crop window is small enough that the guidelines
* should be shown. Public because this function is also used to determine
* if the center handle should be focused.
*
* @return boolean Whether the guidelines should be shown or not
*/
public static boolean showGuidelines() {
if ((Math.abs(Edge.LEFT.getCoordinate() - Edge.RIGHT.getCoordinate()) < DEFAULT_SHOW_GUIDELINES_LIMIT)
|| (Math.abs(Edge.TOP.getCoordinate()
- Edge.BOTTOM.getCoordinate()) < DEFAULT_SHOW_GUIDELINES_LIMIT))
return false;
else
return true;
}
private void drawRuleOfThirdsGuidelines(Canvas canvas) {
final float left = Edge.LEFT.getCoordinate();
final float top = Edge.TOP.getCoordinate();
final float right = Edge.RIGHT.getCoordinate();
final float bottom = Edge.BOTTOM.getCoordinate();
// Draw vertical guidelines.
final float oneThirdCropWidth = Edge.getWidth() / 3;
final float x1 = left + oneThirdCropWidth;
canvas.drawLine(x1, top, x1, bottom, mGuidelinePaint);
final float x2 = right - oneThirdCropWidth;
canvas.drawLine(x2, top, x2, bottom, mGuidelinePaint);
// Draw horizontal guidelines.
final float oneThirdCropHeight = Edge.getHeight() / 3;
final float y1 = top + oneThirdCropHeight;
canvas.drawLine(left, y1, right, y1, mGuidelinePaint);
final float y2 = bottom - oneThirdCropHeight;
canvas.drawLine(left, y2, right, y2, mGuidelinePaint);
}
private void drawBackground(Canvas canvas, Rect bitmapRect) {
final float left = Edge.LEFT.getCoordinate();
final float top = Edge.TOP.getCoordinate();
final float right = Edge.RIGHT.getCoordinate();
final float bottom = Edge.BOTTOM.getCoordinate();
/*-
-------------------------------------
| top |
-------------------------------------
| | | |
| | | |
| left | | right |
| | | |
| | | |
-------------------------------------
| bottom |
-------------------------------------
*/
// Draw "top", "bottom", "left", then "right" quadrants.
canvas.drawRect(bitmapRect.left, bitmapRect.top, bitmapRect.right, top,
mBackgroundPaint);
canvas.drawRect(bitmapRect.left, bottom, bitmapRect.right,
bitmapRect.bottom, mBackgroundPaint);
canvas.drawRect(bitmapRect.left, top, left, bottom, mBackgroundPaint);
canvas.drawRect(right, top, bitmapRect.right, bottom, mBackgroundPaint);
}
private void drawCorners(Canvas canvas) {
final float left = Edge.LEFT.getCoordinate();
final float top = Edge.TOP.getCoordinate();
final float right = Edge.RIGHT.getCoordinate();
final float bottom = Edge.BOTTOM.getCoordinate();
// Draws the corner lines
// Top left
canvas.drawLine(left - mCornerOffset, top - mCornerExtension, left
- mCornerOffset, top + mCornerLength, mCornerPaint);
canvas.drawLine(left, top - mCornerOffset, left + mCornerLength, top
- mCornerOffset, mCornerPaint);
// Top right
canvas.drawLine(right + mCornerOffset, top - mCornerExtension, right
+ mCornerOffset, top + mCornerLength, mCornerPaint);
canvas.drawLine(right, top - mCornerOffset, right - mCornerLength, top
- mCornerOffset, mCornerPaint);
// Bottom left
canvas.drawLine(left - mCornerOffset, bottom + mCornerExtension, left
- mCornerOffset, bottom - mCornerLength, mCornerPaint);
canvas.drawLine(left, bottom + mCornerOffset, left + mCornerLength,
bottom + mCornerOffset, mCornerPaint);
// Bottom left
canvas.drawLine(right + mCornerOffset, bottom + mCornerExtension, right
+ mCornerOffset, bottom - mCornerLength, mCornerPaint);
canvas.drawLine(right, bottom + mCornerOffset, right - mCornerLength,
bottom + mCornerOffset, mCornerPaint);
}
/**
* Handles a {@link MotionEvent#ACTION_DOWN} event.
*
* @param x
* the x-coordinate of the down action
* @param y
* the y-coordinate of the down action
*/
private void onActionDown(float x, float y) {
final float left = Edge.LEFT.getCoordinate();
final float top = Edge.TOP.getCoordinate();
final float right = Edge.RIGHT.getCoordinate();
final float bottom = Edge.BOTTOM.getCoordinate();
mPressedHandle = HandleUtil.getPressedHandle(x, y, left, top, right,
bottom, mHandleRadius);
if (mPressedHandle == null)
return;
// Calculate the offset of the touch point from the precise location
// of the handle. Save these values in a member variable since we want
// to maintain this offset as we drag the handle.
mTouchOffset = HandleUtil.getOffset(mPressedHandle, x, y, left, top,
right, bottom);
invalidate();
}
/**
* Handles a {@link MotionEvent#ACTION_UP} or
* {@link MotionEvent#ACTION_CANCEL} event.
*/
private void onActionUp() {
if (mPressedHandle == null)
return;
mPressedHandle = null;
invalidate();
}
/**
* Handles a {@link MotionEvent#ACTION_MOVE} event.
*
* @param x
* the x-coordinate of the move event
* @param y
* the y-coordinate of the move event
*/
private void onActionMove(float x, float y) {
if (mPressedHandle == null)
return;
// Adjust the coordinates for the finger position's offset (i.e. the
// distance from the initial touch to the precise handle location).
// We want to maintain the initial touch's distance to the pressed
// handle so that the crop window size does not "jump".
x += mTouchOffset.first;
y += mTouchOffset.second;
// Calculate the new crop window size/position.
if (mFixAspectRatio) {
mPressedHandle.updateCropWindow(x, y, mTargetAspectRatio,
mBitmapRect, mSnapRadius);
} else {
mPressedHandle.updateCropWindow(x, y, mBitmapRect, mSnapRadius);
}
invalidate();
}
}