/* * Copyright (C) 2007 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.marshalchen.common.uimodule.cropimage; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.os.Build; import android.util.TypedValue; import android.view.View; import com.marshalchen.common.uimodule.R; /* * Modified from version in AOSP. * * This class is used to display a highlighted cropping rectangle * overlayed on the image. There are two coordinate spaces in use. One is * image, another is screen. computeLayout() uses matrix to map from image * space to screen space. */ class HighlightView { public static final int GROW_NONE = (1 << 0); public static final int GROW_LEFT_EDGE = (1 << 1); public static final int GROW_RIGHT_EDGE = (1 << 2); public static final int GROW_TOP_EDGE = (1 << 3); public static final int GROW_BOTTOM_EDGE = (1 << 4); public static final int MOVE = (1 << 5); private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5; private static final float HANDLE_RADIUS_DP = 12f; private static final float OUTLINE_DP = 2f; enum ModifyMode { None, Move, Grow } enum HandleMode { Changing, Always, Never } RectF cropRect; // Image space Rect drawRect; // Screen space Matrix matrix; private RectF imageRect; // Image space private final Paint outsidePaint = new Paint(); private final Paint outlinePaint = new Paint(); private final Paint handlePaint = new Paint(); private View viewContext; // View displaying image private boolean showThirds; private int highlightColor; private ModifyMode modifyMode = ModifyMode.None; private HandleMode handleMode = HandleMode.Changing; private boolean maintainAspectRatio; private float initialAspectRatio; private float handleRadius; private float outlineWidth; private boolean isFocused; public HighlightView(View context) { viewContext = context; initStyles(context.getContext()); } private void initStyles(Context context) { TypedValue outValue = new TypedValue(); context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true); TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView); try { showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false); highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor, DEFAULT_HIGHLIGHT_COLOR); handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)]; } finally { attributes.recycle(); } } public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) { matrix = new Matrix(m); this.cropRect = cropRect; this.imageRect = new RectF(imageRect); this.maintainAspectRatio = maintainAspectRatio; initialAspectRatio = this.cropRect.width() / this.cropRect.height(); drawRect = computeLayout(); outsidePaint.setARGB(125, 50, 50, 50); outlinePaint.setStyle(Paint.Style.STROKE); outlinePaint.setAntiAlias(true); outlineWidth = dpToPx(OUTLINE_DP); handlePaint.setColor(highlightColor); handlePaint.setStyle(Paint.Style.FILL); handlePaint.setAntiAlias(true); handleRadius = dpToPx(HANDLE_RADIUS_DP); modifyMode = ModifyMode.None; } private float dpToPx(float dp) { return dp * viewContext.getResources().getDisplayMetrics().density; } protected void draw(Canvas canvas) { canvas.save(); Path path = new Path(); outlinePaint.setStrokeWidth(outlineWidth); if (!hasFocus()) { outlinePaint.setColor(Color.BLACK); canvas.drawRect(drawRect, outlinePaint); } else { Rect viewDrawingRect = new Rect(); viewContext.getDrawingRect(viewDrawingRect); path.addRect(new RectF(drawRect), Path.Direction.CW); outlinePaint.setColor(highlightColor); if (isClipPathSupported(canvas)) { canvas.clipPath(path, Region.Op.DIFFERENCE); canvas.drawRect(viewDrawingRect, outsidePaint); } else { drawOutsideFallback(canvas); } canvas.restore(); canvas.drawPath(path, outlinePaint); if (showThirds) { drawThirds(canvas); } if (handleMode == HandleMode.Always || (handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) { drawHandles(canvas); } } } /* * Fall back to naive method for darkening outside crop area */ private void drawOutsideFallback(Canvas canvas) { canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint); canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint); canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint); canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint); } /* * Clip path is broken, unreliable or not supported on: * - JellyBean MR1 * - ICS & ICS MR1 with hardware acceleration turned on */ @SuppressLint("NewApi") private boolean isClipPathSupported(Canvas canvas) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { return false; } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) || Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { return true; } else { return !canvas.isHardwareAccelerated(); } } private void drawHandles(Canvas canvas) { int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2); int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2); canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint); canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint); canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint); canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint); } private void drawThirds(Canvas canvas) { outlinePaint.setStrokeWidth(1); float xThird = (drawRect.right - drawRect.left) / 3; float yThird = (drawRect.bottom - drawRect.top) / 3; canvas.drawLine(drawRect.left + xThird, drawRect.top, drawRect.left + xThird, drawRect.bottom, outlinePaint); canvas.drawLine(drawRect.left + xThird * 2, drawRect.top, drawRect.left + xThird * 2, drawRect.bottom, outlinePaint); canvas.drawLine(drawRect.left, drawRect.top + yThird, drawRect.right, drawRect.top + yThird, outlinePaint); canvas.drawLine(drawRect.left, drawRect.top + yThird * 2, drawRect.right, drawRect.top + yThird * 2, outlinePaint); } public void setMode(ModifyMode mode) { if (mode != modifyMode) { modifyMode = mode; viewContext.invalidate(); } } // Determines which edges are hit by touching at (x, y) public int getHit(float x, float y) { Rect r = computeLayout(); final float hysteresis = 20F; int retval = GROW_NONE; // verticalCheck makes sure the position is between the top and // the bottom edge (with some tolerance). Similar for horizCheck. boolean verticalCheck = (y >= r.top - hysteresis) && (y < r.bottom + hysteresis); boolean horizCheck = (x >= r.left - hysteresis) && (x < r.right + hysteresis); // Check whether the position is near some edge(s) if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) { retval |= GROW_LEFT_EDGE; } if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) { retval |= GROW_RIGHT_EDGE; } if ((Math.abs(r.top - y) < hysteresis) && horizCheck) { retval |= GROW_TOP_EDGE; } if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) { retval |= GROW_BOTTOM_EDGE; } // Not near any edge but inside the rectangle: move if (retval == GROW_NONE && r.contains((int) x, (int) y)) { retval = MOVE; } return retval; } // Handles motion (dx, dy) in screen space. // The "edge" parameter specifies which edges the user is dragging. void handleMotion(int edge, float dx, float dy) { Rect r = computeLayout(); if (edge == MOVE) { // Convert to image space before sending to moveBy() moveBy(dx * (cropRect.width() / r.width()), dy * (cropRect.height() / r.height())); } else { if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) { dx = 0; } if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) { dy = 0; } // Convert to image space before sending to growBy() float xDelta = dx * (cropRect.width() / r.width()); float yDelta = dy * (cropRect.height() / r.height()); growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta, (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta); } } // Grows the cropping rectangle by (dx, dy) in image space void moveBy(float dx, float dy) { Rect invalRect = new Rect(drawRect); cropRect.offset(dx, dy); // Put the cropping rectangle inside image rectangle cropRect.offset( Math.max(0, imageRect.left - cropRect.left), Math.max(0, imageRect.top - cropRect.top)); cropRect.offset( Math.min(0, imageRect.right - cropRect.right), Math.min(0, imageRect.bottom - cropRect.bottom)); drawRect = computeLayout(); invalRect.union(drawRect); invalRect.inset(-(int) handleRadius, -(int) handleRadius); viewContext.invalidate(invalRect); } // Grows the cropping rectangle by (dx, dy) in image space. void growBy(float dx, float dy) { if (maintainAspectRatio) { if (dx != 0) { dy = dx / initialAspectRatio; } else if (dy != 0) { dx = dy * initialAspectRatio; } } // Don't let the cropping rectangle grow too fast. // Grow at most half of the difference between the image rectangle and // the cropping rectangle. RectF r = new RectF(cropRect); if (dx > 0F && r.width() + 2 * dx > imageRect.width()) { dx = (imageRect.width() - r.width()) / 2F; if (maintainAspectRatio) { dy = dx / initialAspectRatio; } } if (dy > 0F && r.height() + 2 * dy > imageRect.height()) { dy = (imageRect.height() - r.height()) / 2F; if (maintainAspectRatio) { dx = dy * initialAspectRatio; } } r.inset(-dx, -dy); // Don't let the cropping rectangle shrink too fast final float widthCap = 25F; if (r.width() < widthCap) { r.inset(-(widthCap - r.width()) / 2F, 0F); } float heightCap = maintainAspectRatio ? (widthCap / initialAspectRatio) : widthCap; if (r.height() < heightCap) { r.inset(0F, -(heightCap - r.height()) / 2F); } // Put the cropping rectangle inside the image rectangle if (r.left < imageRect.left) { r.offset(imageRect.left - r.left, 0F); } else if (r.right > imageRect.right) { r.offset(-(r.right - imageRect.right), 0F); } if (r.top < imageRect.top) { r.offset(0F, imageRect.top - r.top); } else if (r.bottom > imageRect.bottom) { r.offset(0F, -(r.bottom - imageRect.bottom)); } cropRect.set(r); drawRect = computeLayout(); viewContext.invalidate(); } // Returns the cropping rectangle in image space with specified scale public Rect getScaledCropRect(float scale) { return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale), (int) (cropRect.right * scale), (int) (cropRect.bottom * scale)); } // Maps the cropping rectangle from image space to screen space private Rect computeLayout() { RectF r = new RectF(cropRect.left, cropRect.top, cropRect.right, cropRect.bottom); matrix.mapRect(r); return new Rect(Math.round(r.left), Math.round(r.top), Math.round(r.right), Math.round(r.bottom)); } public void invalidate() { drawRect = computeLayout(); } public boolean hasFocus() { return isFocused; } public void setFocus(boolean isFocused) { this.isFocused = isFocused; } }