/* * 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.theartofdev.edmodo.cropper; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; import android.media.ExifInterface; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import com.edmodo.cropper.R; import com.theartofdev.edmodo.cropper.cropwindow.CropOverlayView; import com.theartofdev.edmodo.cropper.cropwindow.edge.Edge; import com.theartofdev.edmodo.cropper.util.ImageViewUtil; /** * Custom view that provides cropping capabilities to an image. */ public class CropImageView extends FrameLayout { //region: Fields and Consts private static final Rect EMPTY_RECT = new Rect(); // Sets the default image guidelines to show when resizing public static final int DEFAULT_GUIDELINES = 1; public static final boolean DEFAULT_FIXED_ASPECT_RATIO = false; public static final int DEFAULT_ASPECT_RATIO_X = 1; public static final int DEFAULT_ASPECT_RATIO_Y = 1; public static final int DEFAULT_SCALE_TYPE_INDEX = 0; public static final int DEFAULT_CROP_SHAPE_INDEX = 0; private static final int DEFAULT_IMAGE_RESOURCE = 0; private static final ImageView.ScaleType[] VALID_SCALE_TYPES = new ImageView.ScaleType[]{ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_CENTER}; private static final CropShape[] VALID_CROP_SHAPES = new CropShape[]{CropShape.RECTANGLE, CropShape.OVAL}; private static final String DEGREES_ROTATED = "DEGREES_ROTATED"; private ImageView mImageView; private CropOverlayView mCropOverlayView; private Bitmap mBitmap; private int mDegreesRotated = 0; private int mLayoutWidth; private int mLayoutHeight; /** * Instance variables for customizable attributes */ private int mGuidelines = DEFAULT_GUIDELINES; private boolean mFixAspectRatio = DEFAULT_FIXED_ASPECT_RATIO; private int mAspectRatioX = DEFAULT_ASPECT_RATIO_X; private int mAspectRatioY = DEFAULT_ASPECT_RATIO_Y; private int mImageResource = DEFAULT_IMAGE_RESOURCE; private ImageView.ScaleType mScaleType = VALID_SCALE_TYPES[DEFAULT_SCALE_TYPE_INDEX]; /** * The shape of the cropping area - rectangle/circular. */ private CropImageView.CropShape mCropShape; /** * The URI that the image was loaded from (if loaded from URI) */ private Uri mLoadedImageUri; /** * The sample size the image was loaded by if was loaded by URI */ private int mLoadedSampleSize = 1; //endregion public CropImageView(Context context) { super(context); init(context); } public CropImageView(Context context, AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0); try { mGuidelines = ta.getInteger(R.styleable.CropImageView_guidelines, DEFAULT_GUIDELINES); mFixAspectRatio = ta.getBoolean(R.styleable.CropImageView_fixAspectRatio, DEFAULT_FIXED_ASPECT_RATIO); mAspectRatioX = ta.getInteger(R.styleable.CropImageView_aspectRatioX, DEFAULT_ASPECT_RATIO_X); mAspectRatioY = ta.getInteger(R.styleable.CropImageView_aspectRatioY, DEFAULT_ASPECT_RATIO_Y); mImageResource = ta.getResourceId(R.styleable.CropImageView_imageResource, DEFAULT_IMAGE_RESOURCE); mScaleType = VALID_SCALE_TYPES[ta.getInt(R.styleable.CropImageView_scaleType, DEFAULT_SCALE_TYPE_INDEX)]; mCropShape = VALID_CROP_SHAPES[ta.getInt(R.styleable.CropImageView_cropShape, DEFAULT_CROP_SHAPE_INDEX)]; } finally { ta.recycle(); } init(context); } /** * Set the scale type of the image in the crop view */ public ImageView.ScaleType getScaleType() { return mScaleType; } /** * Set the scale type of the image in the crop view */ public void setScaleType(ImageView.ScaleType scaleType) { mScaleType = scaleType; if (mImageView != null) mImageView.setScaleType(mScaleType); } /** * The shape of the cropping area - rectangle/circular. */ public CropImageView.CropShape getCropShape() { return mCropShape; } /** * The shape of the cropping area - rectangle/circular. */ public void setCropShape(CropImageView.CropShape cropShape) { if (cropShape != mCropShape) { mCropShape = cropShape; mCropOverlayView.setCropShape(cropShape); } } /** * 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) { mCropOverlayView.setFixedAspectRatio(fixAspectRatio); } /** * 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) { mCropOverlayView.setGuidelines(guidelines); } /** * Sets the both the X and Y values of the aspectRatio. * * @param aspectRatioX int that specifies the new X value of the aspect ratio * @param aspectRatioY int that specifies the new Y value of the aspect ratio */ public void setAspectRatio(int aspectRatioX, int aspectRatioY) { mAspectRatioX = aspectRatioX; mCropOverlayView.setAspectRatioX(mAspectRatioX); mAspectRatioY = aspectRatioY; mCropOverlayView.setAspectRatioY(mAspectRatioY); } /** * Returns the integer of the imageResource */ public int getImageResource() { return mImageResource; } /** * Sets a Bitmap as the content of the CropImageView. * * @param bitmap the Bitmap to set */ public void setImageBitmap(Bitmap bitmap) { // if we allocated the bitmap, release it as fast as possible if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) { mBitmap.recycle(); } // clean the loaded image flags for new image mImageResource = 0; mLoadedImageUri = null; mLoadedSampleSize = 1; mDegreesRotated = 0; mBitmap = bitmap; mImageView.setImageBitmap(mBitmap); if (mCropOverlayView != null) { mCropOverlayView.resetCropOverlayView(); } } /** * Sets a Bitmap and initializes the image rotation according to the EXIT data.<br> * <br> * The EXIF can be retrieved by doing the following: * <code>ExifInterface exif = new ExifInterface(path);</code> * * @param bitmap the original bitmap to set; if null, this * @param exif the EXIF information about this bitmap; may be null */ public void setImageBitmap(Bitmap bitmap, ExifInterface exif) { if (bitmap != null && exif != null) { ImageViewUtil.RotateBitmapResult result = ImageViewUtil.rotateBitmapByExif(bitmap, exif); bitmap = result.bitmap; mDegreesRotated = result.degrees; } setImageBitmap(bitmap); } /** * Sets a Drawable as the content of the CropImageView. * * @param resId the drawable resource ID to set */ public void setImageResource(int resId) { if (resId != 0) { Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId); setImageBitmap(bitmap); mImageResource = resId; } } /** * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br> * Can be used with URI from gallery or camera source.<br> * Will rotate the image by exif data.<br> * * @param uri the URI to load the image from */ public void setImageUri(Uri uri) { if (uri != null) { DisplayMetrics metrics = getResources().getDisplayMetrics(); double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1; int width = (int) (metrics.widthPixels * densityAdj); int height = (int) (metrics.heightPixels * densityAdj); ImageViewUtil.DecodeBitmapResult decodeResult = ImageViewUtil.decodeSampledBitmap(getContext(), uri, width, height); ImageViewUtil.RotateBitmapResult rotateResult = ImageViewUtil.rotateBitmapByExif(getContext(), decodeResult.bitmap, uri); setImageBitmap(rotateResult.bitmap); mLoadedImageUri = uri; mLoadedSampleSize = decodeResult.sampleSize; mDegreesRotated = rotateResult.degrees; } } /** * Gets the crop window's position relative to the source Bitmap (not the image * displayed in the CropImageView). * * @return a RectF instance containing cropped area boundaries of the source Bitmap */ public Rect getActualCropRect() { if (mBitmap != null) { final Rect displayedImageRect = ImageViewUtil.getBitmapRect(mBitmap, mImageView, mScaleType); // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for width. final float actualImageWidth = mBitmap.getWidth(); final float displayedImageWidth = displayedImageRect.width(); final float scaleFactorWidth = actualImageWidth / displayedImageWidth; // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for height. final float actualImageHeight = mBitmap.getHeight(); final float displayedImageHeight = displayedImageRect.height(); final float scaleFactorHeight = actualImageHeight / displayedImageHeight; // Get crop window position relative to the displayed image. final float displayedCropLeft = Edge.LEFT.getCoordinate() - displayedImageRect.left; final float displayedCropTop = Edge.TOP.getCoordinate() - displayedImageRect.top; final float displayedCropWidth = Edge.getWidth(); final float displayedCropHeight = Edge.getHeight(); // Scale the crop window position to the actual size of the Bitmap. float actualCropLeft = displayedCropLeft * scaleFactorWidth; float actualCropTop = displayedCropTop * scaleFactorHeight; float actualCropRight = actualCropLeft + displayedCropWidth * scaleFactorWidth; float actualCropBottom = actualCropTop + displayedCropHeight * scaleFactorHeight; // Correct for floating point errors. Crop rect boundaries should not exceed the source Bitmap bounds. actualCropLeft = Math.max(0f, actualCropLeft); actualCropTop = Math.max(0f, actualCropTop); actualCropRight = Math.min(mBitmap.getWidth(), actualCropRight); actualCropBottom = Math.min(mBitmap.getHeight(), actualCropBottom); return new Rect((int) actualCropLeft, (int) actualCropTop, (int) actualCropRight, (int) actualCropBottom); } else { return null; } } /** * Gets the crop window's position relative to the source Bitmap (not the image * displayed in the CropImageView) and the original rotation. * * @return a RectF instance containing cropped area boundaries of the source Bitmap */ @SuppressWarnings("SuspiciousNameCombination") public Rect getActualCropRectNoRotation() { if (mBitmap != null) { Rect rect = getActualCropRect(); int rotateSide = mDegreesRotated / 90; if (rotateSide == 1) { rect.set(rect.top, mBitmap.getWidth() - rect.right, rect.bottom, mBitmap.getWidth() - rect.left); } else if (rotateSide == 2) { rect.set(mBitmap.getWidth() - rect.right, mBitmap.getHeight() - rect.bottom, mBitmap.getWidth() - rect.left, mBitmap.getHeight() - rect.top); } else if (rotateSide == 3) { rect.set(mBitmap.getHeight() - rect.bottom, rect.left, mBitmap.getHeight() - rect.top, rect.right); } rect.set(rect.left * mLoadedSampleSize, rect.top * mLoadedSampleSize, rect.right * mLoadedSampleSize, rect.bottom * mLoadedSampleSize); return rect; } else { return null; } } /** * Rotates image by the specified number of degrees clockwise. Cycles from 0 to 360 * degrees. * * @param degrees Integer specifying the number of degrees to rotate. */ public void rotateImage(int degrees) { if (mBitmap != null) { Matrix matrix = new Matrix(); matrix.postRotate(degrees); Bitmap bitmap = Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(), mBitmap.getHeight(), matrix, true); setImageBitmap(bitmap); mDegreesRotated += degrees; mDegreesRotated = mDegreesRotated % 360; } } /** * Gets the cropped image based on the current crop window. * * @return a new Bitmap representing the cropped image */ public Bitmap getCroppedImage() { return getCroppedImage(0, 0); } /** * Gets the cropped image based on the current crop window.<br> * If image loaded from URI will use sample size to fir the requested width and height. * * @return a new Bitmap representing the cropped image */ public Bitmap getCroppedImage(int reqWidth, int reqHeight) { if (mBitmap != null) { if (mLoadedImageUri != null && mLoadedSampleSize > 1) { Rect rect = getActualCropRectNoRotation(); reqWidth = reqWidth > 0 ? reqWidth : rect.width(); reqHeight = reqHeight > 0 ? reqHeight : rect.height(); ImageViewUtil.DecodeBitmapResult result = ImageViewUtil.decodeSampledBitmapRegion(getContext(), mLoadedImageUri, rect, reqWidth, reqHeight); Bitmap bitmap = result.bitmap; if (mDegreesRotated > 0) { bitmap = ImageViewUtil.rotateBitmap(bitmap, mDegreesRotated); } return bitmap; } else { Rect rect = getActualCropRect(); return Bitmap.createBitmap(mBitmap, rect.left, rect.top, rect.width(), rect.height()); } } else { return null; } } /** * Gets the cropped circle image based on the current crop selection. * * @return a new Circular Bitmap representing the cropped image */ public Bitmap getCroppedOvalImage() { if (mBitmap != null) { Bitmap cropped = getCroppedImage(); int width = cropped.getWidth(); int height = cropped.getHeight(); Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(output); final int color = 0xff424242; final Paint paint = new Paint(); paint.setAntiAlias(true); canvas.drawARGB(0, 0, 0, 0); paint.setColor(color); RectF rect = new RectF(0, 0, width, height); canvas.drawOval(rect, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); canvas.drawBitmap(cropped, 0, 0, paint); return output; } else { return null; } } //region: Private methods @Override public Parcelable onSaveInstanceState() { final Bundle bundle = new Bundle(); bundle.putParcelable("instanceState", super.onSaveInstanceState()); bundle.putInt(DEGREES_ROTATED, mDegreesRotated); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { final Bundle bundle = (Bundle) state; if (mBitmap != null) { // Fixes the rotation of the image when orientation changes. mDegreesRotated = bundle.getInt(DEGREES_ROTATED); int tempDegrees = mDegreesRotated; rotateImage(mDegreesRotated); mDegreesRotated = tempDegrees; } super.onRestoreInstanceState(bundle.getParcelable("instanceState")); } else { super.onRestoreInstanceState(state); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (mBitmap != null) { final Rect bitmapRect = ImageViewUtil.getBitmapRect(mBitmap, this, mScaleType); mCropOverlayView.setBitmapRect(bitmapRect); } else { mCropOverlayView.setBitmapRect(EMPTY_RECT); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (mBitmap != null) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); // Bypasses a baffling bug when used within a ScrollView, where // heightSize is set to 0. if (heightSize == 0) { heightSize = mBitmap.getHeight(); } int desiredWidth; int desiredHeight; double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY; double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY; // Checks if either width or height needs to be fixed if (widthSize < mBitmap.getWidth()) { viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth(); } if (heightSize < mBitmap.getHeight()) { viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight(); } // If either needs to be fixed, choose smallest ratio and calculate // from there if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) { if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) { desiredWidth = widthSize; desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio); } else { desiredHeight = heightSize; desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio); } } // Otherwise, the picture is within frame layout bounds. Desired // width is // simply picture size else { desiredWidth = mBitmap.getWidth(); desiredHeight = mBitmap.getHeight(); } int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth); int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight); mLayoutWidth = width; mLayoutHeight = height; final Rect bitmapRect = ImageViewUtil.getBitmapRect(mBitmap.getWidth(), mBitmap.getHeight(), mLayoutWidth, mLayoutHeight, mScaleType); mCropOverlayView.setBitmapRect(bitmapRect); // MUST CALL THIS setMeasuredDimension(mLayoutWidth, mLayoutHeight); } else { mCropOverlayView.setBitmapRect(EMPTY_RECT); setMeasuredDimension(widthSize, heightSize); } } protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if (mLayoutWidth > 0 && mLayoutHeight > 0) { // Gets original parameters, and creates the new parameters final ViewGroup.LayoutParams origparams = this.getLayoutParams(); origparams.width = mLayoutWidth; origparams.height = mLayoutHeight; setLayoutParams(origparams); } } private void init(Context context) { final LayoutInflater inflater = LayoutInflater.from(context); final View v = inflater.inflate(R.layout.crop_image_view, this, true); mImageView = (ImageView) v.findViewById(R.id.ImageView_image); mImageView.setScaleType(mScaleType); setImageResource(mImageResource); mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView); mCropOverlayView.setInitialAttributeValues(mGuidelines, mFixAspectRatio, mAspectRatioX, mAspectRatioY); mCropOverlayView.setCropShape(mCropShape); } /** * Determines the specs for the onMeasure function. Calculates the width or height * depending on the mode. * * @param measureSpecMode The mode of the measured width or height. * @param measureSpecSize The size of the measured width or height. * @param desiredSize The desired size of the measured width or height. * @return The final size of the width or height. */ private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) { // Measure Width int spec; if (measureSpecMode == MeasureSpec.EXACTLY) { // Must be this size spec = measureSpecSize; } else if (measureSpecMode == MeasureSpec.AT_MOST) { // Can't be bigger than...; match_parent value spec = Math.min(desiredSize, measureSpecSize); } else { // Be whatever you want; wrap_content spec = desiredSize; } return spec; } //endregion //region: Inner class: CropShape /** * The possible cropping area shape */ public static enum CropShape { RECTANGLE, OVAL } //endregion }