************widgets*************
1)BitmapCroppingWorkerTask.java
===============================
1)BitmapCroppingWorkerTask.java
===============================
// "Therefore those skilled at the unorthodox// are infinite as heaven and earth,// inexhaustible as the great rivers.// When they come to an end,// they begin again,// like the days and months;// they die and are reborn,// like the four seasons."//// - Sun Tsu,// "The Art of War" package com.eppico.widgets; import android.content.Context;import android.graphics.Bitmap;import android.graphics.Rect;import android.net.Uri;import android.os.AsyncTask; import com.eppico.cropwindow.ImageViewUtil; import java.lang.ref.WeakReference; /** * Task to crop bitmap asynchronously from the UI thread. */class BitmapCroppingWorkerTask extends AsyncTask<Void, Void, BitmapCroppingWorkerTask.Result> { //region: Fields and Consts /** * Use a WeakReference to ensure the ImageView can be garbage collected */ private final WeakReference<CropImageView> mCropImageViewReference; /** * the bitmap to crop */ private final Bitmap mBitmap; /** * The Android URI of the image to load */ private final Uri mUri; /** * The context of the crop image view widget used for loading of bitmap by Android URI */ private final Context mContext; /** * Required cropping rectangle */ private final Rect mRect; /** * The shape to crop the image */ private final CropImageView.CropShape mCropShape; /** * Degrees the image was rotated after loading */ private final int mDegreesRotated; /** * required width of the cropping image */ private final int mReqWidth; /** * required height of the cropping image */ private final int mReqHeight; //endregion public BitmapCroppingWorkerTask(CropImageView cropImageView, Bitmap bitmap, Rect rect, CropImageView.CropShape cropShape) { mCropImageViewReference = new WeakReference<>(cropImageView); mContext = cropImageView.getContext(); mBitmap = bitmap; mRect = rect; mCropShape = cropShape; mUri = null; mDegreesRotated = 0; mReqWidth = 0; mReqHeight = 0; } public BitmapCroppingWorkerTask(CropImageView cropImageView, Uri uri, Rect rect, CropImageView.CropShape cropShape, int degreesRotated, int reqWidth, int reqHeight) { mCropImageViewReference = new WeakReference<>(cropImageView); mContext = cropImageView.getContext(); mUri = uri; mRect = rect; mCropShape = cropShape; mDegreesRotated = degreesRotated; mReqWidth = reqWidth; mReqHeight = reqHeight; mBitmap = null; } /** * The Android URI that this task is currently loading. */ public Uri getUri() { return mUri; } /** * Crop image in background. * * @param params ignored * @return the decoded bitmap data */ @Override protected Result doInBackground(Void... params) { try { if (!isCancelled()) { Bitmap bitmap = null; if (mUri != null) { bitmap = ImageViewUtil.cropBitmap( mContext, mUri, mRect, mDegreesRotated, mReqWidth, mReqHeight); } else if (mBitmap != null) { bitmap = ImageViewUtil.cropBitmap(mBitmap, mRect); } if (bitmap != null && mCropShape == CropImageView.CropShape.OVAL) { bitmap = ImageViewUtil.toOvalBitmap(bitmap); } return new Result(bitmap); } return null; } catch (Exception e) { return new Result(e); } } /** * Once complete, see if ImageView is still around and set bitmap. * * @param result the result of bitmap cropping */ @Override protected void onPostExecute(Result result) { if (result != null) { boolean completeCalled = false; if (!isCancelled()) { CropImageView cropImageView = mCropImageViewReference.get(); if (cropImageView != null) { completeCalled = true; cropImageView.onGetImageCroppingAsyncComplete(result); } } if (!completeCalled && result.bitmap != null) { // fast release of unused bitmap result.bitmap.recycle(); } } } //region: Inner class: Result /** * The result of BitmapCroppingWorkerTask async loading. */ public static final class Result { /** * The cropped bitmap */ public final Bitmap bitmap; /** * The error that occurred during async bitmap cropping. */ public final Exception error; Result(Bitmap bitmap) { this.bitmap = bitmap; this.error = null; } Result(Exception error) { this.bitmap = null; this.error = error; } } //endregion}
2)BitmapLoadingWorkerTask.java
==============================
// "Therefore those skilled at the unorthodox// are infinite as heaven and earth,// inexhaustible as the great rivers.// When they come to an end,// they begin again,// like the days and months;// they die and are reborn,// like the four seasons."//// - Sun Tsu,// "The Art of War" package com.eppico.widgets; import android.content.Context;import android.graphics.Bitmap;import android.net.Uri;import android.os.AsyncTask;import android.util.DisplayMetrics; import com.eppico.cropwindow.ImageViewUtil; import java.lang.ref.WeakReference; /** * Task to load bitmap asynchronously from the UI thread. */class BitmapLoadingWorkerTask extends AsyncTask<Void, Void, BitmapLoadingWorkerTask.Result> { //region: Fields and Consts /** * Use a WeakReference to ensure the ImageView can be garbage collected */ private final WeakReference<CropImageView> mCropImageViewReference; /** * The Android URI of the image to load */ private final Uri mUri; /** * Optional: if given use this rotation and not by exif */ private final Integer mPreSetRotation; /** * The context of the crop image view widget used for loading of bitmap by Android URI */ private final Context mContext; /** * required width of the cropping image after density adjustment */ private final int mWidth; /** * required height of the cropping image after density adjustment */ private final int mHeight; //endregion public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri, Integer preSetRotation) { mUri = uri; mPreSetRotation = preSetRotation; mCropImageViewReference = new WeakReference<>(cropImageView); mContext = cropImageView.getContext(); DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics(); double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1; mWidth = (int) (metrics.widthPixels * densityAdj); mHeight = (int) (metrics.heightPixels * densityAdj); } /** * The Android URI that this task is currently loading. */ public Uri getUri() { return mUri; } /** * Decode image in background. * * @param params ignored * @return the decoded bitmap data */ @Override protected Result doInBackground(Void... params) { try { if (!isCancelled()) { ImageViewUtil.DecodeBitmapResult decodeResult = ImageViewUtil.decodeSampledBitmap(mContext, mUri, mWidth, mHeight); if (!isCancelled()) { ImageViewUtil.RotateBitmapResult rotateResult = mPreSetRotation != null ? ImageViewUtil.rotateBitmapResult(decodeResult.bitmap, mPreSetRotation) : ImageViewUtil.rotateBitmapByExif(mContext, decodeResult.bitmap, mUri); return new Result(mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees); } } return null; } catch (Exception e) { return new Result(mUri, e); } } /** * Once complete, see if ImageView is still around and set bitmap. * * @param result the result of bitmap loading */ @Override protected void onPostExecute(Result result) { if (result != null) { boolean completeCalled = false; if (!isCancelled()) { CropImageView cropImageView = mCropImageViewReference.get(); if (cropImageView != null) { completeCalled = true; cropImageView.onSetImageUriAsyncComplete(result); } } if (!completeCalled && result.bitmap != null) { // fast release of unused bitmap result.bitmap.recycle(); } } } //region: Inner class: Result /** * The result of BitmapLoadingWorkerTask async loading. */ public static final class Result { /** * The Android URI of the image to load */ public final Uri uri; /** * The loaded bitmap */ public final Bitmap bitmap; /** * The sample size used to load the given bitmap */ public final int loadSampleSize; /** * The degrees the image was rotated */ public final int degreesRotated; /** * The error that occurred during async bitmap loading. */ public final Exception error; Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) { this.uri = uri; this.bitmap = bitmap; this.loadSampleSize = loadSampleSize; this.degreesRotated = degreesRotated; this.error = null; } Result(Uri uri, Exception error) { this.uri = uri; this.bitmap = null; this.loadSampleSize = 0; this.degreesRotated = 0; this.error = error; } } //endregion}
3)CropImageView.java
=====================
/* * 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.eppico.widgets; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.graphics.Matrix;import android.graphics.Rect;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 android.widget.ProgressBar;import com.eppico.R;import com.eppico.cropwindow.ImageViewUtil;import com.eppico.cropwindow.edge.Edge; import java.lang.ref.WeakReference; /** * Custom view that provides cropping capabilities to an image. */public class CropImageView extends FrameLayout { //region: Fields and Consts /** * Image view widget used to show the image for cropping. */ private final ImageView mImageView; /** * Overlay over the image view to show cropping UI. */ private final CropOverlayView mCropOverlayView; /** * Progress bar widget to show progress bar on async image loading and cropping. */ private final ProgressBar mProgressBar; private Bitmap mBitmap; private int mDegreesRotated = 0; private int mLayoutWidth; private int mLayoutHeight; private int mImageResource = 0; /** * if to show progress bar when image async loading/cropping is in progress.<br> * default: true, disable to provide custom progress bar UI. */ private boolean mShowProgressBar = true; /** * callback to be invoked when image async loading is complete */ private WeakReference<OnSetImageUriCompleteListener> mOnSetImageUriCompleteListener; /** * callback to be invoked when image async cropping is complete */ private WeakReference<OnGetCroppedImageCompleteListener> mOnGetCroppedImageCompleteListener; /** * 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; /** * Task used to load bitmap async from UI thread */ private WeakReference<BitmapLoadingWorkerTask> mBitmapLoadingWorkerTask; /** * Task used to crop bitmap async from UI thread */ private WeakReference<BitmapCroppingWorkerTask> mBitmapCroppingWorkerTask; //endregion public CropImageView(Context context) { this(context, null); } public CropImageView(Context context, AttributeSet attrs) { super(context, attrs); int guidelines = Defaults.DEFAULT_GUIDELINES; boolean fixAspectRatio = Defaults.DEFAULT_FIXED_ASPECT_RATIO; int aspectRatioX = Defaults.DEFAULT_ASPECT_RATIO_X; int aspectRatioY = Defaults.DEFAULT_ASPECT_RATIO_Y; ImageView.ScaleType scaleType = Defaults.VALID_SCALE_TYPES[Defaults.DEFAULT_SCALE_TYPE_INDEX]; CropShape cropShape = CropShape.RECTANGLE; float snapRadius = Defaults.SNAP_RADIUS_DP; if (attrs != null) { TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0); try { guidelines = ta.getInteger(R.styleable.CropImageView_guidelines, guidelines); fixAspectRatio = ta.getBoolean(R.styleable.CropImageView_fixAspectRatio, Defaults.DEFAULT_FIXED_ASPECT_RATIO); aspectRatioX = ta.getInteger(R.styleable.CropImageView_aspectRatioX, Defaults.DEFAULT_ASPECT_RATIO_X); aspectRatioY = ta.getInteger(R.styleable.CropImageView_aspectRatioY, Defaults.DEFAULT_ASPECT_RATIO_Y); scaleType = Defaults.VALID_SCALE_TYPES[ta.getInt(R.styleable.CropImageView_scaleType, Defaults.DEFAULT_SCALE_TYPE_INDEX)]; cropShape = Defaults.VALID_CROP_SHAPES[ta.getInt(R.styleable.CropImageView_cropShape, Defaults.DEFAULT_CROP_SHAPE_INDEX)]; snapRadius = ta.getFloat(R.styleable.CropImageView_snapRadius, snapRadius); mShowProgressBar = ta.getBoolean(R.styleable.CropImageView_showProgressBar, mShowProgressBar); } finally { ta.recycle(); } } LayoutInflater inflater = LayoutInflater.from(context); View v = inflater.inflate(R.layout.crop_image_view, this, true); mImageView = (ImageView) v.findViewById(R.id.ImageView_image); mImageView.setScaleType(scaleType); mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView); mCropOverlayView.setInitialAttributeValues(guidelines, fixAspectRatio, aspectRatioX, aspectRatioY); mCropOverlayView.setCropShape(cropShape); mCropOverlayView.setSnapRadius(snapRadius); mCropOverlayView.setVisibility(mBitmap != null ? VISIBLE : INVISIBLE); mProgressBar = (ProgressBar) v.findViewById(R.id.CropProgressBar); setProgressBarVisibility(); } /** * Set the scale type of the image in the crop view */ public ImageView.ScaleType getScaleType() { return mImageView.getScaleType(); } /** * Set the scale type of the image in the crop view */ public void setScaleType(ImageView.ScaleType scaleType) { mImageView.setScaleType(scaleType); } /** * The shape of the cropping area - rectangle/circular. */ public CropShape getCropShape() { return mCropOverlayView.getCropShape(); } /** * The shape of the cropping area - rectangle/circular. */ public void setCropShape(CropShape 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) { mCropOverlayView.setAspectRatioX(aspectRatioX); mCropOverlayView.setAspectRatioY(aspectRatioY); } /** * 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. (default: 3) */ public void setSnapRadius(float snapRadius) { if (snapRadius >= 0) { mCropOverlayView.setSnapRadius(snapRadius); } } /** * if to show progress bar when image async loading/cropping is in progress.<br> * default: true, disable to provide custom progress bar UI. */ public boolean isShowProgressBar() { return mShowProgressBar; } /** * if to show progress bar when image async loading/cropping is in progress.<br> * default: true, disable to provide custom progress bar UI. */ public void setShowProgressBar(boolean showProgressBar) { mShowProgressBar = showProgressBar; setProgressBarVisibility(); } /** * Returns the integer of the imageResource */ public int getImageResource() { return mImageResource; } /** * Get the URI of an image that was set by URI, null otherwise. */ public Uri getImageUri() { return mLoadedImageUri; } /** * 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, mImageView.getScaleType()); // 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; } } /** * 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 fit in the requested width and height down-sampling * if required - optimization to get best size to quality. * * @return a new Bitmap representing the cropped image */ public Bitmap getCroppedImage(int reqWidth, int reqHeight) { if (mBitmap != null) { if (mLoadedImageUri != null && mLoadedSampleSize > 1) { return ImageViewUtil.cropBitmap( getContext(), mLoadedImageUri, getActualCropRectNoRotation(), mDegreesRotated, reqWidth, reqHeight); } else { return ImageViewUtil.cropBitmap(mBitmap, getActualCropRect()); } } else { return null; } } /** * Gets the cropped circle image based on the current crop selection.<br> * Same as {@link #getCroppedImage()} (as the bitmap is rectangular by nature) but the pixels beyond the * oval crop will be transparent. * * @return a new Circular Bitmap representing the cropped image */ public Bitmap getCroppedOvalImage() { if (mBitmap != null) { Bitmap cropped = getCroppedImage(); return ImageViewUtil.toOvalBitmap(cropped); } else { return null; } } /** * Gets the cropped image based on the current crop window.<br> * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}. */ public void getCroppedImageAsync() { getCroppedImageAsync(CropShape.RECTANGLE, 0, 0); } /** * Gets the cropped image based on the current crop window.<br> * If image loaded from URI will use sample size to fit in the requested width and height down-sampling * if required - optimization to get best size to quality.<br> * The result will be invoked to listener set by {@link #setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener)}. * * @param cropShape the shape to crop the image */ public void getCroppedImageAsync(CropShape cropShape, int reqWidth, int reqHeight) { if (mOnGetCroppedImageCompleteListener == null) { throw new IllegalArgumentException("OnGetCroppedImageCompleteListener is not set"); } BitmapCroppingWorkerTask currentTask = mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null; if (currentTask != null) { // cancel previous cropping currentTask.cancel(true); } mBitmapCroppingWorkerTask = mLoadedImageUri != null && mLoadedSampleSize > 1 ? new WeakReference<>(new BitmapCroppingWorkerTask(this, mLoadedImageUri, getActualCropRectNoRotation(), cropShape, mDegreesRotated, reqWidth, reqHeight)) : new WeakReference<>(new BitmapCroppingWorkerTask(this, mBitmap, getActualCropRect(), cropShape)); mBitmapCroppingWorkerTask.get().execute(); setProgressBarVisibility(); } /** * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) * is complete (successful or failed). */ public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) { mOnSetImageUriCompleteListener = listener != null ? new WeakReference<>(listener) : null; } /** * Set the callback to be invoked when image async cropping ({@link #getCroppedImageAsync()}) * is complete (successful or failed). */ public void setOnGetCroppedImageCompleteListener(OnGetCroppedImageCompleteListener listener) { mOnGetCroppedImageCompleteListener = listener != null ? new WeakReference<>(listener) : null; } /** * Sets a Bitmap as the content of the CropImageView. * * @param bitmap the Bitmap to set */ public void setImageBitmap(Bitmap bitmap) { setBitmap(bitmap, true); } /** * 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; } setBitmap(bitmap, true); } /** * 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); setBitmap(bitmap, true); 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 * @deprecated Use {@link #setImageUriAsync(Uri)} for better async handling */ @Deprecated public void setImageUri(Uri uri) { if (uri != null) { DisplayMetrics metrics = getResources().getDisplayMetrics(); double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1; //mScreenWidth = getActivity().getWindowManager().getDefaultDisplay().getWidth(); DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); int width = displayMetrics.widthPixels; int height = width; // 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); setBitmap(rotateResult.bitmap, true); mLoadedImageUri = uri; mLoadedSampleSize = decodeResult.sampleSize; mDegreesRotated = rotateResult.degrees; } } /** * 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 setImageUriAsync(Uri uri) { setImageUriAsync(uri, null); } /** * Clear the current image set for cropping. */ public void clearImage() { clearImage(true); } /** * 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); setBitmap(bitmap, false); mDegreesRotated += degrees; mDegreesRotated = mDegreesRotated % 360; } } //region: Private methods /** * Load image from given URI async using {@link BitmapLoadingWorkerTask}<br> * optionally rotate the loaded image given degrees, used for restore state. */ private void setImageUriAsync(Uri uri, Integer preSetRotation) { if (uri != null) { BitmapLoadingWorkerTask currentTask = mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null; if (currentTask != null) { // cancel previous loading (no check if the same URI because camera URI can be the same for different images) currentTask.cancel(true); } // either no existing task is working or we canceled it, need to load new URI clearImage(true); mBitmapLoadingWorkerTask = new WeakReference<>(new BitmapLoadingWorkerTask(this, uri, preSetRotation)); mBitmapLoadingWorkerTask.get().execute(); setProgressBarVisibility(); } } /** * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result * to the widget if still relevant and call listener if set. * * @param result the result of bitmap loading */ void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) { mBitmapLoadingWorkerTask = null; setProgressBarVisibility(); if (result.error == null) { setBitmap(result.bitmap, true); mLoadedImageUri = result.uri; mLoadedSampleSize = result.loadSampleSize; mDegreesRotated = result.degreesRotated; } OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener != null ? mOnSetImageUriCompleteListener.get() : null; if (listener != null) { listener.onSetImageUriComplete(this, result.uri, result.error); } } /** * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if set. * * @param result the result of bitmap cropping */ void onGetImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) { mBitmapCroppingWorkerTask = null; setProgressBarVisibility(); OnGetCroppedImageCompleteListener listener = mOnGetCroppedImageCompleteListener != null ? mOnGetCroppedImageCompleteListener.get() : null; if (listener != null) { listener.onGetCroppedImageComplete(this, result.bitmap, result.error); } } /** * Set the given bitmap to be used in for cropping<br> * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been manipulated. */ private void setBitmap(Bitmap bitmap, boolean clearFull) { if (mBitmap != bitmap) { clearImage(clearFull); mBitmap = bitmap; mImageView.setImageBitmap(mBitmap); if (mCropOverlayView != null) { mCropOverlayView.resetCropOverlayView(); mCropOverlayView.setVisibility(VISIBLE); } } } /** * Clear the current image set for cropping.<br> * Full clear will also clear the data of the set image like Uri or Resource id while partial clear * will only clear the bitmap and recycle if required. */ private void clearImage(boolean full) { // if we allocated the bitmap, release it as fast as possible if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) { mBitmap.recycle(); mBitmap = null; } if (full) { // clean the loaded image flags for new image mImageResource = 0; mLoadedImageUri = null; mLoadedSampleSize = 1; mDegreesRotated = 0; mImageView.setImageBitmap(null); if (mCropOverlayView != null) { mCropOverlayView.setVisibility(INVISIBLE); } } } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("instanceState", super.onSaveInstanceState()); bundle.putParcelable("LOADED_IMAGE_URI", mLoadedImageUri); bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource); if (mLoadedImageUri == null && mImageResource < 1) { bundle.putParcelable("SET_BITMAP", mBitmap); } if (mBitmapLoadingWorkerTask != null) { BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get(); if (task != null) { bundle.putParcelable("LOADING_IMAGE_URI", task.getUri()); } } bundle.putInt("DEGREES_ROTATED", mDegreesRotated); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; Bitmap bitmap = null; Uri uri = bundle.getParcelable("LOADED_IMAGE_URI"); if (uri != null) { setImageUriAsync(uri, bundle.getInt("DEGREES_ROTATED")); } else { int resId = bundle.getInt("LOADED_IMAGE_RESOURCE"); if (resId > 0) { setImageResource(resId); } else { bitmap = bundle.getParcelable("SET_BITMAP"); if (bitmap != null) { setBitmap(bitmap, true); } else { uri = bundle.getParcelable("LOADING_IMAGE_URI"); if (uri != null) { setImageUriAsync(uri); } } } } mDegreesRotated = bundle.getInt("DEGREES_ROTATED"); if (mBitmap != null && bitmap == null) { // Fixes the rotation of the image when we reloaded it. int tmpRotated = mDegreesRotated; rotateImage(mDegreesRotated); mDegreesRotated = tmpRotated; } super.onRestoreInstanceState(bundle.getParcelable("instanceState")); } else { super.onRestoreInstanceState(state); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { if (mBitmap != null) { Rect bitmapRect = ImageViewUtil.getBitmapRect(mBitmap, this, mImageView.getScaleType()); mCropOverlayView.setBitmapRect(bitmapRect); } else { mCropOverlayView.setBitmapRect(Defaults.EMPTY_RECT); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); if (mBitmap != null) { // 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); } } else { // Otherwise, the picture is within frame layout bounds. Desired width is simply picture size desiredWidth = mBitmap.getWidth(); desiredHeight = mBitmap.getHeight(); } int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth); int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight); mLayoutWidth = width; mLayoutHeight = height; Rect bitmapRect = ImageViewUtil.getBitmapRect(mBitmap.getWidth(), mBitmap.getHeight(), mLayoutWidth, mLayoutHeight, mImageView.getScaleType()); mCropOverlayView.setBitmapRect(bitmapRect); // MUST CALL THIS setMeasuredDimension(mLayoutWidth, mLayoutHeight); } else { mCropOverlayView.setBitmapRect(Defaults.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 ViewGroup.LayoutParams origParams = this.getLayoutParams(); origParams.width = mLayoutWidth; origParams.height = mLayoutHeight; setLayoutParams(origParams); } } /** * 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; } /** * Set visibility of progress bar when async loading/cropping is in process and show is enabled. */ private void setProgressBarVisibility() { boolean visible = mShowProgressBar && ( (mBitmap == null && mBitmapLoadingWorkerTask != null) || (mBitmapCroppingWorkerTask != null)); mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE); } //endregion //region: Inner class: CropShape /** * The possible cropping area shape. */ public enum CropShape { RECTANGLE, OVAL } //endregion //region: Inner class: OnSetImageUriCompleteListener /** * Interface definition for a callback to be invoked when image async loading is complete. */ public interface OnSetImageUriCompleteListener { /** * Called when a crop image view has completed loading image for cropping.<br> * If loading failed error parameter will contain the error. * * @param view The crop image view that loading of image was complete. * @param uri the URI of the image that was loading * @param error if error occurred during loading will contain the error, otherwise null. */ void onSetImageUriComplete(CropImageView view, Uri uri, Exception error); } //endregion //region: Inner class: OnGetCroppedImageCompleteListener /** * Interface definition for a callback to be invoked when image async cropping is complete. */ public interface OnGetCroppedImageCompleteListener { /** * Called when a crop image view has completed loading image for cropping.<br> * If loading failed error parameter will contain the error. * * @param view The crop image view that cropping of image was complete. * @param bitmap the cropped image bitmap (null if failed) * @param error if error occurred during cropping will contain the error, otherwise null. */ void onGetCroppedImageComplete(CropImageView view, Bitmap bitmap, Exception error); } //endregion}4)CropOverlayView.java======================/* * 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.eppico.widgets; import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Path;import android.graphics.Rect;import android.graphics.Region;import android.os.Build;import android.util.AttributeSet;import android.util.DisplayMetrics;import android.util.Pair;import android.util.TypedValue;import android.view.MotionEvent;import android.view.View;import com.eppico.cropwindow.AspectRatioUtil;import com.eppico.cropwindow.PaintUtil;import com.eppico.cropwindow.edge.Edge;import com.eppico.cropwindow.handle.Handle;import com.eppico.cropwindow.HandleUtil; /** * A custom View representing the crop window and the shaded background outside the crop window. */public class CropOverlayView extends View { //region: Fields and Consts /** * 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 = Defaults.DEFAULT_FIXED_ASPECT_RATIO; // Floats to save the current aspect ratio of the image private int mAspectRatioX = Defaults.DEFAULT_ASPECT_RATIO_X; private int mAspectRatioY = Defaults.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; /** * The shape of the cropping area - rectangle/circular. */ private CropImageView.CropShape mCropShape; // 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; /** * Used to set back LayerType after changing to software. */ private Integer mOriginalLayerType; //endregion public CropOverlayView(Context context) { super(context); init(context); } public CropOverlayView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } /** * 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. */ public void resetCropOverlayView() { if (initializedCropWindow) { initCropWindow(mBitmapRect); invalidate(); } } /** * 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 (mCropShape != cropShape) { mCropShape = cropShape; if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 17) { if (mCropShape == CropImageView.CropShape.OVAL) { mOriginalLayerType = getLayerType(); if (mOriginalLayerType != View.LAYER_TYPE_SOFTWARE) { // TURN off hardware acceleration setLayerType(View.LAYER_TYPE_SOFTWARE, null); } else { mOriginalLayerType = null; } } else if (mOriginalLayerType != null) { // return hardware acceleration back setLayerType(mOriginalLayerType, null); mOriginalLayerType = null; } } 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(); } } } /** * 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. (default: 3) */ public void setSnapRadius(float snapRadius) { mSnapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, snapRadius, getResources().getDisplayMetrics()); } /** * 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; } } //region: Private 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 == Defaults.GUIDELINES_ON) { drawRuleOfThirdsGuidelines(canvas); } else if (mGuidelines == Defaults.GUIDELINES_ON_TOUCH) { // Draw only when resizing if (mPressedHandle != null) drawRuleOfThirdsGuidelines(canvas); } } float w = mBorderPaint.getStrokeWidth(); float l = Edge.LEFT.getCoordinate() + w / 2; float t = Edge.TOP.getCoordinate() + w / 2; float r = Edge.RIGHT.getCoordinate() - w / 2; float b = Edge.BOTTOM.getCoordinate() - w / 2; if (mCropShape == CropImageView.CropShape.RECTANGLE) { // Draw rectangle crop window border. canvas.drawRect(l, t, r, b, mBorderPaint); drawCorners(canvas); } else { // Draw circular crop window border Defaults.EMPTY_RECT_F.set(l, t, r, b); canvas.drawOval(Defaults.EMPTY_RECT_F, mBorderPaint); } } @Override public boolean onTouchEvent(@SuppressWarnings("NullableProblems") 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; } } private void init(Context context) { DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); mHandleRadius = HandleUtil.getTargetRadius(context); mSnapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PT, Defaults.SNAP_RADIUS_DP, displayMetrics); mBorderPaint = PaintUtil.newBorderPaint(context); mGuidelinePaint = PaintUtil.newGuidelinePaint(); mBackgroundPaint = PaintUtil.newBackgroundPaint(); mCornerPaint = PaintUtil.newCornerPaint(context); // Sets the values for the corner sizes mCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, Defaults.DEFAULT_CORNER_OFFSET_DP, displayMetrics); mCornerExtension = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, Defaults.DEFAULT_CORNER_EXTENSION_DP, displayMetrics); mCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, Defaults.DEFAULT_CORNER_LENGTH_DP, displayMetrics); // Sets guidelines to default until specified otherwise mGuidelines = Defaults.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) { if (bitmapRect.width() == 0 || bitmapRect.height() == 0) { return; } // Tells the attribute functions the crop window has already been // initialized if (!initializedCropWindow) { initializedCropWindow = true; } if (mFixAspectRatio && (bitmapRect.left != 0 || bitmapRect.right != 0 || bitmapRect.top != 0 || bitmapRect.bottom != 0)) { // 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); float centerX = getWidth() / 2f; //dirty fix for wrong crop overlay aspect ratio when using fixed aspect ratio mTargetAspectRatio = (float) mAspectRatioX / mAspectRatioY; // Limits the aspect ratio to no less than 40 wide or 40 tall 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()); } 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); float centerY = getHeight() / 2f; // Limits the aspect ratio to no less than 40 wide or 40 tall 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; } 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. float horizontalPadding = 0.1f * bitmapRect.width(); 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()) < Defaults.DEFAULT_SHOW_GUIDELINES_LIMIT) || (Math.abs(Edge.TOP.getCoordinate() - Edge.BOTTOM.getCoordinate()) < Defaults.DEFAULT_SHOW_GUIDELINES_LIMIT)) { return false; } else { return true; } } private void drawRuleOfThirdsGuidelines(Canvas canvas) { float sw = mBorderPaint.getStrokeWidth(); float l = Edge.LEFT.getCoordinate() + sw; float t = Edge.TOP.getCoordinate() + sw; float r = Edge.RIGHT.getCoordinate() - sw; float b = Edge.BOTTOM.getCoordinate() - sw; float oneThirdCropWidth = Edge.getWidth() / 3; float oneThirdCropHeight = Edge.getHeight() / 3; if (mCropShape == CropImageView.CropShape.OVAL) { float w = Edge.getWidth() / 2 - sw; float h = Edge.getHeight() / 2 - sw; // Draw vertical guidelines. float x1 = l + oneThirdCropWidth; float x2 = r - oneThirdCropWidth; float yv = (float) (h * Math.sin(Math.acos((w - oneThirdCropWidth) / w))); canvas.drawLine(x1, t + h - yv, x1, b - h + yv, mGuidelinePaint); canvas.drawLine(x2, t + h - yv, x2, b - h + yv, mGuidelinePaint); // Draw horizontal guidelines. float y1 = t + oneThirdCropHeight; float y2 = b - oneThirdCropHeight; float xv = (float) (w * Math.cos(Math.asin((h - oneThirdCropHeight) / h))); canvas.drawLine(l + w - xv, y1, r - w + xv, y1, mGuidelinePaint); canvas.drawLine(l + w - xv, y2, r - w + xv, y2, mGuidelinePaint); } else { // Draw vertical guidelines. float x1 = l + oneThirdCropWidth; float x2 = r - oneThirdCropWidth; canvas.drawLine(x1, t, x1, b, mGuidelinePaint); canvas.drawLine(x2, t, x2, b, mGuidelinePaint); // Draw horizontal guidelines. float y1 = t + oneThirdCropHeight; float y2 = b - oneThirdCropHeight; canvas.drawLine(l, y1, r, y1, mGuidelinePaint); canvas.drawLine(l, y2, r, y2, mGuidelinePaint); } } private void drawBackground(Canvas canvas, Rect bitmapRect) { float l = Edge.LEFT.getCoordinate(); float t = Edge.TOP.getCoordinate(); float r = Edge.RIGHT.getCoordinate(); float b = Edge.BOTTOM.getCoordinate(); if (mCropShape == CropImageView.CropShape.RECTANGLE) { canvas.drawRect(bitmapRect.left, bitmapRect.top, bitmapRect.right, t, mBackgroundPaint); canvas.drawRect(bitmapRect.left, b, bitmapRect.right, bitmapRect.bottom, mBackgroundPaint); canvas.drawRect(bitmapRect.left, t, l, b, mBackgroundPaint); canvas.drawRect(r, t, bitmapRect.right, b, mBackgroundPaint); } else { Path circleSelectionPath = new Path(); if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 17 && mCropShape == CropImageView.CropShape.OVAL) { Defaults.EMPTY_RECT_F.set(l + 2, t + 2, r - 2, b - 2); } else { Defaults.EMPTY_RECT_F.set(l, t, r, b); } circleSelectionPath.addOval(Defaults.EMPTY_RECT_F, Path.Direction.CW); canvas.save(); canvas.clipPath(circleSelectionPath, Region.Op.XOR); canvas.drawRect(bitmapRect.left, bitmapRect.top, bitmapRect.right, bitmapRect.bottom, mBackgroundPaint); canvas.restore(); } } private void drawCorners(Canvas canvas) { float w = mBorderPaint.getStrokeWidth() * 1.5f + 1; float l = Edge.LEFT.getCoordinate() + w; float t = Edge.TOP.getCoordinate() + w; float r = Edge.RIGHT.getCoordinate() - w; float b = Edge.BOTTOM.getCoordinate() - w; // Top left canvas.drawLine(l - mCornerOffset, t - mCornerExtension, l - mCornerOffset, t + mCornerLength, mCornerPaint); canvas.drawLine(l, t - mCornerOffset, l + mCornerLength, t - mCornerOffset, mCornerPaint); // Top right canvas.drawLine(r + mCornerOffset, t - mCornerExtension, r + mCornerOffset, t + mCornerLength, mCornerPaint); canvas.drawLine(r, t - mCornerOffset, r - mCornerLength, t - mCornerOffset, mCornerPaint); // Bottom left canvas.drawLine(l - mCornerOffset, b + mCornerExtension, l - mCornerOffset, b - mCornerLength, mCornerPaint); canvas.drawLine(l, b + mCornerOffset, l + mCornerLength, b + mCornerOffset, mCornerPaint); // Bottom left canvas.drawLine(r + mCornerOffset, b + mCornerExtension, r + mCornerOffset, b - mCornerLength, mCornerPaint); canvas.drawLine(r, b + mCornerOffset, r - mCornerLength, b + 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) { float left = Edge.LEFT.getCoordinate(); float top = Edge.TOP.getCoordinate(); float right = Edge.RIGHT.getCoordinate(); float bottom = Edge.BOTTOM.getCoordinate(); mPressedHandle = HandleUtil.getPressedHandle(x, y, left, top, right, bottom, mHandleRadius, mCropShape); 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(); } //endregion}5)CustomFont.java===================package com.eppico.widgets; import android.content.Context;import android.graphics.Typeface;import android.util.AttributeSet;import android.widget.TextView; public class CustomFont extends TextView { private Context c; public CustomFont(Context c) { super(c); this.c = c; Typeface tfs = Typeface.createFromAsset(c.getAssets(), "Roboto-Regular_3.ttf"); setTypeface(tfs); } public CustomFont(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.c = context; Typeface tfs = Typeface.createFromAsset(c.getAssets(), "Roboto-Regular_3.ttf"); setTypeface(tfs); } public CustomFont(Context context, AttributeSet attrs) { super(context, attrs); this.c = context; Typeface tfs = Typeface.createFromAsset(c.getAssets(), "Roboto-Regular_3.ttf"); setTypeface(tfs); } }6)Defaults.java===================// "Therefore those skilled at the unorthodox// are infinite as heaven and earth,// inexhaustible as the great rivers.// When they come to an end,// they begin again,// like the days and months;// they die and are reborn,// like the four seasons."//// - Sun Tsu,// "The Art of War" package com.eppico.widgets; import android.graphics.Rect;import android.graphics.RectF;import android.widget.ImageView; import com.eppico.cropwindow.PaintUtil; /** * Defaults used in the library. */class Defaults { public static final Rect EMPTY_RECT = new Rect(); public static final RectF EMPTY_RECT_F = new RectF(); // 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; public static final float SNAP_RADIUS_DP = 3; public 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 public static final float DEFAULT_CORNER_THICKNESS_DP = PaintUtil.getCornerThickness(); public static final float DEFAULT_LINE_THICKNESS_DP = PaintUtil.getLineThickness(); public static final float DEFAULT_CORNER_OFFSET_DP = (DEFAULT_CORNER_THICKNESS_DP / 2) - (DEFAULT_LINE_THICKNESS_DP / 2); public static final float DEFAULT_CORNER_EXTENSION_DP = DEFAULT_CORNER_THICKNESS_DP / 2 + DEFAULT_CORNER_OFFSET_DP; public static final float DEFAULT_CORNER_LENGTH_DP = 15; public static final int GUIDELINES_ON_TOUCH = 1; public static final int GUIDELINES_ON = 2; public static final ImageView.ScaleType[] VALID_SCALE_TYPES = new ImageView.ScaleType[]{ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_CENTER}; public static final CropImageView.CropShape[] VALID_CROP_SHAPES = new CropImageView.CropShape[]{CropImageView.CropShape.RECTANGLE, CropImageView.CropShape.OVAL};}7)EasyDialog.java====================package com.eppico.widgets; import android.animation.Animator;import android.animation.AnimatorSet;import android.animation.ObjectAnimator;import android.annotation.SuppressLint;import android.app.Activity;import android.app.Dialog;import android.content.Context;import android.content.DialogInterface;import android.graphics.Color;import android.graphics.drawable.GradientDrawable;import android.graphics.drawable.LayerDrawable;import android.graphics.drawable.RotateDrawable;import android.os.Build;import android.util.DisplayMetrics;import android.view.LayoutInflater;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.ViewTreeObserver;import android.widget.ImageView;import android.widget.LinearLayout;import android.widget.RelativeLayout;import android.widget.Toast; import com.eppico.R; import java.util.ArrayList;import java.util.List; /** * Created by michael on 15/4/15. */public class EasyDialog { private Context context; /** * 内容在三角形上面 */ public static final int GRAVITY_TOP = 0; /** * 内容在三角形下面 */ public static final int GRAVITY_BOTTOM = 1; /** * 内容在三角形左面 */ public static final int GRAVITY_LEFT = 2; /** * 内容在三角形右面 */ public static final int GRAVITY_RIGHT = 3; /** * 对话框本身 */ private Dialog dialog; /** * 坐标 */ private int[] location; /** * 提醒框位置 */ private int gravity; /** * 外面传递进来的View */ private View contentView; /** * 三角形 */ private ImageView ivTriangle; /** * 用来放外面传递进来的View */ private LinearLayout llContent; /** * 触摸外面,是否关闭对话框 */ private boolean touchOutsideDismiss; /** * 提示框所在的容器 */ private RelativeLayout rlOutsideBackground; public EasyDialog(Context context) { initDialog(context); } private void initDialog(final Context context) { this.context = context; LayoutInflater layoutInflater = ((Activity) context).getLayoutInflater(); View dialogView = layoutInflater.inflate(R.layout.layout_dialog, null); ViewTreeObserver viewTreeObserver = dialogView.getViewTreeObserver(); viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { //当View可以获取宽高的时候,设置view的位置 relocation(location); } }); rlOutsideBackground = (RelativeLayout) dialogView.findViewById(R.id.rlOutsideBackground); setTouchOutsideDismiss(true); ivTriangle = (ImageView) dialogView.findViewById(R.id.ivTriangle); llContent = (LinearLayout) dialogView.findViewById(R.id.llContent); dialog = new Dialog(context, isFullScreen() ? android.R.style.Theme_Translucent_NoTitleBar_Fullscreen : android.R.style.Theme_Translucent_NoTitleBar); dialog.setContentView(dialogView); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface dialog) { if (onEasyDialogDismissed != null) { onEasyDialogDismissed.onDismissed(); } } }); dialog.setOnShowListener(new DialogInterface.OnShowListener() { @Override public void onShow(DialogInterface dialog) { if (onEasyDialogShow != null) { onEasyDialogShow.onShow(); } } }); animatorSetForDialogShow = new AnimatorSet(); animatorSetForDialogDismiss = new AnimatorSet(); objectAnimatorsForDialogShow = new ArrayList<>(); objectAnimatorsForDialogDismiss = new ArrayList<>(); ini(); } final View.OnTouchListener outsideBackgroundListener = new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { if (touchOutsideDismiss && dialog != null) { onDialogDismiss(); } return false; } }; /** * The Dialog instance */ public Dialog getDialog() { return dialog; } /** * 初始化默认值 */ private void ini() { this.setLocation(new int[]{0, 0}) .setGravity(GRAVITY_BOTTOM) .setTouchOutsideDismiss(true) .setOutsideColor(Color.TRANSPARENT) .setBackgroundColor(Color.BLUE) .setMatchParent(true) .setMarginLeftAndRight(24, 24); } /** * 设置提示框中要显示的内容 */ public EasyDialog setLayout(View layout) { if (layout != null) { this.contentView = layout; } return this; } /** * 设置提示框中要显示的内容的布局Id */ public EasyDialog setLayoutResourceId(int layoutResourceId) { View view = ((Activity) context).getLayoutInflater().inflate(layoutResourceId, null); setLayout(view); return this; } /** * 设置三角形所在的位置 */ public EasyDialog setLocation(int[] location) { this.location = location; return this; } /** * 设置三角形所在的位置 * location.x坐标值为attachedView所在屏幕位置的中心 * location.y坐标值依据当前的gravity,如果gravity是top,则为控件上方的y值,如果是bottom,则为控件的下方的y值 * * @param attachedView 在哪个View显示提示信息 */ public EasyDialog setLocationByAttachedView(View attachedView) { if (attachedView != null) { this.attachedView = attachedView; int[] attachedViewLocation = new int[2]; attachedView.getLocationOnScreen(attachedViewLocation); switch (gravity) { case GRAVITY_BOTTOM: attachedViewLocation[0] += attachedView.getWidth() / 2; attachedViewLocation[1] += attachedView.getHeight(); break; case GRAVITY_TOP: attachedViewLocation[0] += attachedView.getWidth() / 2; break; case GRAVITY_LEFT: attachedViewLocation[1] += attachedView.getHeight() / 2; break; case GRAVITY_RIGHT: attachedViewLocation[0] += attachedView.getWidth(); attachedViewLocation[1] += attachedView.getHeight() / 2; } setLocation(attachedViewLocation); } return this; } /** * 对话框所依附的View */ private View attachedView = null; public View getAttachedView() { return this.attachedView; } /** * 设置显示的内容在上方还是下方,如果设置错误,默认是在下方 */ public EasyDialog setGravity(int gravity) { if (gravity != GRAVITY_BOTTOM && gravity != GRAVITY_TOP && gravity != GRAVITY_LEFT && gravity != GRAVITY_RIGHT) { gravity = GRAVITY_BOTTOM; } this.gravity = gravity; switch (this.gravity) { case GRAVITY_BOTTOM: ivTriangle.setBackgroundResource(R.drawable.triangle_bottom); break; case GRAVITY_TOP: ivTriangle.setBackgroundResource(R.drawable.triangle_top); break; case GRAVITY_LEFT: ivTriangle.setBackgroundResource(R.drawable.triangle_left); break; case GRAVITY_RIGHT: ivTriangle.setBackgroundResource(R.drawable.triangle_right); break; } llContent.setBackgroundResource(R.drawable.round_corner_bg); if (attachedView != null)//如果用户调用setGravity()之前就调用过setLocationByAttachedView,需要再调用一次setLocationByAttachedView { this.setLocationByAttachedView(attachedView); } this.setBackgroundColor(backgroundColor); return this; } /** * 设置是否填充屏幕,如果不填充就适应布局内容的宽度,显示内容的位置会尽量随着三角形的位置居中 */ public EasyDialog setMatchParent(boolean matchParent) { ViewGroup.LayoutParams layoutParams = llContent.getLayoutParams(); layoutParams.width = matchParent ? ViewGroup.LayoutParams.MATCH_PARENT : ViewGroup.LayoutParams.WRAP_CONTENT; llContent.setLayoutParams(layoutParams); return this; } /** * 距离屏幕左右的边距 */ public EasyDialog setMarginLeftAndRight(int left, int right) { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) llContent.getLayoutParams(); layoutParams.setMargins(left, 0, right, 0); llContent.setLayoutParams(layoutParams); return this; } public EasyDialog setMarginTopAndBottom(int top, int bottom) { RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) llContent.getLayoutParams(); layoutParams.setMargins(0, top, 0, bottom); llContent.setLayoutParams(layoutParams); return this; } /** * 设置触摸对话框外面,对话框是否消失 */ public EasyDialog setTouchOutsideDismiss(boolean touchOutsideDismiss) { this.touchOutsideDismiss = touchOutsideDismiss; if (touchOutsideDismiss) { rlOutsideBackground.setOnTouchListener(outsideBackgroundListener); } else { rlOutsideBackground.setOnTouchListener(null); } return this; } /** * 设置提醒框外部区域的颜色 */ public EasyDialog setOutsideColor(int color) { rlOutsideBackground.setBackgroundColor(color); return this; } private int backgroundColor; /** * 设置对话框的颜色 * 三角形的图片是layer-list里面嵌套一个RotateDrawable,在设置颜色的时候需要特别处理 * http://stackoverflow.com/questions/24492000/set-color-of-triangle-on-run-time * http://stackoverflow.com/questions/16636412/change-shape-solid-color-at-runtime-inside-drawable-xml-used-as-background */ public EasyDialog setBackgroundColor(int color) { backgroundColor = color; LayerDrawable drawableTriangle = (LayerDrawable) ivTriangle.getBackground(); GradientDrawable shapeTriangle = (GradientDrawable) (((RotateDrawable) drawableTriangle.findDrawableByLayerId(R.id.shape_id)).getDrawable()); if (shapeTriangle != null) { shapeTriangle.setColor(color); } else { Toast.makeText(context, "shape is null", Toast.LENGTH_SHORT).show(); } GradientDrawable drawableRound = (GradientDrawable) llContent.getBackground(); if (drawableRound != null) { drawableRound.setColor(color); } return this; } /** * 显示提示框 */ public EasyDialog show() { if (dialog != null) { if (contentView == null) { throw new RuntimeException("您是否未调用setLayout()或者setLayoutResourceId()方法来设置要显示的内容呢?"); } if (llContent.getChildCount() > 0) { llContent.removeAllViews(); } llContent.addView(contentView); dialog.show(); onDialogShowing(); } return this; } /** * 显示对话框的View的parent,如果想自己写动画,可以获取这个实例来写动画 */ public View getTipViewInstance() { return rlOutsideBackground.findViewById(R.id.rlParentForAnimate); } /** * 横向 */ public static final int DIRECTION_X = 0; /** * 纵向 */ public static final int DIRECTION_Y = 1; /** * 水平动画 * * @param direction 动画的方向 * @param duration 动画执行的时间长度 * @param values 动画移动的位置 */ public EasyDialog setAnimationTranslationShow(int direction, int duration, float... values) { return setAnimationTranslation(true, direction, duration, values); } /** * 水平动画 * * @param direction 动画的方向 * @param duration 动画执行的时间长度 * @param values 动画移动的位置 */ public EasyDialog setAnimationTranslationDismiss(int direction, int duration, float... values) { return setAnimationTranslation(false, direction, duration, values); } private EasyDialog setAnimationTranslation(boolean isShow, int direction, int duration, float... values) { if (direction != DIRECTION_X && direction != DIRECTION_Y) { direction = DIRECTION_X; } String propertyName = ""; switch (direction) { case DIRECTION_X: propertyName = "translationX"; break; case DIRECTION_Y: propertyName = "translationY"; break; } ObjectAnimator animator = ObjectAnimator.ofFloat(rlOutsideBackground.findViewById(R.id.rlParentForAnimate), propertyName, values) .setDuration(duration); if (isShow) { objectAnimatorsForDialogShow.add(animator); } else { objectAnimatorsForDialogDismiss.add(animator); } return this; } /** * 对话框出现时候的渐变动画 * * @param duration 动画执行的时间长度 * @param values 动画移动的位置 */ public EasyDialog setAnimationAlphaShow(int duration, float... values) { return setAnimationAlpha(true, duration, values); } /** * 对话框消失时候的渐变动画 * * @param duration 动画执行的时间长度 * @param values 动画移动的位置 */ public EasyDialog setAnimationAlphaDismiss(int duration, float... values) { return setAnimationAlpha(false, duration, values); } private EasyDialog setAnimationAlpha(boolean isShow, int duration, float... values) { ObjectAnimator animator = ObjectAnimator.ofFloat(rlOutsideBackground.findViewById(R.id.rlParentForAnimate), "alpha", values).setDuration(duration); if (isShow) { objectAnimatorsForDialogShow.add(animator); } else { objectAnimatorsForDialogDismiss.add(animator); } return this; } private AnimatorSet animatorSetForDialogShow; private AnimatorSet animatorSetForDialogDismiss; private List<Animator> objectAnimatorsForDialogShow; private List<Animator> objectAnimatorsForDialogDismiss; private void onDialogShowing() { if (animatorSetForDialogShow != null && objectAnimatorsForDialogShow != null && objectAnimatorsForDialogShow.size() > 0) { animatorSetForDialogShow.playTogether(objectAnimatorsForDialogShow); animatorSetForDialogShow.start(); } //TODO 缩放的动画效果不好,不能从控件所在的位置开始缩放// ObjectAnimator.ofFloat(rlOutsideBackground.findViewById(R.id.rlParentForAnimate), "scaleX", 0.3f, 1.0f).setDuration(500).start();// ObjectAnimator.ofFloat(rlOutsideBackground.findViewById(R.id.rlParentForAnimate), "scaleY", 0.3f, 1.0f).setDuration(500).start(); } @SuppressLint("NewApi") private void onDialogDismiss() { if (animatorSetForDialogDismiss.isRunning()) { return; } if (animatorSetForDialogDismiss != null && objectAnimatorsForDialogDismiss != null && objectAnimatorsForDialogDismiss.size() > 0) { animatorSetForDialogDismiss.playTogether(objectAnimatorsForDialogDismiss); animatorSetForDialogDismiss.start(); animatorSetForDialogDismiss.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { //这里有可能会有bug,当Dialog所依赖的Activity关闭的时候,如果这个时候,用户关闭对话框,由于对话框的动画关闭需要时间,当动画执行完毕后,对话框所依赖的Activity已经被销毁了,执行dismiss()就会报错 if (context != null && context instanceof Activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { if (!((Activity) context).isDestroyed()) { dialog.dismiss(); } } else { try { dialog.dismiss(); } catch (final IllegalArgumentException e) { } catch (final Exception e) { } finally { dialog = null; } } } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); } else { dialog.dismiss(); } } /** * 关闭提示框 */ public void dismiss() { if (dialog != null && dialog.isShowing()) { onDialogDismiss(); } } /** * 根据x,y,重新设置控件的位置 * 因为setX setY为0的时候,都是在状态栏以下的,所以app不是全屏的话,需要扣掉状态栏的高度 */ private void relocation(int[] location) { float statusBarHeight = isFullScreen() ? 0.0f : getStatusBarHeight(); ivTriangle.setX(location[0] - ivTriangle.getWidth() / 2); ivTriangle.setY(location[1] - ivTriangle.getHeight() / 2 - statusBarHeight); switch (gravity) { case GRAVITY_BOTTOM: llContent.setY(location[1] - ivTriangle.getHeight() / 2 - statusBarHeight + ivTriangle.getHeight()); break; case GRAVITY_TOP: llContent.setY(location[1] - llContent.getHeight() - statusBarHeight - ivTriangle.getHeight() / 2); break; case GRAVITY_LEFT: llContent.setX(location[0] - llContent.getWidth() - ivTriangle.getWidth() / 2); break; case GRAVITY_RIGHT: llContent.setX(location[0] + ivTriangle.getWidth() / 2); break; } RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) llContent.getLayoutParams(); switch (gravity) { case GRAVITY_BOTTOM: case GRAVITY_TOP: int triangleCenterX = (int) (ivTriangle.getX() + ivTriangle.getWidth() / 2); int contentWidth = llContent.getWidth(); int rightMargin = getScreenWidth() - triangleCenterX; int leftMargin = getScreenWidth() - rightMargin; int availableLeftMargin = leftMargin - layoutParams.leftMargin; int availableRightMargin = rightMargin - layoutParams.rightMargin; int x = 0; if (contentWidth / 2 <= availableLeftMargin && contentWidth / 2 <= availableRightMargin) { x = triangleCenterX - contentWidth / 2; } else { if (availableLeftMargin <= availableRightMargin) { x = layoutParams.leftMargin; } else { x = getScreenWidth() - (contentWidth + layoutParams.rightMargin); } } llContent.setX(x); break; case GRAVITY_LEFT: case GRAVITY_RIGHT: int triangleCenterY = (int) (ivTriangle.getY() + ivTriangle.getHeight() / 2); int contentHeight = llContent.getHeight(); int topMargin = triangleCenterY; int bottomMargin = getScreenHeight() - topMargin; int availableTopMargin = topMargin - layoutParams.topMargin; int availableBottomMargin = bottomMargin - layoutParams.bottomMargin; int y = 0; if (contentHeight / 2 <= availableTopMargin && contentHeight / 2 <= availableBottomMargin) { y = triangleCenterY - contentHeight / 2; } else { if (availableTopMargin <= availableBottomMargin) { y = layoutParams.topMargin; } else { y = getScreenHeight() - (contentHeight + layoutParams.topMargin); } } llContent.setY(y); break; } } /** * 获取屏幕的宽度 */ private int getScreenWidth() { DisplayMetrics metrics = context.getResources().getDisplayMetrics(); return metrics.widthPixels; } private int getScreenHeight() { int statusBarHeight = isFullScreen() ? 0 : getStatusBarHeight(); DisplayMetrics metrics = context.getResources().getDisplayMetrics(); return metrics.heightPixels - statusBarHeight; } /** * 获取状态栏的高度 */ private int getStatusBarHeight() { int result = 0; int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = context.getResources().getDimensionPixelSize(resourceId); } return result; } /** * 判断下当前要显示对话框的Activity是否是全屏 */ public boolean isFullScreen() { int flg = ((Activity) context).getWindow().getAttributes().flags; boolean flag = false; if ((flg & 1024) == 1024) { flag = true; } return flag; } /** * 设置是否可以按返回按钮取消 */ public EasyDialog setCancelable(boolean cancelable) { dialog.setCancelable(cancelable); return this; } private OnEasyDialogDismissed onEasyDialogDismissed; public EasyDialog setOnEasyDialogDismissed(OnEasyDialogDismissed onEasyDialogDismissed) { this.onEasyDialogDismissed = onEasyDialogDismissed; return this; } /** * Dialog is dismissed */ public interface OnEasyDialogDismissed { public void onDismissed(); } private OnEasyDialogShow onEasyDialogShow; public EasyDialog setOnEasyDialogShow(OnEasyDialogShow onEasyDialogShow) { this.onEasyDialogShow = onEasyDialogShow; return this; } /** * Dialog is showing */ public interface OnEasyDialogShow { public void onShow(); } }8)EdittextRobotoRegular.java==============================package com.eppico.widgets; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Typeface;import android.text.InputFilter;import android.util.AttributeSet;import android.widget.EditText; import com.eppico.R; public class EdittextRobotoRegular extends EditText { Context context; boolean disableEmoticons=true; // Initial disable emoticons public EdittextRobotoRegular(Context context) { this(context,null); /*super(context); this.context = context; init(context);*/ } public EdittextRobotoRegular(Context context, AttributeSet attrs) { this(context,attrs,android.R.attr.editTextStyle); /*super(context, attrs); this.context = context; init(context);*/ } public EdittextRobotoRegular(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.context = context; init(context,attrs); } private void init(Context context,AttributeSet attrs) { TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.MyEditText, 0, 0); this.disableEmoticons=attr.getBoolean(R.styleable.MyEditText_disableEmoticons, disableEmoticons); //this.disableEmoticons=attr.getBoolean(R.styleable.MyEdittext_disableEmoticons, disableEmoticons); Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "Roboto-Regular_3.ttf"); setTypeface(tf); /*InputFilter[] filterArray = new InputFilter[2]; filterArray[0] = new InputFilter.LengthFilter(maxLength); filterArray[1] = new EmojiExcludeFilter(context); setFilters(filterArray);*/ if(disableEmoticons) { InputFilter curFilters[] = getFilters(); if (curFilters != null) { InputFilter newFilters[] = new InputFilter[curFilters.length + 1]; System.arraycopy(curFilters, 0, newFilters, 0, curFilters.length); newFilters[curFilters.length] = new EmojiExcludeFilter(context); setFilters(newFilters); } } } /** Default it is enabled * @param disableEmoticons true: Disable emoticons, false: enable emoticons * */ public void setDisableEmoticons(boolean disableEmoticons) { this.disableEmoticons=disableEmoticons; invalidate(); } /*private class EmojiExcludeFilter implements InputFilter { @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for (int i = start; i < end; i++) { int type = Character.getType(source.charAt(i)); if (type == Character.SURROGATE || type == Character.OTHER_SYMBOL) { //Show message only one time if(firstTime) { firstTime=false; ToastHelper.getInstance().showToast(context,context.getString(R.string.msgEmoticonsNotAllowed)); } return ""; } } return null; } }*/}9)EmojiExcludeFilter.java==========================package com.eppico.widgets; import android.content.Context;import android.text.InputFilter;import android.text.Spanned;import android.widget.Toast; import com.eppico.R;import com.eppico.utility.ToastHelper; public class EmojiExcludeFilter implements InputFilter { Context context; private boolean firstTime=true; public EmojiExcludeFilter(Context context) { this.context= context; } @Override public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { for (int i = start; i < end; i++) { int type = Character.getType(source.charAt(i)); if (type == Character.SURROGATE || type == Character.OTHER_SYMBOL) { //Show message only one time if (firstTime) { firstTime = false; ToastHelper.getInstance().showToast(context, context.getString(R.string.msgEmoticonsNotAllowed), Toast.LENGTH_LONG); } return ""; } } return null; } }10)MyEditText.java=======================package com.eppico.widgets; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Typeface;import android.text.InputFilter;import android.util.AttributeSet;import android.widget.EditText; import com.eppico.R; public class MyEditText extends EditText { Context context; boolean disableEmoticons=true; // Initial disable emoticons int fontType; String fontNames[]; public MyEditText(Context context) { this(context,null); } public MyEditText(Context context, AttributeSet attrs) { this(context,attrs,android.R.attr.editTextStyle); } public MyEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.context = context; fontNames=context.getResources().getStringArray(R.array.fontNames); init(context,attrs); } private void init(Context context,AttributeSet attrs) { TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.MyEditText, 0, 0); this.disableEmoticons=attr.getBoolean(R.styleable.MyEditText_disableEmoticons, disableEmoticons); this.fontType=attr.getInt(R.styleable.MyEditText_fontType, 0); // Set TypeFace Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), fontNames[fontType]); setTypeface(typeface); if(disableEmoticons) { InputFilter curFilters[] = getFilters(); if (curFilters != null) { InputFilter newFilters[] = new InputFilter[curFilters.length + 1]; System.arraycopy(curFilters, 0, newFilters, 0, curFilters.length); newFilters[curFilters.length] = new EmojiExcludeFilter(context); setFilters(newFilters); } } } /** Default it is enabled * @param disableEmoticons true: Disable emoticons, false: enable emoticons * */ public void setDisableEmoticons(boolean disableEmoticons) { this.disableEmoticons=disableEmoticons; invalidate(); } }11)MyTextView.java========================package com.eppico.widgets; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Typeface;import android.text.InputFilter;import android.util.AttributeSet;import android.widget.EditText;import android.widget.TextView; import com.eppico.R; public class MyTextView extends TextView { Context context; int fontType; String fontNames[]; public MyTextView(Context context) { this(context,null); } public MyTextView(Context context, AttributeSet attrs) { this(context,attrs,android.R.attr.textViewStyle); } public MyTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.context = context; fontNames=context.getResources().getStringArray(R.array.fontNames); init(context,attrs); } private void init(Context context,AttributeSet attrs) { TypedArray attr = context.obtainStyledAttributes(attrs, R.styleable.MyEditText, 0, 0); this.fontType=attr.getInt(R.styleable.MyTextView_fontType, 0); // Set TypeFace Typeface typeface = Typeface.createFromAsset(getContext().getAssets(), fontNames[fontType]); setTypeface(typeface); } }12)ShapedImageView.java========================package com.eppico.widgets; import android.content.Context;import android.content.res.TypedArray;import android.graphics.Bitmap;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.Paint;import android.graphics.Path;import android.graphics.PorterDuff;import android.graphics.PorterDuffXfermode;import android.graphics.RectF;import android.graphics.drawable.shapes.RoundRectShape;import android.graphics.drawable.shapes.Shape;import android.os.Build;import android.util.AttributeSet;import android.widget.ImageView; import com.eppico.R; import java.util.Arrays; public class ShapedImageView extends ImageView { public static final int SHAPE_MODE_ROUND_RECT = 1; public static final int SHAPE_MODE_CIRCLE = 2; private static final int LAYER_FLAGS = Canvas.MATRIX_SAVE_FLAG | Canvas.CLIP_SAVE_FLAG | Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.FULL_COLOR_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG; private int mShapeMode = 0; private float mRadius = 0; private int mStrokeColor = 0x26000000; private float mStrokeWidth = 0; private Path mPath; private Shape mShape, mStrokeShape; private Paint mPaint, mStrokePaint, mPathPaint; private Bitmap mStrokeBitmap; private PathExtension mExtension; public ShapedImageView(Context context) { super(context); init(null); } public ShapedImageView(Context context, AttributeSet attrs) { super(context, attrs); init(attrs); } public ShapedImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(attrs); } private void init(AttributeSet attrs) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { setLayerType(LAYER_TYPE_HARDWARE, null); } if (attrs != null) { TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ShapedImageView); mShapeMode = a.getInt(R.styleable.ShapedImageView_shape_mode, 2); mRadius = a.getDimension(R.styleable.ShapedImageView_round_radius, 0); mStrokeWidth = a.getDimension(R.styleable.ShapedImageView_stroke_width, 0); mStrokeColor = a.getColor(R.styleable.ShapedImageView_stroke_color, mStrokeColor); a.recycle(); } mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPaint.setFilterBitmap(true); mPaint.setColor(Color.BLACK); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); mStrokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mStrokePaint.setFilterBitmap(true); mStrokePaint.setColor(mStrokeColor); mPathPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPathPaint.setFilterBitmap(true); mPathPaint.setColor(Color.BLACK); mPathPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR)); mPath = new Path(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { switch (mShapeMode) { case SHAPE_MODE_ROUND_RECT: break; case SHAPE_MODE_CIRCLE: int min = Math.min(getWidth(), getHeight()); mRadius = (float) min / 2; break; } if (mShape == null) { float[] radius = new float[8]; Arrays.fill(radius, mRadius); mShape = new RoundRectShape(radius, null, null); mStrokeShape = new RoundRectShape(radius, null, null); } mShape.resize(getWidth(), getHeight()); mStrokeShape.resize(getWidth() - mStrokeWidth * 2, getHeight() - mStrokeWidth * 2); if (mStrokeWidth > 0 && mStrokeBitmap == null) { mStrokeBitmap = makeStrokeBitmap(getWidth(), getHeight()); } if (mExtension != null) { mExtension.onLayout(mPath, getWidth(), getHeight()); } } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mStrokeWidth > 0 && mStrokeShape != null && mStrokeBitmap != null) { int i = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, LAYER_FLAGS); mStrokePaint.setXfermode(null); canvas.drawBitmap(mStrokeBitmap, 0, 0, mStrokePaint); canvas.translate(mStrokeWidth, mStrokeWidth); mStrokePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); mStrokeShape.draw(canvas, mStrokePaint); canvas.restoreToCount(i); } if (mExtension != null) { canvas.drawPath(mPath, mPathPaint); } switch (mShapeMode) { case SHAPE_MODE_ROUND_RECT: case SHAPE_MODE_CIRCLE: if (mShape != null) { mShape.draw(canvas, mPaint); } break; } } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mStrokeWidth > 0 && mStrokeBitmap == null && mStrokeShape != null) { mStrokeBitmap = makeStrokeBitmap(getWidth(), getHeight()); } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (mStrokeBitmap != null) { mStrokeBitmap.recycle(); mStrokeBitmap = null; } } private Bitmap makeStrokeBitmap(int w, int h) { Bitmap bm = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas c = new Canvas(bm); Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); p.setColor(mStrokeColor); c.drawRect(new RectF(0, 0, w, h), p); return bm; } public void setExtension(PathExtension extension) { mExtension = extension; requestLayout(); } public interface PathExtension { void onLayout(Path path, int width, int height); } public void setShapeMode(int shapeMode) { this.mShapeMode=shapeMode; invalidate(); } }13)TextViewRobotoBold.java==========================package com.eppico.widgets; import android.content.Context;import android.graphics.Typeface;import android.util.AttributeSet;import android.widget.TextView; public class TextViewRobotoBold extends TextView { Context context; public TextViewRobotoBold(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); this.context = context; init(context); } public TextViewRobotoBold(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; init(context); } public TextViewRobotoBold(Context context) { super(context); this.context = context; init(context); } private void init(Context context) {// if (!isInEditMode()) { Typeface tf = Typeface.createFromAsset(getContext().getAssets(), "Roboto-Bold_3.ttf"); setTypeface(tf);// } }}14)TouchImageView.java==============================package com.eppico.widgets; import android.annotation.TargetApi;import android.content.Context;import android.graphics.Bitmap;import android.graphics.Matrix;import android.graphics.PointF;import android.graphics.drawable.Drawable;import android.net.Uri;import android.os.Build;import android.os.Build.VERSION;import android.os.Build.VERSION_CODES;import android.os.Bundle;import android.os.Parcelable;import android.util.AttributeSet;import android.util.Log;import android.view.GestureDetector;import android.view.MotionEvent;import android.view.ScaleGestureDetector;import android.view.View;import android.view.animation.AccelerateDecelerateInterpolator;import android.widget.ImageView;import android.widget.Scroller; public class TouchImageView extends ImageView { private static final String DEBUG = "DEBUG"; // SuperMin and SuperMax multipliers. Determine how much the image can be // zoomed below or above the zoom boundaries, before animating back to the // min/max zoom boundary. // private static final float SUPER_MIN_MULTIPLIER = .75f; private static final float SUPER_MAX_MULTIPLIER = 1.25f; // Scale of image ranges from minScale to maxScale, where minScale == 1 // when the image is stretched to fit view. // private float normalizedScale; // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix // saved prior to the screen rotating. // private Matrix matrix, prevMatrix; public static enum State { NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM }; private State state; private float minScale; private float maxScale; private float superMinScale; private float superMaxScale; private float[] m; private Context context; private Fling fling; // // Size of view and previous view size (ie before rotation) // private int viewWidth, viewHeight, prevViewWidth, prevViewHeight; // // Size of image when it is stretched to fit view. Before and After // rotation. // private float matchViewWidth, matchViewHeight, prevMatchViewWidth, prevMatchViewHeight; // // After setting image, a value of true means the new image should maintain // the zoom of the previous image. False means it should be resized within // the view. // private boolean maintainZoomAfterSetImage; // // True when maintainZoomAfterSetImage has been set to true and setImage has // been called. // private boolean setImageCalledRecenterImage; private ScaleGestureDetector mScaleDetector; private GestureDetector mGestureDetector; public TouchImageView(Context context) { super(context); sharedConstructing(context); } public TouchImageView(Context context, AttributeSet attrs) { super(context, attrs); sharedConstructing(context); } public TouchImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); sharedConstructing(context); } private void sharedConstructing(Context context) { super.setClickable(true); this.context = context; mScaleDetector = new ScaleGestureDetector(context, new ScaleListener()); mGestureDetector = new GestureDetector(context, new GestureListener()); matrix = new Matrix(); prevMatrix = new Matrix(); m = new float[9]; normalizedScale = 1; minScale = 1; maxScale = 3; superMinScale = SUPER_MIN_MULTIPLIER * minScale; superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; maintainZoomAfterSetImage = true; setImageMatrix(matrix); setScaleType(ScaleType.MATRIX); setState(State.NONE); setOnTouchListener(new TouchImageViewListener()); } public void enableZoom(boolean enable) { if(enable) setOnTouchListener(new TouchImageViewListener()); else setOnTouchListener(null); } @Override public void setImageResource(int resId) { super.setImageResource(resId); setImageCalled(); savePreviousImageValues(); fitImageToView(); } @Override public void setImageBitmap(Bitmap bm) { super.setImageBitmap(bm); setImageCalled(); savePreviousImageValues(); fitImageToView(); } @Override public void setImageDrawable(Drawable drawable) { super.setImageDrawable(drawable); setImageCalled(); savePreviousImageValues(); fitImageToView(); } @Override public void setImageURI(Uri uri) { super.setImageURI(uri); setImageCalled(); savePreviousImageValues(); fitImageToView(); } private void setImageCalled() { if (!maintainZoomAfterSetImage) { setImageCalledRecenterImage = true; } } /** * Save the current matrix and view dimensions in the prevMatrix and * prevView variables. */ private void savePreviousImageValues() { if (matrix != null) { matrix.getValues(m); prevMatrix.setValues(m); prevMatchViewHeight = matchViewHeight; prevMatchViewWidth = matchViewWidth; prevViewHeight = viewHeight; prevViewWidth = viewWidth; } } @Override public Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable("instanceState", super.onSaveInstanceState()); bundle.putFloat("saveScale", normalizedScale); bundle.putFloat("matchViewHeight", matchViewHeight); bundle.putFloat("matchViewWidth", matchViewWidth); bundle.putInt("viewWidth", viewWidth); bundle.putInt("viewHeight", viewHeight); matrix.getValues(m); bundle.putFloatArray("matrix", m); return bundle; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof Bundle) { Bundle bundle = (Bundle) state; normalizedScale = bundle.getFloat("saveScale"); m = bundle.getFloatArray("matrix"); prevMatrix.setValues(m); prevMatchViewHeight = bundle.getFloat("matchViewHeight"); prevMatchViewWidth = bundle.getFloat("matchViewWidth"); prevViewHeight = bundle.getInt("viewHeight"); prevViewWidth = bundle.getInt("viewWidth"); super.onRestoreInstanceState(bundle.getParcelable("instanceState")); return; } super.onRestoreInstanceState(state); } /** * Get the max zoom multiplier. * * @return max zoom multiplier. */ public float getMaxZoom() { return maxScale; } /** * Set the max zoom multiplier. Default value: 3. * * @param max * max zoom multiplier. */ public void setMaxZoom(float max) { maxScale = max; superMaxScale = SUPER_MAX_MULTIPLIER * maxScale; } /** * Get the min zoom multiplier. * * @return min zoom multiplier. */ public float getMinZoom() { return minScale; } /** * After setting image, a value of true means the new image should maintain * the zoom of the previous image. False means the image should be resized * within the view. Defaults value is true. * * @param maintainZoom */ public void maintainZoomAfterSetImage(boolean maintainZoom) { maintainZoomAfterSetImage = maintainZoom; } /** * Get the current zoom. This is the zoom relative to the initial scale, not * the original resource. * * @return current zoom multiplier. */ public float getCurrentZoom() { return normalizedScale; } /** * Set the min zoom multiplier. Default value: 1. * * @param min * min zoom multiplier. */ public void setMinZoom(float min) { minScale = min; superMinScale = SUPER_MIN_MULTIPLIER * minScale; } /** * For a given point on the view (ie, a touch event), returns the point * relative to the original drawable's coordinate system. * * @param x * @param y * @return PointF relative to original drawable's coordinate system. */ public PointF getDrawablePointFromTouchPoint(float x, float y) { return transformCoordTouchToBitmap(x, y, true); } /** * For a given point on the view (ie, a touch event), returns the point * relative to the original drawable's coordinate system. * * @param p * @return PointF relative to original drawable's coordinate system. */ public PointF getDrawablePointFromTouchPoint(PointF p) { return transformCoordTouchToBitmap(p.x, p.y, true); } /** * Performs boundary checking and fixes the image matrix if it is out of * bounds. */ private void fixTrans() { matrix.getValues(m); float transX = m[Matrix.MTRANS_X]; float transY = m[Matrix.MTRANS_Y]; float fixTransX = getFixTrans(transX, viewWidth, getImageWidth()); float fixTransY = getFixTrans(transY, viewHeight, getImageHeight()); if (fixTransX != 0 || fixTransY != 0) { matrix.postTranslate(fixTransX, fixTransY); } } /** * When transitioning from zooming from focus to zoom from center (or vice * versa) the image can become unaligned within the view. This is apparent * when zooming quickly. When the content size is less than the view size, * the content will often be centered incorrectly within the view. * fixScaleTrans first calls fixTrans() and then makes sure the image is * centered correctly within the view. */ private void fixScaleTrans() { fixTrans(); matrix.getValues(m); if (getImageWidth() < viewWidth) { m[Matrix.MTRANS_X] = (viewWidth - getImageWidth()) / 2; } if (getImageHeight() < viewHeight) { m[Matrix.MTRANS_Y] = (viewHeight - getImageHeight()) / 2; } matrix.setValues(m); } private float getFixTrans(float trans, float viewSize, float contentSize) { float minTrans, maxTrans; if (contentSize <= viewSize) { minTrans = 0; maxTrans = viewSize - contentSize; } else { minTrans = viewSize - contentSize; maxTrans = 0; } if (trans < minTrans) return -trans + minTrans; if (trans > maxTrans) return -trans + maxTrans; return 0; } private float getFixDragTrans(float delta, float viewSize, float contentSize) { if (contentSize <= viewSize) { return 0; } return delta; } private float getImageWidth() { return matchViewWidth * normalizedScale; } private float getImageHeight() { return matchViewHeight * normalizedScale; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { Drawable drawable = getDrawable(); if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { setMeasuredDimension(0, 0); return; } int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); viewWidth = setViewSize(widthMode, widthSize, drawableWidth); viewHeight = setViewSize(heightMode, heightSize, drawableHeight); // // Set view dimensions // setMeasuredDimension(viewWidth, viewHeight); // // Fit content within view // fitImageToView(); } /** * If the normalizedScale is equal to 1, then the image is made to fit the * screen. Otherwise, it is made to fit the screen according to the * dimensions of the previous image matrix. This allows the image to * maintain its zoom after rotation. */ private void fitImageToView() { Drawable drawable = getDrawable(); if (drawable == null || drawable.getIntrinsicWidth() == 0 || drawable.getIntrinsicHeight() == 0) { return; } if (matrix == null || prevMatrix == null) { return; } int drawableWidth = drawable.getIntrinsicWidth(); int drawableHeight = drawable.getIntrinsicHeight(); // // Scale image for view // float scaleX = (float) viewWidth / drawableWidth; float scaleY = (float) viewHeight / drawableHeight; float scale = Math.min(scaleX, scaleY); // // Center the image // float redundantYSpace = viewHeight - (scale * drawableHeight); float redundantXSpace = viewWidth - (scale * drawableWidth); matchViewWidth = viewWidth - redundantXSpace; matchViewHeight = viewHeight - redundantYSpace; if (normalizedScale == 1 || setImageCalledRecenterImage) { // // Stretch and center image to fit view // matrix.setScale(scale, scale); matrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2); normalizedScale = 1; setImageCalledRecenterImage = false; } else { prevMatrix.getValues(m); // // Rescale Matrix after rotation // m[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * normalizedScale; m[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * normalizedScale; // // TransX and TransY from previous matrix // float transX = m[Matrix.MTRANS_X]; float transY = m[Matrix.MTRANS_Y]; // // Width // float prevActualWidth = prevMatchViewWidth * normalizedScale; float actualWidth = getImageWidth(); translateMatrixAfterRotate(Matrix.MTRANS_X, transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth); // // Height // float prevActualHeight = prevMatchViewHeight * normalizedScale; float actualHeight = getImageHeight(); translateMatrixAfterRotate(Matrix.MTRANS_Y, transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight); // Set the matrix to the adjusted scale and translate values. // matrix.setValues(m); } setImageMatrix(matrix); } /** * Set view dimensions based on layout params * * @param mode * @param size * @param drawableWidth * @return */ private int setViewSize(int mode, int size, int drawableWidth) { int viewSize; switch (mode) { case MeasureSpec.EXACTLY: viewSize = size; break; case MeasureSpec.AT_MOST: viewSize = Math.min(drawableWidth, size); break; case MeasureSpec.UNSPECIFIED: viewSize = drawableWidth; break; default: viewSize = size; break; } return viewSize; } /** * After rotating, the matrix needs to be translated. This function finds * the area of image which was previously centered and adjusts translations * so that is again the center, post-rotation. * * @param axis * Matrix.MTRANS_X or Matrix.MTRANS_Y * @param trans * the value of trans in that axis before the rotation * @param prevImageSize * the width/height of the image before the rotation * @param imageSize * width/height of the image after rotation * @param prevViewSize * width/height of view before rotation * @param viewSize * width/height of view after rotation * @param drawableSize * width/height of drawable */ private void translateMatrixAfterRotate(int axis, float trans, float prevImageSize, float imageSize, int prevViewSize, int viewSize, int drawableSize) { if (imageSize < viewSize) { // // The width/height of image is less than the view's width/height. // Center it. // m[axis] = (viewSize - (drawableSize * m[Matrix.MSCALE_X])) * 0.5f; } else if (trans > 0) { // // The image is larger than the view, but was not before rotation. // Center it. // m[axis] = -((imageSize - viewSize) * 0.5f); } else { // // Find the area of the image which was previously centered in the // view. Determine its distance // from the left/top side of the view as a fraction of the entire // image's width/height. Use that percentage // to calculate the trans in the new view width/height. // float percentage = (Math.abs(trans) + (0.5f * prevViewSize)) / prevImageSize; m[axis] = -((percentage * imageSize) - (viewSize * 0.5f)); } } private void setState(State state) { this.state = state; } /** * Gesture Listener detects a single click or long click and passes that on * to the view's listener. * * @author Ortiz * */ private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onSingleTapConfirmed(MotionEvent e) { return performClick(); } @Override public void onLongPress(MotionEvent e) { performLongClick(); } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (fling != null) { // // If a previous fling is still active, it should be cancelled // so that two flings // are not run simultaenously. // fling.cancelFling(); } fling = new Fling((int) velocityX, (int) velocityY); compatPostOnAnimation(fling); return super.onFling(e1, e2, velocityX, velocityY); } @Override public boolean onDoubleTap(MotionEvent e) { boolean consumed = false; if (state == State.NONE) { float targetZoom = (normalizedScale == minScale) ? maxScale : minScale; DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, e.getX(), e.getY(), false); compatPostOnAnimation(doubleTap); consumed = true; } return consumed; } } /** * Responsible for all touch events. Handles the heavy lifting of drag and * also sends touch events to Scale Detector and Gesture Detector. * * @author Ortiz * */ private class TouchImageViewListener implements OnTouchListener { // // Remember last point position for dragging // private PointF last = new PointF(); @Override public boolean onTouch(View v, MotionEvent event) { mScaleDetector.onTouchEvent(event); mGestureDetector.onTouchEvent(event); PointF curr = new PointF(event.getX(), event.getY()); if (state == State.NONE || state == State.DRAG || state == State.FLING) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: last.set(curr); if (fling != null) fling.cancelFling(); setState(State.DRAG); break; case MotionEvent.ACTION_MOVE: if (state == State.DRAG) { float deltaX = curr.x - last.x; float deltaY = curr.y - last.y; float fixTransX = getFixDragTrans(deltaX, viewWidth, getImageWidth()); float fixTransY = getFixDragTrans(deltaY, viewHeight, getImageHeight()); matrix.postTranslate(fixTransX, fixTransY); fixTrans(); last.set(curr.x, curr.y); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_POINTER_UP: setState(State.NONE); break; } } setImageMatrix(matrix); // // indicate event was handled // return true; } } /** * ScaleListener detects user two finger scaling and scales image. * * @author Ortiz * */ private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScaleBegin(ScaleGestureDetector detector) { setState(State.ZOOM); return true; } @Override public boolean onScale(ScaleGestureDetector detector) { scaleImage(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY(), true); return true; } @Override public void onScaleEnd(ScaleGestureDetector detector) { super.onScaleEnd(detector); setState(State.NONE); boolean animateToZoomBoundary = false; float targetZoom = normalizedScale; if (normalizedScale > maxScale) { targetZoom = maxScale; animateToZoomBoundary = true; } else if (normalizedScale < minScale) { targetZoom = minScale; animateToZoomBoundary = true; } if (animateToZoomBoundary) { DoubleTapZoom doubleTap = new DoubleTapZoom(targetZoom, viewWidth / 2, viewHeight / 2, true); compatPostOnAnimation(doubleTap); } } } private void scaleImage(float deltaScale, float focusX, float focusY, boolean stretchImageToSuper) { float lowerScale, upperScale; if (stretchImageToSuper) { lowerScale = superMinScale; upperScale = superMaxScale; } else { lowerScale = minScale; upperScale = maxScale; } float origScale = normalizedScale; normalizedScale *= deltaScale; if (normalizedScale > upperScale) { normalizedScale = upperScale; deltaScale = upperScale / origScale; } else if (normalizedScale < lowerScale) { normalizedScale = lowerScale; deltaScale = lowerScale / origScale; } matrix.postScale(deltaScale, deltaScale, focusX, focusY); fixScaleTrans(); } /** * DoubleTapZoom calls a series of runnables which apply an animated zoom * in/out graphic to the image. * * @author Ortiz * */ private class DoubleTapZoom implements Runnable { private long startTime; private static final float ZOOM_TIME = 500; private float startZoom, targetZoom; private float bitmapX, bitmapY; private boolean stretchImageToSuper; private AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator(); private PointF startTouch; private PointF endTouch; DoubleTapZoom(float targetZoom, float focusX, float focusY, boolean stretchImageToSuper) { setState(State.ANIMATE_ZOOM); startTime = System.currentTimeMillis(); this.startZoom = normalizedScale; this.targetZoom = targetZoom; this.stretchImageToSuper = stretchImageToSuper; PointF bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false); this.bitmapX = bitmapPoint.x; this.bitmapY = bitmapPoint.y; // // Used for translating image during scaling // startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY); endTouch = new PointF(viewWidth / 2, viewHeight / 2); } @Override public void run() { float t = interpolate(); float deltaScale = calculateDeltaScale(t); scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper); translateImageToCenterTouchPosition(t); fixScaleTrans(); setImageMatrix(matrix); if (t < 1f) { // // We haven't finished zooming // compatPostOnAnimation(this); } else { // // Finished zooming // setState(State.NONE); } } /** * Interpolate between where the image should start and end in order to * translate the image so that the point that is touched is what ends up * centered at the end of the zoom. * * @param t */ private void translateImageToCenterTouchPosition(float t) { float targetX = startTouch.x + t * (endTouch.x - startTouch.x); float targetY = startTouch.y + t * (endTouch.y - startTouch.y); PointF curr = transformCoordBitmapToTouch(bitmapX, bitmapY); matrix.postTranslate(targetX - curr.x, targetY - curr.y); } /** * Use interpolator to get t * * @return */ private float interpolate() { long currTime = System.currentTimeMillis(); float elapsed = (currTime - startTime) / ZOOM_TIME; elapsed = Math.min(1f, elapsed); return interpolator.getInterpolation(elapsed); } /** * Interpolate the current targeted zoom and get the delta from the * current zoom. * * @param t * @return */ private float calculateDeltaScale(float t) { float zoom = startZoom + t * (targetZoom - startZoom); return zoom / normalizedScale; } } /** * This function will transform the coordinates in the touch event to the * coordinate system of the drawable that the imageview contain * * @param x * x-coordinate of touch event * @param y * y-coordinate of touch event * @param clipToBitmap * Touch event may occur within view, but outside image content. * True, to clip return value to the bounds of the bitmap size. * @return Coordinates of the point touched, in the coordinate system of the * original drawable. */ private PointF transformCoordTouchToBitmap(float x, float y, boolean clipToBitmap) { matrix.getValues(m); float origW = getDrawable().getIntrinsicWidth(); float origH = getDrawable().getIntrinsicHeight(); float transX = m[Matrix.MTRANS_X]; float transY = m[Matrix.MTRANS_Y]; float finalX = ((x - transX) * origW) / getImageWidth(); float finalY = ((y - transY) * origH) / getImageHeight(); if (clipToBitmap) { finalX = Math.min(Math.max(x, 0), origW); finalY = Math.min(Math.max(y, 0), origH); } return new PointF(finalX, finalY); } /** * Inverse of transformCoordTouchToBitmap. This function will transform the * coordinates in the drawable's coordinate system to the view's coordinate * system. * * @param bx * x-coordinate in original bitmap coordinate system * @param by * y-coordinate in original bitmap coordinate system * @return Coordinates of the point in the view's coordinate system. */ private PointF transformCoordBitmapToTouch(float bx, float by) { matrix.getValues(m); float origW = getDrawable().getIntrinsicWidth(); float origH = getDrawable().getIntrinsicHeight(); float px = bx / origW; float py = by / origH; float finalX = m[Matrix.MTRANS_X] + getImageWidth() * px; float finalY = m[Matrix.MTRANS_Y] + getImageHeight() * py; return new PointF(finalX, finalY); } /** * Fling launches sequential runnables which apply the fling graphic to the * image. The values for the translation are interpolated by the Scroller. * * @author Ortiz * */ private class Fling implements Runnable { Scroller scroller; int currX, currY; Fling(int velocityX, int velocityY) { setState(State.FLING); scroller = new Scroller(context); matrix.getValues(m); int startX = (int) m[Matrix.MTRANS_X]; int startY = (int) m[Matrix.MTRANS_Y]; int minX, maxX, minY, maxY; if (getImageWidth() > viewWidth) { minX = viewWidth - (int) getImageWidth(); maxX = 0; } else { minX = maxX = startX; } if (getImageHeight() > viewHeight) { minY = viewHeight - (int) getImageHeight(); maxY = 0; } else { minY = maxY = startY; } scroller.fling(startX, startY, (int) velocityX, (int) velocityY, minX, maxX, minY, maxY); currX = startX; currY = startY; } public void cancelFling() { if (scroller != null) { setState(State.NONE); scroller.forceFinished(true); } } @Override public void run() { if (scroller.isFinished()) { scroller = null; return; } if (scroller.computeScrollOffset()) { int newX = scroller.getCurrX(); int newY = scroller.getCurrY(); int transX = newX - currX; int transY = newY - currY; currX = newX; currY = newY; matrix.postTranslate(transX, transY); fixTrans(); setImageMatrix(matrix); compatPostOnAnimation(this); } } } @TargetApi(VERSION_CODES.JELLY_BEAN) private void compatPostOnAnimation(Runnable runnable) { if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { postOnAnimation(runnable); } else { postDelayed(runnable, 1000 / 60); } } private void printMatrixInfo() { matrix.getValues(m); Log.d(DEBUG, "Scale: " + m[Matrix.MSCALE_X] + " TransX: " + m[Matrix.MTRANS_X] + " TransY: " + m[Matrix.MTRANS_Y]); } }
No comments:
Post a Comment