├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── lib-cropview ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── jeanboy │ │ └── cropview │ │ ├── CropImageView.java │ │ ├── animation │ │ ├── SimpleValueAnimator.java │ │ ├── SimpleValueAnimatorListener.java │ │ ├── ValueAnimatorV14.java │ │ └── ValueAnimatorV8.java │ │ ├── callback │ │ ├── Callback.java │ │ ├── CropCallback.java │ │ ├── LoadCallback.java │ │ └── SaveCallback.java │ │ ├── cropper │ │ ├── CropActivity.java │ │ ├── CropperHandler.java │ │ ├── CropperManager.java │ │ ├── CropperParams.java │ │ └── LoadingDialog.java │ │ └── util │ │ ├── Logger.java │ │ └── Utils.java │ └── res │ ├── drawable-v21 │ ├── ic_crop_done.xml │ ├── ic_crop_rotate_left.xml │ ├── ic_crop_rotate_right.xml │ └── ic_select_photo.xml │ ├── drawable │ ├── ic_crop_done.xml │ ├── ic_crop_rotate_left.xml │ ├── ic_crop_rotate_right.xml │ └── ic_select_photo.xml │ ├── layout │ ├── activity_crop.xml │ └── dialog_loading.xml │ ├── values-v21 │ └── styles.xml │ └── values │ ├── attrs_crop_image_view.xml │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── resource └── ScreenRecord.gif ├── settings.gradle └── simple ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── jeanboy │ └── cropper │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── jeanboy │ │ └── cropper │ │ └── MainActivity.java └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── jeanboy └── cropper └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | /local.properties 4 | .DS_Store 5 | /build 6 | /gradle 7 | /captures 8 | /gradlew 9 | /gradlew.bat 10 | /library-photocropper/build 11 | /library-crop/build 12 | *.apk 13 | *.iml 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CropView 2 | 3 | ![](https://img.shields.io/badge/platform-Android-brightgreen.svg) ![](https://img.shields.io/badge/language-java-yellow.svg) ![](https://img.shields.io/badge/license-Apache--2.0-blue.svg) 4 | 5 | ------ 6 | 7 | 8 | 9 | ## 介绍 10 | 11 | android image cropping library. Support from camera, gallery, image rotate. 12 | 13 | Android的图像裁剪库。支持从相机,图库选择图片,裁切时图像旋转。 14 | 15 | ## 使用 16 | 17 | * 导入lib-cropview并在AndroidManifest.xml中添加Activity 18 | ```java 19 | 21 | ``` 22 | 23 | * 实现CropperHandler并实现方法 24 | ```java 25 | @Override 26 | public Activity getActivity() { 27 | return this; 28 | } 29 | 30 | @Override 31 | public CropperParams getParams() { 32 | //配置裁切框比例 33 | return new CropperParams(1, 1); 34 | //不约束裁切比例 35 | // return new CropperParams(0, 0); 36 | } 37 | 38 | @Override 39 | public void onCropped(Uri uri) { 40 | Log.d("=====onCropped======", "======裁切成功=======" + uri); 41 | } 42 | 43 | @Override 44 | public void onCropCancel() { 45 | Log.d("=====onCropCancel====", "======裁切取消====="); 46 | } 47 | 48 | @Override 49 | public void onCropFailed(String msg) { 50 | Log.d("=====onCropFailed===", "=======裁切失败======" + msg); 51 | } 52 | ``` 53 | 54 | * 初始化CropperManager 55 | 56 | ```java 57 | @Override 58 | protected void onCreate(Bundle savedInstanceState) { 59 | super.onCreate(savedInstanceState); 60 | 61 | CropperManager.getInstance().build(this); 62 | } 63 | 64 | 65 | @Override 66 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 67 | super.onActivityResult(requestCode, resultCode, data); 68 | CropperManager.getInstance().handlerResult(requestCode, resultCode, data); 69 | } 70 | 71 | @Override 72 | protected void onDestroy() { 73 | super.onDestroy(); 74 | CropperManager.getInstance().destroy(); 75 | } 76 | ``` 77 | 78 | * 调用操作 79 | ```java 80 | CropperManager.getInstance().pickFromCamera();//拍照裁切 81 | CropperManager.getInstance().pickFromGallery();//图库选择裁切 82 | ``` 83 | ## 注意事项 84 | CropActivity不需要ActionBar 85 | ```java 86 | 92 | ``` 93 | 94 | 95 | ## Demo 96 | 97 | ![演示][1] 98 | 99 | ## 感谢 100 | 101 | * [IsseiAoki/SimpleCropView](https://github.com/IsseiAoki/SimpleCropView) 102 | 103 | ## 关于我 104 | 105 | 如果对你有帮助,请 star 一下,然后 follow 我,给我增加一下分享动力,谢谢! 106 | 107 | 如果你有什么疑问或者问题,可以提交 issue 和 request,发邮件给我 jeanboy@foxmail.com 。 108 | 109 | 或者加入下面的 QQ 群来一起学习交流。 110 | 111 | Android技术进阶:386463747 112 | 113 | ## License 114 | 115 | Copyright 2015 jeanboy 116 | 117 | Licensed under the Apache License, Version 2.0 (the "License"); 118 | you may not use this file except in compliance with the License. 119 | You may obtain a copy of the License at 120 | 121 | http://www.apache.org/licenses/LICENSE-2.0 122 | 123 | Unless required by applicable law or agreed to in writing, software 124 | distributed under the License is distributed on an "AS IS" BASIS, 125 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 126 | See the License for the specific language governing permissions and 127 | limitations under the License. 128 | 129 | [1]: https://github.com/freekite/CropView/blob/master/resource/ScreenRecord.gif 130 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.3' 9 | // NOTE: Do not place your application dependencies here; they belong 10 | // in the individual module build.gradle files 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | jcenter() 17 | maven { url 'http://oss.jfrog.org/artifactory/oss-snapshot-local/' } 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /lib-cropview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lib-cropview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | 7 | defaultConfig { 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | compile fileTree(dir: 'libs', include: ['*.jar']) 23 | compile "com.android.support:appcompat-v7:25.3.1" 24 | compile "com.android.support:design:25.3.1" 25 | } -------------------------------------------------------------------------------- /lib-cropview/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Applications/android-sdk/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /lib-cropview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/CropImageView.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Bitmap; 6 | import android.graphics.BitmapFactory; 7 | import android.graphics.BitmapRegionDecoder; 8 | import android.graphics.Canvas; 9 | import android.graphics.Matrix; 10 | import android.graphics.Paint; 11 | import android.graphics.Path; 12 | import android.graphics.PointF; 13 | import android.graphics.PorterDuff; 14 | import android.graphics.PorterDuffXfermode; 15 | import android.graphics.Rect; 16 | import android.graphics.RectF; 17 | import android.graphics.drawable.BitmapDrawable; 18 | import android.graphics.drawable.Drawable; 19 | import android.net.Uri; 20 | import android.os.Build; 21 | import android.os.Handler; 22 | import android.os.Looper; 23 | import android.os.Parcel; 24 | import android.os.Parcelable; 25 | import android.util.AttributeSet; 26 | import android.util.DisplayMetrics; 27 | import android.view.MotionEvent; 28 | import android.view.WindowManager; 29 | import android.view.animation.DecelerateInterpolator; 30 | import android.view.animation.Interpolator; 31 | import android.widget.ImageView; 32 | 33 | import com.jeanboy.cropview.R; 34 | import com.jeanboy.cropview.animation.SimpleValueAnimator; 35 | import com.jeanboy.cropview.animation.SimpleValueAnimatorListener; 36 | import com.jeanboy.cropview.animation.ValueAnimatorV14; 37 | import com.jeanboy.cropview.animation.ValueAnimatorV8; 38 | import com.jeanboy.cropview.callback.Callback; 39 | import com.jeanboy.cropview.callback.CropCallback; 40 | import com.jeanboy.cropview.callback.LoadCallback; 41 | import com.jeanboy.cropview.callback.SaveCallback; 42 | import com.jeanboy.cropview.util.Logger; 43 | import com.jeanboy.cropview.util.Utils; 44 | 45 | import java.io.IOException; 46 | import java.io.InputStream; 47 | import java.io.OutputStream; 48 | import java.util.concurrent.ExecutorService; 49 | import java.util.concurrent.Executors; 50 | 51 | @SuppressWarnings("unused") 52 | public class CropImageView extends ImageView { 53 | private static final String TAG = CropImageView.class.getSimpleName(); 54 | 55 | // Constants /////////////////////////////////////////////////////////////////////////////////// 56 | 57 | private static final int HANDLE_SIZE_IN_DP = 14; 58 | private static final int MIN_FRAME_SIZE_IN_DP = 50; 59 | private static final int FRAME_STROKE_WEIGHT_IN_DP = 1; 60 | private static final int GUIDE_STROKE_WEIGHT_IN_DP = 1; 61 | private static final float DEFAULT_INITIAL_FRAME_SCALE = 1f; 62 | private static final int DEFAULT_ANIMATION_DURATION_MILLIS = 100; 63 | private static final int DEBUG_TEXT_SIZE_IN_DP = 15; 64 | 65 | private static final int TRANSPARENT = 0x00000000; 66 | private static final int TRANSLUCENT_WHITE = 0xBBFFFFFF; 67 | private static final int WHITE = 0xFFFFFFFF; 68 | private static final int TRANSLUCENT_BLACK = 0xBB000000; 69 | 70 | // Member variables //////////////////////////////////////////////////////////////////////////// 71 | 72 | private int mViewWidth = 0; 73 | private int mViewHeight = 0; 74 | private float mScale = 1.0f; 75 | private float mAngle = 0.0f; 76 | private float mImgWidth = 0.0f; 77 | private float mImgHeight = 0.0f; 78 | 79 | private boolean mIsInitialized = false; 80 | private Matrix mMatrix = null; 81 | private Paint mPaintTranslucent; 82 | private Paint mPaintFrame; 83 | private Paint mPaintBitmap; 84 | private Paint mPaintDebug; 85 | private RectF mFrameRect; 86 | private RectF mImageRect; 87 | private PointF mCenter = new PointF(); 88 | private float mLastX, mLastY; 89 | private boolean mIsRotating = false; 90 | private boolean mIsAnimating = false; 91 | private SimpleValueAnimator mAnimator = null; 92 | private final Interpolator DEFAULT_INTERPOLATOR = new DecelerateInterpolator(); 93 | private Interpolator mInterpolator = DEFAULT_INTERPOLATOR; 94 | private LoadCallback mLoadCallback = null; 95 | private CropCallback mCropCallback = null; 96 | private SaveCallback mSaveCallback = null; 97 | private ExecutorService mExecutor; 98 | private Handler mHandler = new Handler(Looper.getMainLooper()); 99 | private Uri mSourceUri = null; 100 | private Uri mSaveUri = null; 101 | private int mExifRotation = 0; 102 | private int mOutputMaxWidth; 103 | private int mOutputMaxHeight; 104 | private int mOutputWidth = 0; 105 | private int mOutputHeight = 0; 106 | private boolean mIsDebug = false; 107 | private boolean mIsCropping = false; 108 | private Bitmap.CompressFormat mCompressFormat = Bitmap.CompressFormat.PNG; 109 | private int mCompressQuality = 100; 110 | private int mInputImageWidth = 0; 111 | private int mInputImageHeight = 0; 112 | private int mOutputImageWidth = 0; 113 | private int mOutputImageHeight = 0; 114 | private boolean mIsLoading = false; 115 | // Instance variables for customizable attributes ////////////////////////////////////////////// 116 | 117 | private TouchArea mTouchArea = TouchArea.OUT_OF_BOUNDS; 118 | 119 | private CropMode mCropMode = CropMode.SQUARE; 120 | private ShowMode mGuideShowMode = ShowMode.SHOW_ALWAYS; 121 | private ShowMode mHandleShowMode = ShowMode.SHOW_ALWAYS; 122 | private float mMinFrameSize; 123 | private int mHandleSize; 124 | private int mTouchPadding = 0; 125 | private boolean mShowGuide = true; 126 | private boolean mShowHandle = true; 127 | private boolean mIsCropEnabled = true; 128 | private boolean mIsEnabled = true; 129 | private PointF mCustomRatio = new PointF(1.0f, 1.0f); 130 | private float mFrameStrokeWeight = 2.0f; 131 | private float mGuideStrokeWeight = 2.0f; 132 | private int mBackgroundColor; 133 | private int mOverlayColor; 134 | private int mFrameColor; 135 | private int mHandleColor; 136 | private int mGuideColor; 137 | private float mInitialFrameScale; // 0.01 ~ 1.0, 0.75 is default value 138 | private boolean mIsAnimationEnabled = true; 139 | private int mAnimationDurationMillis = DEFAULT_ANIMATION_DURATION_MILLIS; 140 | private boolean mIsHandleShadowEnabled = true; 141 | 142 | // Constructor ///////////////////////////////////////////////////////////////////////////////// 143 | 144 | public CropImageView(Context context) { 145 | this(context, null); 146 | } 147 | 148 | public CropImageView(Context context, AttributeSet attrs) { 149 | this(context, attrs, 0); 150 | } 151 | 152 | public CropImageView(Context context, AttributeSet attrs, int defStyle) { 153 | super(context, attrs, defStyle); 154 | 155 | mExecutor = Executors.newSingleThreadExecutor(); 156 | float density = getDensity(); 157 | mHandleSize = (int) (density * HANDLE_SIZE_IN_DP); 158 | mMinFrameSize = density * MIN_FRAME_SIZE_IN_DP; 159 | mFrameStrokeWeight = density * FRAME_STROKE_WEIGHT_IN_DP; 160 | mGuideStrokeWeight = density * GUIDE_STROKE_WEIGHT_IN_DP; 161 | 162 | mPaintFrame = new Paint(); 163 | mPaintTranslucent = new Paint(); 164 | mPaintBitmap = new Paint(); 165 | mPaintBitmap.setFilterBitmap(true); 166 | mPaintDebug = new Paint(); 167 | mPaintDebug.setAntiAlias(true); 168 | mPaintDebug.setStyle(Paint.Style.STROKE); 169 | mPaintDebug.setColor(WHITE); 170 | mPaintDebug.setTextSize((float) DEBUG_TEXT_SIZE_IN_DP * density); 171 | 172 | mMatrix = new Matrix(); 173 | mScale = 1.0f; 174 | mBackgroundColor = TRANSPARENT; 175 | mFrameColor = WHITE; 176 | mOverlayColor = TRANSLUCENT_BLACK; 177 | mHandleColor = WHITE; 178 | mGuideColor = TRANSLUCENT_WHITE; 179 | 180 | // handle Styleable 181 | handleStyleable(context, attrs, defStyle, density); 182 | } 183 | 184 | // Lifecycle methods /////////////////////////////////////////////////////////////////////////// 185 | 186 | @Override 187 | public Parcelable onSaveInstanceState() { 188 | Parcelable superState = super.onSaveInstanceState(); 189 | SavedState ss = new SavedState(superState); 190 | ss.image = getBitmap(); 191 | ss.mode = this.mCropMode; 192 | ss.backgroundColor = this.mBackgroundColor; 193 | ss.overlayColor = this.mOverlayColor; 194 | ss.frameColor = this.mFrameColor; 195 | ss.guideShowMode = this.mGuideShowMode; 196 | ss.handleShowMode = this.mHandleShowMode; 197 | ss.showGuide = this.mShowGuide; 198 | ss.showHandle = this.mShowHandle; 199 | ss.handleSize = this.mHandleSize; 200 | ss.touchPadding = this.mTouchPadding; 201 | ss.minFrameSize = this.mMinFrameSize; 202 | ss.customRatioX = this.mCustomRatio.x; 203 | ss.customRatioY = this.mCustomRatio.y; 204 | ss.frameStrokeWeight = this.mFrameStrokeWeight; 205 | ss.guideStrokeWeight = this.mGuideStrokeWeight; 206 | ss.isCropEnabled = this.mIsCropEnabled; 207 | ss.handleColor = this.mHandleColor; 208 | ss.guideColor = this.mGuideColor; 209 | ss.initialFrameScale = this.mInitialFrameScale; 210 | ss.angle = this.mAngle; 211 | ss.isAnimationEnabled = this.mIsAnimationEnabled; 212 | ss.animationDuration = this.mAnimationDurationMillis; 213 | ss.exifRotation = this.mExifRotation; 214 | ss.sourceUri = this.mSourceUri; 215 | ss.saveUri = this.mSaveUri; 216 | ss.compressFormat = this.mCompressFormat; 217 | ss.compressQuality = this.mCompressQuality; 218 | ss.isDebug = this.mIsDebug; 219 | ss.outputMaxWidth = this.mOutputMaxWidth; 220 | ss.outputMaxHeight = this.mOutputMaxHeight; 221 | ss.outputWidth = this.mOutputWidth; 222 | ss.outputHeight = this.mOutputHeight; 223 | ss.isHandleShadowEnabled = this.mIsHandleShadowEnabled; 224 | ss.inputImageWidth = this.mInputImageWidth; 225 | ss.inputImageHeight = this.mInputImageHeight; 226 | ss.outputImageWidth = this.mOutputImageWidth; 227 | ss.outputImageHeight = this.mOutputImageHeight; 228 | return ss; 229 | } 230 | 231 | @Override 232 | public void onRestoreInstanceState(Parcelable state) { 233 | SavedState ss = (SavedState) state; 234 | super.onRestoreInstanceState(ss.getSuperState()); 235 | this.mCropMode = ss.mode; 236 | this.mBackgroundColor = ss.backgroundColor; 237 | this.mOverlayColor = ss.overlayColor; 238 | this.mFrameColor = ss.frameColor; 239 | this.mGuideShowMode = ss.guideShowMode; 240 | this.mHandleShowMode = ss.handleShowMode; 241 | this.mShowGuide = ss.showGuide; 242 | this.mShowHandle = ss.showHandle; 243 | this.mHandleSize = ss.handleSize; 244 | this.mTouchPadding = ss.touchPadding; 245 | this.mMinFrameSize = ss.minFrameSize; 246 | this.mCustomRatio = new PointF(ss.customRatioX, ss.customRatioY); 247 | this.mFrameStrokeWeight = ss.frameStrokeWeight; 248 | this.mGuideStrokeWeight = ss.guideStrokeWeight; 249 | this.mIsCropEnabled = ss.isCropEnabled; 250 | this.mHandleColor = ss.handleColor; 251 | this.mGuideColor = ss.guideColor; 252 | this.mInitialFrameScale = ss.initialFrameScale; 253 | this.mAngle = ss.angle; 254 | this.mIsAnimationEnabled = ss.isAnimationEnabled; 255 | this.mAnimationDurationMillis = ss.animationDuration; 256 | this.mExifRotation = ss.exifRotation; 257 | this.mSourceUri = ss.sourceUri; 258 | this.mSaveUri = ss.saveUri; 259 | this.mCompressFormat = ss.compressFormat; 260 | this.mCompressQuality = ss.compressQuality; 261 | this.mIsDebug = ss.isDebug; 262 | this.mOutputMaxWidth = ss.outputMaxWidth; 263 | this.mOutputMaxHeight = ss.outputMaxHeight; 264 | this.mOutputWidth = ss.outputWidth; 265 | this.mOutputHeight = ss.outputHeight; 266 | this.mIsHandleShadowEnabled = ss.isHandleShadowEnabled; 267 | this.mInputImageWidth = ss.inputImageWidth; 268 | this.mInputImageHeight = ss.inputImageHeight; 269 | this.mOutputImageWidth = ss.outputImageWidth; 270 | this.mOutputImageHeight = ss.outputImageHeight; 271 | setImageBitmap(ss.image); 272 | requestLayout(); 273 | } 274 | 275 | @Override 276 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 277 | final int viewWidth = MeasureSpec.getSize(widthMeasureSpec); 278 | final int viewHeight = MeasureSpec.getSize(heightMeasureSpec); 279 | 280 | setMeasuredDimension(viewWidth, viewHeight); 281 | 282 | mViewWidth = viewWidth - getPaddingLeft() - getPaddingRight(); 283 | mViewHeight = viewHeight - getPaddingTop() - getPaddingBottom(); 284 | } 285 | 286 | @Override 287 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 288 | if (getDrawable() != null) setupLayout(mViewWidth, mViewHeight); 289 | } 290 | 291 | @Override 292 | public void onDraw(Canvas canvas) { 293 | canvas.drawColor(mBackgroundColor); 294 | 295 | if (mIsInitialized) { 296 | setMatrix(); 297 | Bitmap bm = getBitmap(); 298 | if (bm != null) { 299 | canvas.drawBitmap(bm, mMatrix, mPaintBitmap); 300 | // draw edit frame 301 | drawCropFrame(canvas); 302 | } 303 | if (mIsDebug) { 304 | drawDebugInfo(canvas); 305 | } 306 | } 307 | } 308 | 309 | @Override 310 | protected void onDetachedFromWindow() { 311 | mExecutor.shutdown(); 312 | super.onDetachedFromWindow(); 313 | } 314 | 315 | // Handle styleable //////////////////////////////////////////////////////////////////////////// 316 | 317 | private void handleStyleable(Context context, AttributeSet attrs, int defStyle, 318 | float mDensity) { 319 | TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.scv_CropImageView, 320 | defStyle, 0); 321 | Drawable drawable; 322 | mCropMode = CropMode.SQUARE; 323 | try { 324 | drawable = ta.getDrawable(R.styleable.scv_CropImageView_scv_img_src); 325 | if (drawable != null) setImageDrawable(drawable); 326 | for (CropMode mode : CropMode.values()) { 327 | if (ta.getInt(R.styleable.scv_CropImageView_scv_crop_mode, 3) == mode.getId()) { 328 | mCropMode = mode; 329 | break; 330 | } 331 | } 332 | mBackgroundColor = ta.getColor(R.styleable.scv_CropImageView_scv_background_color, 333 | TRANSPARENT); 334 | mOverlayColor = ta.getColor(R.styleable.scv_CropImageView_scv_overlay_color, 335 | TRANSLUCENT_BLACK); 336 | mFrameColor = ta.getColor(R.styleable.scv_CropImageView_scv_frame_color, WHITE); 337 | mHandleColor = ta.getColor(R.styleable.scv_CropImageView_scv_handle_color, WHITE); 338 | mGuideColor = ta.getColor(R.styleable.scv_CropImageView_scv_guide_color, 339 | TRANSLUCENT_WHITE); 340 | for (ShowMode mode : ShowMode.values()) { 341 | if (ta.getInt(R.styleable.scv_CropImageView_scv_guide_show_mode, 342 | 1) == mode.getId()) { 343 | mGuideShowMode = mode; 344 | break; 345 | } 346 | } 347 | 348 | for (ShowMode mode : ShowMode.values()) { 349 | if (ta.getInt(R.styleable.scv_CropImageView_scv_handle_show_mode, 350 | 1) == mode.getId()) { 351 | mHandleShowMode = mode; 352 | break; 353 | } 354 | } 355 | setGuideShowMode(mGuideShowMode); 356 | setHandleShowMode(mHandleShowMode); 357 | mHandleSize = ta.getDimensionPixelSize(R.styleable.scv_CropImageView_scv_handle_size, 358 | (int) (HANDLE_SIZE_IN_DP * mDensity)); 359 | mTouchPadding = ta.getDimensionPixelSize( 360 | R.styleable.scv_CropImageView_scv_touch_padding, 0); 361 | mMinFrameSize = ta.getDimensionPixelSize( 362 | R.styleable.scv_CropImageView_scv_min_frame_size, 363 | (int) (MIN_FRAME_SIZE_IN_DP * mDensity)); 364 | mFrameStrokeWeight = ta.getDimensionPixelSize( 365 | R.styleable.scv_CropImageView_scv_frame_stroke_weight, 366 | (int) (FRAME_STROKE_WEIGHT_IN_DP * mDensity)); 367 | mGuideStrokeWeight = ta.getDimensionPixelSize( 368 | R.styleable.scv_CropImageView_scv_guide_stroke_weight, 369 | (int) (GUIDE_STROKE_WEIGHT_IN_DP * mDensity)); 370 | mIsCropEnabled = ta.getBoolean(R.styleable.scv_CropImageView_scv_crop_enabled, true); 371 | mInitialFrameScale = constrain( 372 | ta.getFloat(R.styleable.scv_CropImageView_scv_initial_frame_scale, 373 | DEFAULT_INITIAL_FRAME_SCALE), 0.01f, 1.0f, 374 | DEFAULT_INITIAL_FRAME_SCALE); 375 | mIsAnimationEnabled = ta.getBoolean(R.styleable.scv_CropImageView_scv_animation_enabled, 376 | true); 377 | mAnimationDurationMillis = ta.getInt( 378 | R.styleable.scv_CropImageView_scv_animation_duration, 379 | DEFAULT_ANIMATION_DURATION_MILLIS); 380 | mIsHandleShadowEnabled = ta.getBoolean(R.styleable.scv_CropImageView_scv_handle_shadow_enabled, true); 381 | } catch (Exception e) { 382 | e.printStackTrace(); 383 | } finally { 384 | ta.recycle(); 385 | } 386 | } 387 | 388 | // Drawing method ////////////////////////////////////////////////////////////////////////////// 389 | 390 | private void drawDebugInfo(Canvas canvas) { 391 | Paint.FontMetrics fontMetrics = mPaintDebug.getFontMetrics(); 392 | mPaintDebug.measureText("W"); 393 | int textHeight = (int) (fontMetrics.descent - fontMetrics.ascent); 394 | int x = (int) (mImageRect.left + (float) mHandleSize * 0.5f * getDensity()); 395 | int y = (int) (mImageRect.top + textHeight + (float) mHandleSize * 0.5f * getDensity()); 396 | StringBuilder builder = new StringBuilder(); 397 | builder.append("LOADED FROM: ") 398 | .append(mSourceUri != null ? "Uri" : "Bitmap"); 399 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 400 | builder = new StringBuilder(); 401 | 402 | if (mSourceUri == null) { 403 | builder.append("INPUT_IMAGE_SIZE: ") 404 | .append((int) mImgWidth) 405 | .append("x") 406 | .append((int) mImgHeight); 407 | y += textHeight; 408 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 409 | builder = new StringBuilder(); 410 | } else { 411 | builder = new StringBuilder() 412 | .append("INPUT_IMAGE_SIZE: ") 413 | .append(mInputImageWidth) 414 | .append("x") 415 | .append(mInputImageHeight); 416 | y += textHeight; 417 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 418 | builder = new StringBuilder(); 419 | } 420 | builder.append("LOADED_IMAGE_SIZE: ") 421 | .append(getBitmap().getWidth()) 422 | .append("x") 423 | .append(getBitmap().getHeight()); 424 | y += textHeight; 425 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 426 | builder = new StringBuilder(); 427 | if (mOutputImageWidth > 0 && mOutputImageHeight > 0) { 428 | builder.append("OUTPUT_IMAGE_SIZE: ") 429 | .append(mOutputImageWidth) 430 | .append("x") 431 | .append(mOutputImageHeight); 432 | y += textHeight; 433 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 434 | builder = new StringBuilder() 435 | .append("EXIF ROTATION: ") 436 | .append(mExifRotation); 437 | y += textHeight; 438 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 439 | builder = new StringBuilder() 440 | .append("CURRENT_ROTATION: ") 441 | .append((int) mAngle); 442 | y += textHeight; 443 | canvas.drawText(builder.toString(), x, y, mPaintDebug); 444 | } 445 | } 446 | 447 | private void drawCropFrame(Canvas canvas) { 448 | if (!mIsCropEnabled) return; 449 | if (mIsRotating) return; 450 | drawOverlay(canvas); 451 | drawFrame(canvas); 452 | if (mShowGuide) drawGuidelines(canvas); 453 | if (mShowHandle) drawHandles(canvas); 454 | } 455 | 456 | private void drawOverlay(Canvas canvas) { 457 | mPaintTranslucent.setAntiAlias(true); 458 | mPaintTranslucent.setFilterBitmap(true); 459 | mPaintTranslucent.setColor(mOverlayColor); 460 | mPaintTranslucent.setStyle(Paint.Style.FILL); 461 | Path path = new Path(); 462 | RectF overlayRect = new RectF((float) Math.floor(mImageRect.left), 463 | (float) Math.floor(mImageRect.top), 464 | (float) Math.ceil(mImageRect.right), 465 | (float) Math.ceil(mImageRect.bottom)); 466 | if (!mIsAnimating 467 | && (mCropMode == CropMode.CIRCLE || mCropMode == CropMode.CIRCLE_SQUARE)) { 468 | path.addRect( 469 | overlayRect, 470 | Path.Direction.CW 471 | ); 472 | PointF circleCenter = new PointF((mFrameRect.left + mFrameRect.right) / 2, 473 | (mFrameRect.top + mFrameRect.bottom) / 2); 474 | float circleRadius = (mFrameRect.right - mFrameRect.left) / 2; 475 | path.addCircle(circleCenter.x, circleCenter.y, circleRadius, Path.Direction.CCW); 476 | canvas.drawPath(path, mPaintTranslucent); 477 | } else { 478 | path.addRect( 479 | overlayRect, 480 | Path.Direction.CW 481 | ); 482 | path.addRect(mFrameRect, Path.Direction.CCW); 483 | canvas.drawPath(path, mPaintTranslucent); 484 | } 485 | } 486 | 487 | private void drawFrame(Canvas canvas) { 488 | mPaintFrame.setAntiAlias(true); 489 | mPaintFrame.setFilterBitmap(true); 490 | mPaintFrame.setStyle(Paint.Style.STROKE); 491 | mPaintFrame.setColor(mFrameColor); 492 | mPaintFrame.setStrokeWidth(mFrameStrokeWeight); 493 | canvas.drawRect(mFrameRect, mPaintFrame); 494 | } 495 | 496 | private void drawGuidelines(Canvas canvas) { 497 | mPaintFrame.setColor(mGuideColor); 498 | mPaintFrame.setStrokeWidth(mGuideStrokeWeight); 499 | float h1 = mFrameRect.left + (mFrameRect.right - mFrameRect.left) / 3.0f; 500 | float h2 = mFrameRect.right - (mFrameRect.right - mFrameRect.left) / 3.0f; 501 | float v1 = mFrameRect.top + (mFrameRect.bottom - mFrameRect.top) / 3.0f; 502 | float v2 = mFrameRect.bottom - (mFrameRect.bottom - mFrameRect.top) / 3.0f; 503 | canvas.drawLine(h1, mFrameRect.top, h1, mFrameRect.bottom, mPaintFrame); 504 | canvas.drawLine(h2, mFrameRect.top, h2, mFrameRect.bottom, mPaintFrame); 505 | canvas.drawLine(mFrameRect.left, v1, mFrameRect.right, v1, mPaintFrame); 506 | canvas.drawLine(mFrameRect.left, v2, mFrameRect.right, v2, mPaintFrame); 507 | } 508 | 509 | private void drawHandles(Canvas canvas) { 510 | if (mIsHandleShadowEnabled) drawHandleShadows(canvas); 511 | mPaintFrame.setStyle(Paint.Style.FILL); 512 | mPaintFrame.setColor(mHandleColor); 513 | canvas.drawCircle(mFrameRect.left, mFrameRect.top, mHandleSize, mPaintFrame); 514 | canvas.drawCircle(mFrameRect.right, mFrameRect.top, mHandleSize, mPaintFrame); 515 | canvas.drawCircle(mFrameRect.left, mFrameRect.bottom, mHandleSize, mPaintFrame); 516 | canvas.drawCircle(mFrameRect.right, mFrameRect.bottom, mHandleSize, mPaintFrame); 517 | } 518 | 519 | private void drawHandleShadows(Canvas canvas) { 520 | mPaintFrame.setStyle(Paint.Style.FILL); 521 | mPaintFrame.setColor(TRANSLUCENT_BLACK); 522 | RectF rect = new RectF(mFrameRect); 523 | rect.offset(0, 1); 524 | canvas.drawCircle(rect.left, rect.top, mHandleSize, mPaintFrame); 525 | canvas.drawCircle(rect.right, rect.top, mHandleSize, mPaintFrame); 526 | canvas.drawCircle(rect.left, rect.bottom, mHandleSize, mPaintFrame); 527 | canvas.drawCircle(rect.right, rect.bottom, mHandleSize, mPaintFrame); 528 | } 529 | 530 | private void setMatrix() { 531 | mMatrix.reset(); 532 | mMatrix.setTranslate(mCenter.x - mImgWidth * 0.5f, mCenter.y - mImgHeight * 0.5f); 533 | mMatrix.postScale(mScale, mScale, mCenter.x, mCenter.y); 534 | mMatrix.postRotate(mAngle, mCenter.x, mCenter.y); 535 | } 536 | 537 | // Layout calculation ////////////////////////////////////////////////////////////////////////// 538 | 539 | private void setupLayout(int viewW, int viewH) { 540 | if (viewW == 0 || viewH == 0) return; 541 | setCenter(new PointF(getPaddingLeft() + viewW * 0.5f, getPaddingTop() + viewH * 0.5f)); 542 | setScale(calcScale(viewW, viewH, mAngle)); 543 | setMatrix(); 544 | mImageRect = calcImageRect(new RectF(0f, 0f, mImgWidth, mImgHeight), mMatrix); 545 | mFrameRect = calcFrameRect(mImageRect); 546 | mIsInitialized = true; 547 | invalidate(); 548 | } 549 | 550 | private float calcScale(int viewW, int viewH, float angle) { 551 | mImgWidth = getDrawable().getIntrinsicWidth(); 552 | mImgHeight = getDrawable().getIntrinsicHeight(); 553 | if (mImgWidth <= 0) mImgWidth = viewW; 554 | if (mImgHeight <= 0) mImgHeight = viewH; 555 | float viewRatio = (float) viewW / (float) viewH; 556 | float imgRatio = getRotatedWidth(angle) / getRotatedHeight(angle); 557 | float scale = 1.0f; 558 | if (imgRatio >= viewRatio) { 559 | scale = viewW / getRotatedWidth(angle); 560 | } else if (imgRatio < viewRatio) { 561 | scale = viewH / getRotatedHeight(angle); 562 | } 563 | return scale; 564 | } 565 | 566 | private RectF calcImageRect(RectF rect, Matrix matrix) { 567 | RectF applied = new RectF(); 568 | matrix.mapRect(applied, rect); 569 | return applied; 570 | } 571 | 572 | private RectF calcFrameRect(RectF imageRect) { 573 | float frameW = getRatioX(imageRect.width()); 574 | float frameH = getRatioY(imageRect.height()); 575 | float imgRatio = imageRect.width() / imageRect.height(); 576 | float frameRatio = frameW / frameH; 577 | float l = imageRect.left, t = imageRect.top, r = imageRect.right, b = imageRect.bottom; 578 | if (frameRatio >= imgRatio) { 579 | l = imageRect.left; 580 | r = imageRect.right; 581 | float hy = (imageRect.top + imageRect.bottom) * 0.5f; 582 | float hh = (imageRect.width() / frameRatio) * 0.5f; 583 | t = hy - hh; 584 | b = hy + hh; 585 | } else if (frameRatio < imgRatio) { 586 | t = imageRect.top; 587 | b = imageRect.bottom; 588 | float hx = (imageRect.left + imageRect.right) * 0.5f; 589 | float hw = imageRect.height() * frameRatio * 0.5f; 590 | l = hx - hw; 591 | r = hx + hw; 592 | } 593 | float w = r - l; 594 | float h = b - t; 595 | float cx = l + w / 2; 596 | float cy = t + h / 2; 597 | float sw = w * mInitialFrameScale; 598 | float sh = h * mInitialFrameScale; 599 | return new RectF(cx - sw / 2, cy - sh / 2, cx + sw / 2, cy + sh / 2); 600 | } 601 | 602 | // Touch Event ///////////////////////////////////////////////////////////////////////////////// 603 | 604 | @Override 605 | public boolean onTouchEvent(MotionEvent event) { 606 | if (!mIsInitialized) return false; 607 | if (!mIsCropEnabled) return false; 608 | if (!mIsEnabled) return false; 609 | if (mIsRotating) return false; 610 | if (mIsAnimating) return false; 611 | if (mIsLoading) return false; 612 | if (mIsCropping) return false; 613 | switch (event.getAction()) { 614 | case MotionEvent.ACTION_DOWN: 615 | onDown(event); 616 | return true; 617 | case MotionEvent.ACTION_MOVE: 618 | onMove(event); 619 | if (mTouchArea != TouchArea.OUT_OF_BOUNDS) { 620 | getParent().requestDisallowInterceptTouchEvent(true); 621 | } 622 | return true; 623 | case MotionEvent.ACTION_CANCEL: 624 | getParent().requestDisallowInterceptTouchEvent(false); 625 | onCancel(); 626 | return true; 627 | case MotionEvent.ACTION_UP: 628 | getParent().requestDisallowInterceptTouchEvent(false); 629 | onUp(event); 630 | return true; 631 | } 632 | return false; 633 | } 634 | 635 | 636 | private void onDown(MotionEvent e) { 637 | invalidate(); 638 | mLastX = e.getX(); 639 | mLastY = e.getY(); 640 | checkTouchArea(e.getX(), e.getY()); 641 | } 642 | 643 | private void onMove(MotionEvent e) { 644 | float diffX = e.getX() - mLastX; 645 | float diffY = e.getY() - mLastY; 646 | switch (mTouchArea) { 647 | case CENTER: 648 | moveFrame(diffX, diffY); 649 | break; 650 | case LEFT_TOP: 651 | moveHandleLT(diffX, diffY); 652 | break; 653 | case RIGHT_TOP: 654 | moveHandleRT(diffX, diffY); 655 | break; 656 | case LEFT_BOTTOM: 657 | moveHandleLB(diffX, diffY); 658 | break; 659 | case RIGHT_BOTTOM: 660 | moveHandleRB(diffX, diffY); 661 | break; 662 | case OUT_OF_BOUNDS: 663 | break; 664 | } 665 | invalidate(); 666 | mLastX = e.getX(); 667 | mLastY = e.getY(); 668 | } 669 | 670 | private void onUp(MotionEvent e) { 671 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = false; 672 | if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = false; 673 | mTouchArea = TouchArea.OUT_OF_BOUNDS; 674 | invalidate(); 675 | } 676 | 677 | private void onCancel() { 678 | mTouchArea = TouchArea.OUT_OF_BOUNDS; 679 | invalidate(); 680 | } 681 | 682 | // Hit test //////////////////////////////////////////////////////////////////////////////////// 683 | 684 | private void checkTouchArea(float x, float y) { 685 | if (isInsideCornerLeftTop(x, y)) { 686 | mTouchArea = TouchArea.LEFT_TOP; 687 | if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true; 688 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true; 689 | return; 690 | } 691 | if (isInsideCornerRightTop(x, y)) { 692 | mTouchArea = TouchArea.RIGHT_TOP; 693 | if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true; 694 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true; 695 | return; 696 | } 697 | if (isInsideCornerLeftBottom(x, y)) { 698 | mTouchArea = TouchArea.LEFT_BOTTOM; 699 | if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true; 700 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true; 701 | return; 702 | } 703 | if (isInsideCornerRightBottom(x, y)) { 704 | mTouchArea = TouchArea.RIGHT_BOTTOM; 705 | if (mHandleShowMode == ShowMode.SHOW_ON_TOUCH) mShowHandle = true; 706 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true; 707 | return; 708 | } 709 | if (isInsideFrame(x, y)) { 710 | if (mGuideShowMode == ShowMode.SHOW_ON_TOUCH) mShowGuide = true; 711 | mTouchArea = TouchArea.CENTER; 712 | return; 713 | } 714 | mTouchArea = TouchArea.OUT_OF_BOUNDS; 715 | } 716 | 717 | private boolean isInsideFrame(float x, float y) { 718 | if (mFrameRect.left <= x && mFrameRect.right >= x) { 719 | if (mFrameRect.top <= y && mFrameRect.bottom >= y) { 720 | mTouchArea = TouchArea.CENTER; 721 | return true; 722 | } 723 | } 724 | return false; 725 | } 726 | 727 | private boolean isInsideCornerLeftTop(float x, float y) { 728 | float dx = x - mFrameRect.left; 729 | float dy = y - mFrameRect.top; 730 | float d = dx * dx + dy * dy; 731 | return sq(mHandleSize + mTouchPadding) >= d; 732 | } 733 | 734 | private boolean isInsideCornerRightTop(float x, float y) { 735 | float dx = x - mFrameRect.right; 736 | float dy = y - mFrameRect.top; 737 | float d = dx * dx + dy * dy; 738 | return sq(mHandleSize + mTouchPadding) >= d; 739 | } 740 | 741 | private boolean isInsideCornerLeftBottom(float x, float y) { 742 | float dx = x - mFrameRect.left; 743 | float dy = y - mFrameRect.bottom; 744 | float d = dx * dx + dy * dy; 745 | return sq(mHandleSize + mTouchPadding) >= d; 746 | } 747 | 748 | private boolean isInsideCornerRightBottom(float x, float y) { 749 | float dx = x - mFrameRect.right; 750 | float dy = y - mFrameRect.bottom; 751 | float d = dx * dx + dy * dy; 752 | return sq(mHandleSize + mTouchPadding) >= d; 753 | } 754 | 755 | // Adjust frame //////////////////////////////////////////////////////////////////////////////// 756 | 757 | private void moveFrame(float x, float y) { 758 | mFrameRect.left += x; 759 | mFrameRect.right += x; 760 | mFrameRect.top += y; 761 | mFrameRect.bottom += y; 762 | checkMoveBounds(); 763 | } 764 | 765 | @SuppressWarnings("UnnecessaryLocalVariable") 766 | private void moveHandleLT(float diffX, float diffY) { 767 | if (mCropMode == CropMode.FREE) { 768 | mFrameRect.left += diffX; 769 | mFrameRect.top += diffY; 770 | if (isWidthTooSmall()) { 771 | float offsetX = mMinFrameSize - getFrameW(); 772 | mFrameRect.left -= offsetX; 773 | } 774 | if (isHeightTooSmall()) { 775 | float offsetY = mMinFrameSize - getFrameH(); 776 | mFrameRect.top -= offsetY; 777 | } 778 | checkScaleBounds(); 779 | } else { 780 | float dx = diffX; 781 | float dy = diffX * getRatioY() / getRatioX(); 782 | mFrameRect.left += dx; 783 | mFrameRect.top += dy; 784 | if (isWidthTooSmall()) { 785 | float offsetX = mMinFrameSize - getFrameW(); 786 | mFrameRect.left -= offsetX; 787 | float offsetY = offsetX * getRatioY() / getRatioX(); 788 | mFrameRect.top -= offsetY; 789 | } 790 | if (isHeightTooSmall()) { 791 | float offsetY = mMinFrameSize - getFrameH(); 792 | mFrameRect.top -= offsetY; 793 | float offsetX = offsetY * getRatioX() / getRatioY(); 794 | mFrameRect.left -= offsetX; 795 | } 796 | float ox, oy; 797 | if (!isInsideHorizontal(mFrameRect.left)) { 798 | ox = mImageRect.left - mFrameRect.left; 799 | mFrameRect.left += ox; 800 | oy = ox * getRatioY() / getRatioX(); 801 | mFrameRect.top += oy; 802 | } 803 | if (!isInsideVertical(mFrameRect.top)) { 804 | oy = mImageRect.top - mFrameRect.top; 805 | mFrameRect.top += oy; 806 | ox = oy * getRatioX() / getRatioY(); 807 | mFrameRect.left += ox; 808 | } 809 | } 810 | } 811 | 812 | @SuppressWarnings("UnnecessaryLocalVariable") 813 | private void moveHandleRT(float diffX, float diffY) { 814 | if (mCropMode == CropMode.FREE) { 815 | mFrameRect.right += diffX; 816 | mFrameRect.top += diffY; 817 | if (isWidthTooSmall()) { 818 | float offsetX = mMinFrameSize - getFrameW(); 819 | mFrameRect.right += offsetX; 820 | } 821 | if (isHeightTooSmall()) { 822 | float offsetY = mMinFrameSize - getFrameH(); 823 | mFrameRect.top -= offsetY; 824 | } 825 | checkScaleBounds(); 826 | } else { 827 | float dx = diffX; 828 | float dy = diffX * getRatioY() / getRatioX(); 829 | mFrameRect.right += dx; 830 | mFrameRect.top -= dy; 831 | if (isWidthTooSmall()) { 832 | float offsetX = mMinFrameSize - getFrameW(); 833 | mFrameRect.right += offsetX; 834 | float offsetY = offsetX * getRatioY() / getRatioX(); 835 | mFrameRect.top -= offsetY; 836 | } 837 | if (isHeightTooSmall()) { 838 | float offsetY = mMinFrameSize - getFrameH(); 839 | mFrameRect.top -= offsetY; 840 | float offsetX = offsetY * getRatioX() / getRatioY(); 841 | mFrameRect.right += offsetX; 842 | } 843 | float ox, oy; 844 | if (!isInsideHorizontal(mFrameRect.right)) { 845 | ox = mFrameRect.right - mImageRect.right; 846 | mFrameRect.right -= ox; 847 | oy = ox * getRatioY() / getRatioX(); 848 | mFrameRect.top += oy; 849 | } 850 | if (!isInsideVertical(mFrameRect.top)) { 851 | oy = mImageRect.top - mFrameRect.top; 852 | mFrameRect.top += oy; 853 | ox = oy * getRatioX() / getRatioY(); 854 | mFrameRect.right -= ox; 855 | } 856 | } 857 | } 858 | 859 | @SuppressWarnings("UnnecessaryLocalVariable") 860 | private void moveHandleLB(float diffX, float diffY) { 861 | if (mCropMode == CropMode.FREE) { 862 | mFrameRect.left += diffX; 863 | mFrameRect.bottom += diffY; 864 | if (isWidthTooSmall()) { 865 | float offsetX = mMinFrameSize - getFrameW(); 866 | mFrameRect.left -= offsetX; 867 | } 868 | if (isHeightTooSmall()) { 869 | float offsetY = mMinFrameSize - getFrameH(); 870 | mFrameRect.bottom += offsetY; 871 | } 872 | checkScaleBounds(); 873 | } else { 874 | float dx = diffX; 875 | float dy = diffX * getRatioY() / getRatioX(); 876 | mFrameRect.left += dx; 877 | mFrameRect.bottom -= dy; 878 | if (isWidthTooSmall()) { 879 | float offsetX = mMinFrameSize - getFrameW(); 880 | mFrameRect.left -= offsetX; 881 | float offsetY = offsetX * getRatioY() / getRatioX(); 882 | mFrameRect.bottom += offsetY; 883 | } 884 | if (isHeightTooSmall()) { 885 | float offsetY = mMinFrameSize - getFrameH(); 886 | mFrameRect.bottom += offsetY; 887 | float offsetX = offsetY * getRatioX() / getRatioY(); 888 | mFrameRect.left -= offsetX; 889 | } 890 | float ox, oy; 891 | if (!isInsideHorizontal(mFrameRect.left)) { 892 | ox = mImageRect.left - mFrameRect.left; 893 | mFrameRect.left += ox; 894 | oy = ox * getRatioY() / getRatioX(); 895 | mFrameRect.bottom -= oy; 896 | } 897 | if (!isInsideVertical(mFrameRect.bottom)) { 898 | oy = mFrameRect.bottom - mImageRect.bottom; 899 | mFrameRect.bottom -= oy; 900 | ox = oy * getRatioX() / getRatioY(); 901 | mFrameRect.left += ox; 902 | } 903 | } 904 | } 905 | 906 | @SuppressWarnings("UnnecessaryLocalVariable") 907 | private void moveHandleRB(float diffX, float diffY) { 908 | if (mCropMode == CropMode.FREE) { 909 | mFrameRect.right += diffX; 910 | mFrameRect.bottom += diffY; 911 | if (isWidthTooSmall()) { 912 | float offsetX = mMinFrameSize - getFrameW(); 913 | mFrameRect.right += offsetX; 914 | } 915 | if (isHeightTooSmall()) { 916 | float offsetY = mMinFrameSize - getFrameH(); 917 | mFrameRect.bottom += offsetY; 918 | } 919 | checkScaleBounds(); 920 | } else { 921 | float dx = diffX; 922 | float dy = diffX * getRatioY() / getRatioX(); 923 | mFrameRect.right += dx; 924 | mFrameRect.bottom += dy; 925 | if (isWidthTooSmall()) { 926 | float offsetX = mMinFrameSize - getFrameW(); 927 | mFrameRect.right += offsetX; 928 | float offsetY = offsetX * getRatioY() / getRatioX(); 929 | mFrameRect.bottom += offsetY; 930 | } 931 | if (isHeightTooSmall()) { 932 | float offsetY = mMinFrameSize - getFrameH(); 933 | mFrameRect.bottom += offsetY; 934 | float offsetX = offsetY * getRatioX() / getRatioY(); 935 | mFrameRect.right += offsetX; 936 | } 937 | float ox, oy; 938 | if (!isInsideHorizontal(mFrameRect.right)) { 939 | ox = mFrameRect.right - mImageRect.right; 940 | mFrameRect.right -= ox; 941 | oy = ox * getRatioY() / getRatioX(); 942 | mFrameRect.bottom -= oy; 943 | } 944 | if (!isInsideVertical(mFrameRect.bottom)) { 945 | oy = mFrameRect.bottom - mImageRect.bottom; 946 | mFrameRect.bottom -= oy; 947 | ox = oy * getRatioX() / getRatioY(); 948 | mFrameRect.right -= ox; 949 | } 950 | } 951 | } 952 | 953 | // Frame position correction /////////////////////////////////////////////////////////////////// 954 | 955 | private void checkScaleBounds() { 956 | float lDiff = mFrameRect.left - mImageRect.left; 957 | float rDiff = mFrameRect.right - mImageRect.right; 958 | float tDiff = mFrameRect.top - mImageRect.top; 959 | float bDiff = mFrameRect.bottom - mImageRect.bottom; 960 | 961 | if (lDiff < 0) { 962 | mFrameRect.left -= lDiff; 963 | } 964 | if (rDiff > 0) { 965 | mFrameRect.right -= rDiff; 966 | } 967 | if (tDiff < 0) { 968 | mFrameRect.top -= tDiff; 969 | } 970 | if (bDiff > 0) { 971 | mFrameRect.bottom -= bDiff; 972 | } 973 | } 974 | 975 | private void checkMoveBounds() { 976 | float diff = mFrameRect.left - mImageRect.left; 977 | if (diff < 0) { 978 | mFrameRect.left -= diff; 979 | mFrameRect.right -= diff; 980 | } 981 | diff = mFrameRect.right - mImageRect.right; 982 | if (diff > 0) { 983 | mFrameRect.left -= diff; 984 | mFrameRect.right -= diff; 985 | } 986 | diff = mFrameRect.top - mImageRect.top; 987 | if (diff < 0) { 988 | mFrameRect.top -= diff; 989 | mFrameRect.bottom -= diff; 990 | } 991 | diff = mFrameRect.bottom - mImageRect.bottom; 992 | if (diff > 0) { 993 | mFrameRect.top -= diff; 994 | mFrameRect.bottom -= diff; 995 | } 996 | } 997 | 998 | private boolean isInsideHorizontal(float x) { 999 | return mImageRect.left <= x && mImageRect.right >= x; 1000 | } 1001 | 1002 | private boolean isInsideVertical(float y) { 1003 | return mImageRect.top <= y && mImageRect.bottom >= y; 1004 | } 1005 | 1006 | private boolean isWidthTooSmall() { 1007 | return getFrameW() < mMinFrameSize; 1008 | } 1009 | 1010 | private boolean isHeightTooSmall() { 1011 | return getFrameH() < mMinFrameSize; 1012 | } 1013 | 1014 | // Frame aspect ratio correction /////////////////////////////////////////////////////////////// 1015 | 1016 | private void recalculateFrameRect(int durationMillis) { 1017 | if (mImageRect == null) return; 1018 | if (mIsAnimating) { 1019 | getAnimator().cancelAnimation(); 1020 | } 1021 | final RectF currentRect = new RectF(mFrameRect); 1022 | final RectF newRect = calcFrameRect(mImageRect); 1023 | final float diffL = newRect.left - currentRect.left; 1024 | final float diffT = newRect.top - currentRect.top; 1025 | final float diffR = newRect.right - currentRect.right; 1026 | final float diffB = newRect.bottom - currentRect.bottom; 1027 | if (mIsAnimationEnabled) { 1028 | SimpleValueAnimator animator = getAnimator(); 1029 | animator.addAnimatorListener(new SimpleValueAnimatorListener() { 1030 | @Override 1031 | public void onAnimationStarted() { 1032 | mIsAnimating = true; 1033 | } 1034 | 1035 | @Override 1036 | public void onAnimationUpdated(float scale) { 1037 | mFrameRect = new RectF(currentRect.left + diffL * scale, 1038 | currentRect.top + diffT * scale, 1039 | currentRect.right + diffR * scale, 1040 | currentRect.bottom + diffB * scale); 1041 | invalidate(); 1042 | } 1043 | 1044 | @Override 1045 | public void onAnimationFinished() { 1046 | mFrameRect = newRect; 1047 | invalidate(); 1048 | mIsAnimating = false; 1049 | } 1050 | }); 1051 | animator.startAnimation(durationMillis); 1052 | } else { 1053 | mFrameRect = calcFrameRect(mImageRect); 1054 | invalidate(); 1055 | } 1056 | } 1057 | 1058 | private float getRatioX(float w) { 1059 | switch (mCropMode) { 1060 | case FIT_IMAGE: 1061 | return mImageRect.width(); 1062 | case FREE: 1063 | return w; 1064 | case RATIO_4_3: 1065 | return 4; 1066 | case RATIO_3_4: 1067 | return 3; 1068 | case RATIO_16_9: 1069 | return 16; 1070 | case RATIO_9_16: 1071 | return 9; 1072 | case SQUARE: 1073 | case CIRCLE: 1074 | case CIRCLE_SQUARE: 1075 | return 1; 1076 | case CUSTOM: 1077 | return mCustomRatio.x; 1078 | default: 1079 | return w; 1080 | } 1081 | } 1082 | 1083 | private float getRatioY(float h) { 1084 | switch (mCropMode) { 1085 | case FIT_IMAGE: 1086 | return mImageRect.height(); 1087 | case FREE: 1088 | return h; 1089 | case RATIO_4_3: 1090 | return 3; 1091 | case RATIO_3_4: 1092 | return 4; 1093 | case RATIO_16_9: 1094 | return 9; 1095 | case RATIO_9_16: 1096 | return 16; 1097 | case SQUARE: 1098 | case CIRCLE: 1099 | case CIRCLE_SQUARE: 1100 | return 1; 1101 | case CUSTOM: 1102 | return mCustomRatio.y; 1103 | default: 1104 | return h; 1105 | } 1106 | } 1107 | 1108 | private float getRatioX() { 1109 | switch (mCropMode) { 1110 | case FIT_IMAGE: 1111 | return mImageRect.width(); 1112 | case RATIO_4_3: 1113 | return 4; 1114 | case RATIO_3_4: 1115 | return 3; 1116 | case RATIO_16_9: 1117 | return 16; 1118 | case RATIO_9_16: 1119 | return 9; 1120 | case SQUARE: 1121 | case CIRCLE: 1122 | case CIRCLE_SQUARE: 1123 | return 1; 1124 | case CUSTOM: 1125 | return mCustomRatio.x; 1126 | default: 1127 | return 1; 1128 | } 1129 | } 1130 | 1131 | private float getRatioY() { 1132 | switch (mCropMode) { 1133 | case FIT_IMAGE: 1134 | return mImageRect.height(); 1135 | case RATIO_4_3: 1136 | return 3; 1137 | case RATIO_3_4: 1138 | return 4; 1139 | case RATIO_16_9: 1140 | return 9; 1141 | case RATIO_9_16: 1142 | return 16; 1143 | case SQUARE: 1144 | case CIRCLE: 1145 | case CIRCLE_SQUARE: 1146 | return 1; 1147 | case CUSTOM: 1148 | return mCustomRatio.y; 1149 | default: 1150 | return 1; 1151 | } 1152 | } 1153 | 1154 | // Utility ///////////////////////////////////////////////////////////////////////////////////// 1155 | 1156 | private float getDensity() { 1157 | DisplayMetrics displayMetrics = new DisplayMetrics(); 1158 | ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay() 1159 | .getMetrics(displayMetrics); 1160 | return displayMetrics.density; 1161 | } 1162 | 1163 | private float sq(float value) { 1164 | return value * value; 1165 | } 1166 | 1167 | private float constrain(float val, float min, float max, float defaultVal) { 1168 | if (val < min || val > max) return defaultVal; 1169 | return val; 1170 | } 1171 | 1172 | private void postErrorOnMainThread(final Callback callback) { 1173 | if (callback == null) return; 1174 | if (Looper.myLooper() == Looper.getMainLooper()) { 1175 | callback.onError(); 1176 | } else { 1177 | mHandler.post(new Runnable() { 1178 | @Override 1179 | public void run() { 1180 | callback.onError(); 1181 | } 1182 | }); 1183 | } 1184 | } 1185 | 1186 | private Bitmap getBitmap() { 1187 | Bitmap bm = null; 1188 | Drawable d = getDrawable(); 1189 | if (d != null && d instanceof BitmapDrawable) bm = ((BitmapDrawable) d).getBitmap(); 1190 | return bm; 1191 | } 1192 | 1193 | private float getRotatedWidth(float angle) { 1194 | return getRotatedWidth(angle, mImgWidth, mImgHeight); 1195 | } 1196 | 1197 | private float getRotatedWidth(float angle, float width, float height) { 1198 | return angle % 180 == 0 ? width : height; 1199 | } 1200 | 1201 | private float getRotatedHeight(float angle) { 1202 | return getRotatedHeight(angle, mImgWidth, mImgHeight); 1203 | } 1204 | 1205 | private float getRotatedHeight(float angle, float width, float height) { 1206 | return angle % 180 == 0 ? height : width; 1207 | } 1208 | 1209 | private Bitmap getRotatedBitmap(Bitmap bitmap) { 1210 | Matrix rotateMatrix = new Matrix(); 1211 | rotateMatrix.setRotate(mAngle, bitmap.getWidth() / 2, bitmap.getHeight() / 2); 1212 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), 1213 | rotateMatrix, true); 1214 | } 1215 | 1216 | // Animation /////////////////////////////////////////////////////////////////////////////////// 1217 | 1218 | private SimpleValueAnimator getAnimator() { 1219 | setupAnimatorIfNeeded(); 1220 | return mAnimator; 1221 | } 1222 | 1223 | private void setupAnimatorIfNeeded() { 1224 | if (mAnimator == null) { 1225 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 1226 | mAnimator = new ValueAnimatorV8(mInterpolator); 1227 | } else { 1228 | mAnimator = new ValueAnimatorV14(mInterpolator); 1229 | } 1230 | } 1231 | } 1232 | 1233 | // Cropping //////////////////////////////////////////////////////////////////////////////////// 1234 | 1235 | private Bitmap decodeRegion() { 1236 | Bitmap cropped = null; 1237 | InputStream is = null; 1238 | try { 1239 | is = getContext().getContentResolver() 1240 | .openInputStream(mSourceUri); 1241 | BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); 1242 | final int originalImageWidth = decoder.getWidth(); 1243 | final int originalImageHeight = decoder.getHeight(); 1244 | Rect cropRect = calcCropRect(originalImageWidth, originalImageHeight); 1245 | if (mAngle != 0) { 1246 | Matrix matrix = new Matrix(); 1247 | matrix.setRotate(-mAngle); 1248 | RectF rotated = new RectF(); 1249 | matrix.mapRect(rotated, new RectF(cropRect)); 1250 | rotated.offset(rotated.left < 0 ? originalImageWidth : 0, 1251 | rotated.top < 0 ? originalImageHeight : 0); 1252 | cropRect = new Rect((int) rotated.left, (int) rotated.top, (int) rotated.right, 1253 | (int) rotated.bottom); 1254 | } 1255 | cropped = decoder.decodeRegion(cropRect, new BitmapFactory.Options()); 1256 | if (mAngle != 0) { 1257 | Bitmap rotated = getRotatedBitmap(cropped); 1258 | if (cropped != getBitmap() && cropped != rotated) { 1259 | cropped.recycle(); 1260 | } 1261 | cropped = rotated; 1262 | } 1263 | } catch (IOException e) { 1264 | Logger.e("An error occurred while cropping the image: " + e.getMessage(), e); 1265 | } catch (OutOfMemoryError e) { 1266 | Logger.e("OOM Error: " + e.getMessage(), e); 1267 | } catch (Exception e) { 1268 | Logger.e("An unexpected error has occurred: " + e.getMessage(), e); 1269 | } finally { 1270 | Utils.closeQuietly(is); 1271 | } 1272 | return cropped; 1273 | } 1274 | 1275 | private Rect calcCropRect(int originalImageWidth, int originalImageHeight) { 1276 | float scaleToOriginal = getRotatedWidth(mAngle, originalImageWidth, 1277 | originalImageHeight) / mImageRect.width(); 1278 | float offsetX = mImageRect.left * scaleToOriginal; 1279 | float offsetY = mImageRect.top * scaleToOriginal; 1280 | int left = Math.round(mFrameRect.left * scaleToOriginal - offsetX); 1281 | int top = Math.round(mFrameRect.top * scaleToOriginal - offsetY); 1282 | int right = Math.round(mFrameRect.right * scaleToOriginal - offsetX); 1283 | int bottom = Math.round(mFrameRect.bottom * scaleToOriginal - offsetY); 1284 | int imageW = Math.round(getRotatedWidth(mAngle, originalImageWidth, originalImageHeight)); 1285 | int imageH = Math.round(getRotatedHeight(mAngle, originalImageWidth, originalImageHeight)); 1286 | return new Rect(Math.max(left, 0), Math.max(top, 0), Math.min(right, imageW), 1287 | Math.min(bottom, imageH)); 1288 | } 1289 | 1290 | private Bitmap scaleBitmapIfNeeded(Bitmap cropped) { 1291 | int width = cropped.getWidth(); 1292 | int height = cropped.getHeight(); 1293 | int outWidth = 0; 1294 | int outHeight = 0; 1295 | float imageRatio = getRatioX(mFrameRect.width()) / getRatioY(mFrameRect.height()); 1296 | 1297 | if (mOutputWidth > 0) { 1298 | outWidth = mOutputWidth; 1299 | outHeight = Math.round(mOutputWidth / imageRatio); 1300 | } else if (mOutputHeight > 0) { 1301 | outHeight = mOutputHeight; 1302 | outWidth = Math.round(mOutputHeight * imageRatio); 1303 | } else { 1304 | if (mOutputMaxWidth > 0 && mOutputMaxHeight > 0 1305 | && (width > mOutputMaxWidth || height > mOutputMaxHeight)) { 1306 | float maxRatio = (float) mOutputMaxWidth / (float) mOutputMaxHeight; 1307 | if (maxRatio >= imageRatio) { 1308 | outHeight = mOutputMaxHeight; 1309 | outWidth = Math.round((float) mOutputMaxHeight * imageRatio); 1310 | } else { 1311 | outWidth = mOutputMaxWidth; 1312 | outHeight = Math.round((float) mOutputMaxWidth / imageRatio); 1313 | } 1314 | } 1315 | } 1316 | 1317 | if (outWidth > 0 && outHeight > 0) { 1318 | Bitmap scaled = Utils.getScaledBitmap(cropped, outWidth, outHeight); 1319 | if (cropped != getBitmap() && cropped != scaled) { 1320 | cropped.recycle(); 1321 | } 1322 | cropped = scaled; 1323 | } 1324 | return cropped; 1325 | } 1326 | 1327 | // File save /////////////////////////////////////////////////////////////////////////////////// 1328 | 1329 | private void saveToFile(Bitmap bitmap, final Uri uri) { 1330 | OutputStream outputStream = null; 1331 | try { 1332 | outputStream = getContext().getContentResolver() 1333 | .openOutputStream(uri); 1334 | if (outputStream != null) { 1335 | bitmap.compress(mCompressFormat, mCompressQuality, outputStream); 1336 | } 1337 | } catch (IOException e) { 1338 | Logger.e("An error occurred while saving the image: " + uri, e); 1339 | postErrorOnMainThread(mSaveCallback); 1340 | } finally { 1341 | Utils.closeQuietly(outputStream); 1342 | } 1343 | 1344 | mHandler.post(new Runnable() { 1345 | @Override 1346 | public void run() { 1347 | if (mSaveCallback != null) mSaveCallback.onSuccess(uri); 1348 | } 1349 | }); 1350 | } 1351 | 1352 | // Public methods ////////////////////////////////////////////////////////////////////////////// 1353 | 1354 | /** 1355 | * Get source image bitmap 1356 | * 1357 | * @return src bitmap 1358 | */ 1359 | public Bitmap getImageBitmap() { 1360 | return getBitmap(); 1361 | } 1362 | 1363 | /** 1364 | * Set source image bitmap 1365 | * 1366 | * @param bitmap src image bitmap 1367 | */ 1368 | @Override 1369 | public void setImageBitmap(Bitmap bitmap) { 1370 | super.setImageBitmap(bitmap); // calles setImageDrawable internally 1371 | } 1372 | 1373 | /** 1374 | * Set source image resource id 1375 | * 1376 | * @param resId source image resource id 1377 | */ 1378 | @Override 1379 | public void setImageResource(int resId) { 1380 | mIsInitialized = false; 1381 | super.setImageResource(resId); 1382 | updateLayout(); 1383 | } 1384 | 1385 | /** 1386 | * Set image drawable. 1387 | * 1388 | * @param drawable source image drawable 1389 | */ 1390 | @Override 1391 | public void setImageDrawable(Drawable drawable) { 1392 | mIsInitialized = false; 1393 | super.setImageDrawable(drawable); 1394 | updateLayout(); 1395 | } 1396 | 1397 | /** 1398 | * Set image uri 1399 | * 1400 | * @param uri source image local uri 1401 | */ 1402 | @Override 1403 | public void setImageURI(Uri uri) { 1404 | mIsInitialized = false; 1405 | super.setImageURI(uri); 1406 | updateLayout(); 1407 | } 1408 | 1409 | private void updateLayout() { 1410 | resetImageInfo(); 1411 | Drawable d = getDrawable(); 1412 | if (d != null) { 1413 | setupLayout(mViewWidth, mViewHeight); 1414 | } 1415 | } 1416 | 1417 | private void resetImageInfo() { 1418 | if (mIsLoading) return; 1419 | mSourceUri = null; 1420 | mSaveUri = null; 1421 | mInputImageWidth = 0; 1422 | mInputImageHeight = 0; 1423 | mOutputImageWidth = 0; 1424 | mOutputImageHeight = 0; 1425 | mAngle = mExifRotation; 1426 | } 1427 | 1428 | /** 1429 | * Load image from Uri. 1430 | * 1431 | * @param sourceUri Image Uri 1432 | * @param callback Callback 1433 | */ 1434 | public void startLoad(Uri sourceUri, LoadCallback callback) { 1435 | mLoadCallback = callback; 1436 | mSourceUri = sourceUri; 1437 | if (sourceUri == null) { 1438 | postErrorOnMainThread(mLoadCallback); 1439 | throw new IllegalStateException("Source Uri must not be null."); 1440 | } 1441 | mExecutor.submit(new Runnable() { 1442 | @Override 1443 | public void run() { 1444 | mIsLoading = true; 1445 | mExifRotation = Utils.getExifOrientation(getContext(), mSourceUri); 1446 | int maxSize = Utils.getMaxSize(); 1447 | int requestSize = Math.max(mViewWidth, mViewHeight); 1448 | if (requestSize == 0) requestSize = maxSize; 1449 | try { 1450 | final Bitmap sampledBitmap = Utils.decodeSampledBitmapFromUri(getContext(), 1451 | mSourceUri, 1452 | requestSize); 1453 | mInputImageWidth = Utils.sInputImageWidth; 1454 | mInputImageHeight = Utils.sInputImageHeight; 1455 | mHandler.post(new Runnable() { 1456 | @Override 1457 | public void run() { 1458 | mAngle = mExifRotation; 1459 | setImageBitmap(sampledBitmap); 1460 | if (mLoadCallback != null) mLoadCallback.onSuccess(); 1461 | mIsLoading = false; 1462 | } 1463 | }); 1464 | } catch (OutOfMemoryError e) { 1465 | Logger.e("OOM Error: " + e.getMessage(), e); 1466 | postErrorOnMainThread(mLoadCallback); 1467 | mIsLoading = false; 1468 | } catch (Exception e) { 1469 | Logger.e("An unexpected error has occurred: " + e.getMessage(), e); 1470 | postErrorOnMainThread(mLoadCallback); 1471 | mIsLoading = false; 1472 | } 1473 | } 1474 | }); 1475 | } 1476 | 1477 | /** 1478 | * Rotate image 1479 | * 1480 | * @param degrees rotation angle 1481 | * @param durationMillis animation duration in milliseconds 1482 | */ 1483 | public void rotateImage(RotateDegrees degrees, int durationMillis) { 1484 | if (mIsRotating) { 1485 | getAnimator().cancelAnimation(); 1486 | } 1487 | final float currentAngle = mAngle; 1488 | final float newAngle = (mAngle + degrees.getValue()); 1489 | final float angleDiff = newAngle - currentAngle; 1490 | final float currentScale = mScale; 1491 | final float newScale = calcScale(mViewWidth, mViewHeight, newAngle); 1492 | if (mIsAnimationEnabled) { 1493 | final float scaleDiff = newScale - currentScale; 1494 | SimpleValueAnimator animator = getAnimator(); 1495 | animator.addAnimatorListener(new SimpleValueAnimatorListener() { 1496 | @Override 1497 | public void onAnimationStarted() { 1498 | mIsRotating = true; 1499 | } 1500 | 1501 | @Override 1502 | public void onAnimationUpdated(float scale) { 1503 | mAngle = currentAngle + angleDiff * scale; 1504 | mScale = currentScale + scaleDiff * scale; 1505 | setMatrix(); 1506 | invalidate(); 1507 | } 1508 | 1509 | @Override 1510 | public void onAnimationFinished() { 1511 | mAngle = newAngle % 360; 1512 | mScale = newScale; 1513 | setupLayout(mViewWidth, mViewHeight); 1514 | mIsRotating = false; 1515 | } 1516 | }); 1517 | animator.startAnimation(durationMillis); 1518 | } else { 1519 | mAngle = newAngle % 360; 1520 | mScale = newScale; 1521 | setupLayout(mViewWidth, mViewHeight); 1522 | } 1523 | } 1524 | 1525 | /** 1526 | * Rotate image 1527 | * 1528 | * @param degrees rotation angle 1529 | */ 1530 | public void rotateImage(RotateDegrees degrees) { 1531 | rotateImage(degrees, mAnimationDurationMillis); 1532 | } 1533 | 1534 | /** 1535 | * Get cropped image bitmap 1536 | * 1537 | * @return cropped image bitmap 1538 | */ 1539 | public Bitmap getCroppedBitmap() { 1540 | Bitmap source = getBitmap(); 1541 | if (source == null) return null; 1542 | 1543 | Bitmap rotated = getRotatedBitmap(source); 1544 | Rect cropRect = calcCropRect(source.getWidth(), source.getHeight()); 1545 | Bitmap cropped = Bitmap.createBitmap( 1546 | rotated, 1547 | cropRect.left, 1548 | cropRect.top, 1549 | cropRect.width(), 1550 | cropRect.height(), 1551 | null, 1552 | false 1553 | ); 1554 | if (rotated != cropped && rotated != source) { 1555 | rotated.recycle(); 1556 | } 1557 | 1558 | if (mCropMode == CropMode.CIRCLE) { 1559 | Bitmap circle = getCircularBitmap(cropped); 1560 | if (cropped != getBitmap()) { 1561 | cropped.recycle(); 1562 | } 1563 | cropped = circle; 1564 | } 1565 | return cropped; 1566 | } 1567 | 1568 | /** 1569 | * Crop the square image in a circular 1570 | * 1571 | * @param square image bitmap 1572 | * @return circular image bitmap 1573 | */ 1574 | public Bitmap getCircularBitmap(Bitmap square) { 1575 | if (square == null) return null; 1576 | Bitmap output = Bitmap.createBitmap(square.getWidth(), square.getHeight(), 1577 | Bitmap.Config.ARGB_8888); 1578 | 1579 | final Rect rect = new Rect(0, 0, square.getWidth(), square.getHeight()); 1580 | Canvas canvas = new Canvas(output); 1581 | 1582 | int halfWidth = square.getWidth() / 2; 1583 | int halfHeight = square.getHeight() / 2; 1584 | 1585 | final Paint paint = new Paint(); 1586 | paint.setAntiAlias(true); 1587 | paint.setFilterBitmap(true); 1588 | 1589 | canvas.drawCircle(halfWidth, halfHeight, Math.min(halfWidth, halfHeight), paint); 1590 | paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); 1591 | canvas.drawBitmap(square, rect, rect, paint); 1592 | return output; 1593 | } 1594 | 1595 | /** 1596 | * Crop image from Uri 1597 | * 1598 | * @param saveUri Uri for saving the cropped image 1599 | * @param cropCallback Callback for cropping the image 1600 | * @param saveCallback Callback for saving the image 1601 | */ 1602 | public void startCrop(Uri saveUri, CropCallback cropCallback, SaveCallback saveCallback) { 1603 | mSaveUri = saveUri; 1604 | mCropCallback = cropCallback; 1605 | mSaveCallback = saveCallback; 1606 | if (mIsCropping) { 1607 | postErrorOnMainThread(mCropCallback); 1608 | postErrorOnMainThread(mSaveCallback); 1609 | return; 1610 | } 1611 | mIsCropping = true; 1612 | mExecutor.submit(new Runnable() { 1613 | @Override 1614 | public void run() { 1615 | Bitmap cropped; 1616 | 1617 | // Use thumbnail for crop 1618 | if (mSourceUri == null) { 1619 | cropped = getCroppedBitmap(); 1620 | } 1621 | // Use file for crop 1622 | else { 1623 | cropped = decodeRegion(); 1624 | if (mCropMode == CropMode.CIRCLE) { 1625 | Bitmap circle = getCircularBitmap(cropped); 1626 | if (cropped != getBitmap()) { 1627 | cropped.recycle(); 1628 | } 1629 | cropped = circle; 1630 | } 1631 | } 1632 | 1633 | // Success 1634 | if (cropped != null) { 1635 | cropped = scaleBitmapIfNeeded(cropped); 1636 | final Bitmap tmp = cropped; 1637 | mOutputImageWidth = tmp.getWidth(); 1638 | mOutputImageHeight = tmp.getHeight(); 1639 | mHandler.post(new Runnable() { 1640 | @Override 1641 | public void run() { 1642 | if (mCropCallback != null) mCropCallback.onSuccess(tmp); 1643 | if (mIsDebug) invalidate(); 1644 | } 1645 | }); 1646 | } 1647 | // Error 1648 | else { 1649 | postErrorOnMainThread(mCropCallback); 1650 | } 1651 | 1652 | if (mSaveUri == null) { 1653 | postErrorOnMainThread(mSaveCallback); 1654 | return; 1655 | } 1656 | saveToFile(cropped, mSaveUri); 1657 | mIsCropping = false; 1658 | } 1659 | }); 1660 | } 1661 | 1662 | /** 1663 | * Get frame position relative to the source bitmap. 1664 | * 1665 | * @return crop area boundaries. 1666 | */ 1667 | public RectF getActualCropRect() { 1668 | float offsetX = (mImageRect.left / mScale); 1669 | float offsetY = (mImageRect.top / mScale); 1670 | float l = (mFrameRect.left / mScale) - offsetX; 1671 | float t = (mFrameRect.top / mScale) - offsetY; 1672 | float r = (mFrameRect.right / mScale) - offsetX; 1673 | float b = (mFrameRect.bottom / mScale) - offsetY; 1674 | return new RectF(l, t, r, b); 1675 | } 1676 | 1677 | /** 1678 | * Set crop mode 1679 | * 1680 | * @param mode crop mode 1681 | * @param durationMillis animation duration in milliseconds 1682 | */ 1683 | public void setCropMode(CropMode mode, int durationMillis) { 1684 | if (mode == CropMode.CUSTOM) { 1685 | setCustomRatio(1, 1); 1686 | } else { 1687 | mCropMode = mode; 1688 | recalculateFrameRect(durationMillis); 1689 | } 1690 | } 1691 | 1692 | /** 1693 | * Set crop mode 1694 | * 1695 | * @param mode crop mode 1696 | */ 1697 | public void setCropMode(CropMode mode) { 1698 | setCropMode(mode, mAnimationDurationMillis); 1699 | } 1700 | 1701 | /** 1702 | * Set custom aspect ratio to crop frame 1703 | * 1704 | * @param ratioX ratio x 1705 | * @param ratioY ratio y 1706 | * @param durationMillis animation duration in milliseconds 1707 | */ 1708 | public void setCustomRatio(int ratioX, int ratioY, int durationMillis) { 1709 | if (ratioX == 0 || ratioY == 0) return; 1710 | mCropMode = CropMode.CUSTOM; 1711 | mCustomRatio = new PointF(ratioX, ratioY); 1712 | recalculateFrameRect(durationMillis); 1713 | } 1714 | 1715 | /** 1716 | * Set custom aspect ratio to crop frame 1717 | * 1718 | * @param ratioX ratio x 1719 | * @param ratioY ratio y 1720 | */ 1721 | public void setCustomRatio(int ratioX, int ratioY) { 1722 | setCustomRatio(ratioX, ratioY, mAnimationDurationMillis); 1723 | } 1724 | 1725 | /** 1726 | * Set image overlay color 1727 | * 1728 | * @param overlayColor color resId or color int(ex. 0xFFFFFFFF) 1729 | */ 1730 | public void setOverlayColor(int overlayColor) { 1731 | this.mOverlayColor = overlayColor; 1732 | invalidate(); 1733 | } 1734 | 1735 | /** 1736 | * Set crop frame color 1737 | * 1738 | * @param frameColor color resId or color int(ex. 0xFFFFFFFF) 1739 | */ 1740 | public void setFrameColor(int frameColor) { 1741 | this.mFrameColor = frameColor; 1742 | invalidate(); 1743 | } 1744 | 1745 | /** 1746 | * Set handle color 1747 | * 1748 | * @param handleColor color resId or color int(ex. 0xFFFFFFFF) 1749 | */ 1750 | public void setHandleColor(int handleColor) { 1751 | this.mHandleColor = handleColor; 1752 | invalidate(); 1753 | } 1754 | 1755 | /** 1756 | * Set guide color 1757 | * 1758 | * @param guideColor color resId or color int(ex. 0xFFFFFFFF) 1759 | */ 1760 | public void setGuideColor(int guideColor) { 1761 | this.mGuideColor = guideColor; 1762 | invalidate(); 1763 | } 1764 | 1765 | /** 1766 | * Set view background color 1767 | * 1768 | * @param bgColor color resId or color int(ex. 0xFFFFFFFF) 1769 | */ 1770 | public void setBackgroundColor(int bgColor) { 1771 | this.mBackgroundColor = bgColor; 1772 | invalidate(); 1773 | } 1774 | 1775 | /** 1776 | * Set crop frame minimum size in density-independent pixels. 1777 | * 1778 | * @param minDp crop frame minimum size in density-independent pixels 1779 | */ 1780 | public void setMinFrameSizeInDp(int minDp) { 1781 | mMinFrameSize = minDp * getDensity(); 1782 | } 1783 | 1784 | /** 1785 | * Set crop frame minimum size in pixels. 1786 | * 1787 | * @param minPx crop frame minimum size in pixels 1788 | */ 1789 | public void setMinFrameSizeInPx(int minPx) { 1790 | mMinFrameSize = minPx; 1791 | } 1792 | 1793 | /** 1794 | * Set handle radius in density-independent pixels. 1795 | * 1796 | * @param handleDp handle radius in density-independent pixels 1797 | */ 1798 | public void setHandleSizeInDp(int handleDp) { 1799 | mHandleSize = (int) (handleDp * getDensity()); 1800 | } 1801 | 1802 | /** 1803 | * Set crop frame handle touch padding(touch area) in density-independent pixels. 1804 | * 1805 | * handle touch area : a circle of radius R.(R = handle size + touch padding) 1806 | * 1807 | * @param paddingDp crop frame handle touch padding(touch area) in density-independent pixels 1808 | */ 1809 | public void setTouchPaddingInDp(int paddingDp) { 1810 | mTouchPadding = (int) (paddingDp * getDensity()); 1811 | } 1812 | 1813 | /** 1814 | * Set guideline show mode. 1815 | * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH) 1816 | * 1817 | * @param mode guideline show mode 1818 | */ 1819 | public void setGuideShowMode(ShowMode mode) { 1820 | mGuideShowMode = mode; 1821 | switch (mode) { 1822 | case SHOW_ALWAYS: 1823 | mShowGuide = true; 1824 | break; 1825 | case NOT_SHOW: 1826 | case SHOW_ON_TOUCH: 1827 | mShowGuide = false; 1828 | break; 1829 | } 1830 | invalidate(); 1831 | } 1832 | 1833 | /** 1834 | * Set handle show mode. 1835 | * (SHOW_ALWAYS/NOT_SHOW/SHOW_ON_TOUCH) 1836 | * 1837 | * @param mode handle show mode 1838 | */ 1839 | public void setHandleShowMode(ShowMode mode) { 1840 | mHandleShowMode = mode; 1841 | switch (mode) { 1842 | case SHOW_ALWAYS: 1843 | mShowHandle = true; 1844 | break; 1845 | case NOT_SHOW: 1846 | case SHOW_ON_TOUCH: 1847 | mShowHandle = false; 1848 | break; 1849 | } 1850 | invalidate(); 1851 | } 1852 | 1853 | /** 1854 | * Set frame stroke weight in density-independent pixels. 1855 | * 1856 | * @param weightDp frame stroke weight in density-independent pixels. 1857 | */ 1858 | public void setFrameStrokeWeightInDp(int weightDp) { 1859 | mFrameStrokeWeight = weightDp * getDensity(); 1860 | invalidate(); 1861 | } 1862 | 1863 | /** 1864 | * Set guideline stroke weight in density-independent pixels. 1865 | * 1866 | * @param weightDp guideline stroke weight in density-independent pixels. 1867 | */ 1868 | public void setGuideStrokeWeightInDp(int weightDp) { 1869 | mGuideStrokeWeight = weightDp * getDensity(); 1870 | invalidate(); 1871 | } 1872 | 1873 | /** 1874 | * Set whether to show crop frame. 1875 | * 1876 | * @param enabled should show crop frame? 1877 | */ 1878 | public void setCropEnabled(boolean enabled) { 1879 | mIsCropEnabled = enabled; 1880 | invalidate(); 1881 | } 1882 | 1883 | /** 1884 | * Set locking the crop frame. 1885 | * 1886 | * @param enabled should lock crop frame? 1887 | */ 1888 | @Override 1889 | public void setEnabled(boolean enabled) { 1890 | super.setEnabled(enabled); 1891 | mIsEnabled = enabled; 1892 | } 1893 | 1894 | /** 1895 | * Set initial scale of the frame.(0.01 ~ 1.0) 1896 | * 1897 | * @param initialScale initial scale 1898 | */ 1899 | public void setInitialFrameScale(float initialScale) { 1900 | mInitialFrameScale = constrain(initialScale, 0.01f, 1.0f, DEFAULT_INITIAL_FRAME_SCALE); 1901 | } 1902 | 1903 | /** 1904 | * Set whether to animate 1905 | * 1906 | * @param enabled is animation enabled? 1907 | */ 1908 | public void setAnimationEnabled(boolean enabled) { 1909 | mIsAnimationEnabled = enabled; 1910 | } 1911 | 1912 | /** 1913 | * Set duration of animation 1914 | * 1915 | * @param durationMillis animation duration in milliseconds 1916 | */ 1917 | public void setAnimationDuration(int durationMillis) { 1918 | mAnimationDurationMillis = durationMillis; 1919 | } 1920 | 1921 | /** 1922 | * Set interpolator of animation 1923 | * (Default interpolator is DecelerateInterpolator) 1924 | * 1925 | * @param interpolator interpolator used for animation 1926 | */ 1927 | public void setInterpolator(Interpolator interpolator) { 1928 | mInterpolator = interpolator; 1929 | mAnimator = null; 1930 | setupAnimatorIfNeeded(); 1931 | } 1932 | 1933 | /** 1934 | * Set whether to show debug display 1935 | * 1936 | * @param debug is logging enabled 1937 | */ 1938 | public void setDebug(boolean debug) { 1939 | mIsDebug = debug; 1940 | invalidate(); 1941 | } 1942 | 1943 | /** 1944 | * Set whether to log exception 1945 | * 1946 | * @param enabled is logging enabled 1947 | */ 1948 | public void setLoggingEnabled(boolean enabled) { 1949 | Logger.enabled = enabled; 1950 | } 1951 | 1952 | /** 1953 | * Set Image Load callback 1954 | * 1955 | * @param callback callback 1956 | */ 1957 | public void setLoadCallback(LoadCallback callback) { 1958 | mLoadCallback = callback; 1959 | } 1960 | 1961 | /** 1962 | * Set Image Crop callback 1963 | * 1964 | * @param callback callback 1965 | */ 1966 | public void setCropCallback(CropCallback callback) { 1967 | mCropCallback = callback; 1968 | } 1969 | 1970 | /** 1971 | * Set Image Save callback 1972 | * 1973 | * @param callback callback 1974 | */ 1975 | public void setSaveCallback(SaveCallback callback) { 1976 | mSaveCallback = callback; 1977 | } 1978 | 1979 | /** 1980 | * Set fixed width for output 1981 | * (After cropping, the image is scaled to the specified size.) 1982 | * 1983 | * @param outputWidth output width 1984 | */ 1985 | public void setOutputWidth(int outputWidth) { 1986 | mOutputWidth = outputWidth; 1987 | mOutputHeight = 0; 1988 | } 1989 | 1990 | /** 1991 | * Set fixed height for output 1992 | * (After cropping, the image is scaled to the specified size.) 1993 | * 1994 | * @param outputHeight output height 1995 | */ 1996 | public void setOutputHeight(int outputHeight) { 1997 | mOutputHeight = outputHeight; 1998 | mOutputWidth = 0; 1999 | } 2000 | 2001 | /** 2002 | * Set maximum size for output 2003 | * (If cropped image size is larger than max size, the image is scaled to the smaller size. 2004 | * If fixed output width/height has already set, these parameters are ignored.) 2005 | * 2006 | * @param maxWidth max output width 2007 | * @param maxHeight max output height 2008 | */ 2009 | public void setOutputMaxSize(int maxWidth, int maxHeight) { 2010 | mOutputMaxWidth = maxWidth; 2011 | mOutputMaxHeight = maxHeight; 2012 | } 2013 | 2014 | /** 2015 | * Set compress format for output 2016 | * 2017 | * @param format compress format{@link android.graphics.Bitmap.CompressFormat} 2018 | */ 2019 | public void setCompressFormat(Bitmap.CompressFormat format) { 2020 | mCompressFormat = format; 2021 | } 2022 | 2023 | /** 2024 | * Set compress quality for output 2025 | * 2026 | * @param quality compress quality(0-100: 100 is default.) 2027 | */ 2028 | public void setCompressQuality(int quality) { 2029 | mCompressQuality = quality; 2030 | } 2031 | 2032 | /** 2033 | * Set whether to show handle shadows 2034 | * 2035 | * @param handleShadowEnabled should show handle shadows? 2036 | */ 2037 | public void setHandleShadowEnabled(boolean handleShadowEnabled) { 2038 | mIsHandleShadowEnabled = handleShadowEnabled; 2039 | } 2040 | 2041 | private void setScale(float mScale) { 2042 | this.mScale = mScale; 2043 | } 2044 | 2045 | private void setCenter(PointF mCenter) { 2046 | this.mCenter = mCenter; 2047 | } 2048 | 2049 | private float getFrameW() { 2050 | return (mFrameRect.right - mFrameRect.left); 2051 | } 2052 | 2053 | private float getFrameH() { 2054 | return (mFrameRect.bottom - mFrameRect.top); 2055 | } 2056 | 2057 | // Enum //////////////////////////////////////////////////////////////////////////////////////// 2058 | 2059 | private enum TouchArea { 2060 | OUT_OF_BOUNDS, CENTER, LEFT_TOP, RIGHT_TOP, LEFT_BOTTOM, RIGHT_BOTTOM 2061 | } 2062 | 2063 | public enum CropMode { 2064 | FIT_IMAGE(0), RATIO_4_3(1), RATIO_3_4(2), SQUARE(3), RATIO_16_9(4), RATIO_9_16(5), FREE( 2065 | 6), CUSTOM(7), CIRCLE(8), CIRCLE_SQUARE(9); 2066 | private final int ID; 2067 | 2068 | CropMode(final int id) { 2069 | this.ID = id; 2070 | } 2071 | 2072 | public int getId() { 2073 | return ID; 2074 | } 2075 | } 2076 | 2077 | public enum ShowMode { 2078 | SHOW_ALWAYS(1), SHOW_ON_TOUCH(2), NOT_SHOW(3); 2079 | private final int ID; 2080 | 2081 | ShowMode(final int id) { 2082 | this.ID = id; 2083 | } 2084 | 2085 | public int getId() { 2086 | return ID; 2087 | } 2088 | } 2089 | 2090 | public enum RotateDegrees { 2091 | ROTATE_90D(90), ROTATE_180D(180), ROTATE_270D(270), ROTATE_M90D(-90), ROTATE_M180D(-180), 2092 | ROTATE_M270D(-270); 2093 | 2094 | private final int VALUE; 2095 | 2096 | RotateDegrees(final int value) { 2097 | this.VALUE = value; 2098 | } 2099 | 2100 | public int getValue() { 2101 | return VALUE; 2102 | } 2103 | } 2104 | 2105 | // Save/Restore support //////////////////////////////////////////////////////////////////////// 2106 | 2107 | public static class SavedState extends BaseSavedState { 2108 | Bitmap image; 2109 | CropMode mode; 2110 | int backgroundColor; 2111 | int overlayColor; 2112 | int frameColor; 2113 | ShowMode guideShowMode; 2114 | ShowMode handleShowMode; 2115 | boolean showGuide; 2116 | boolean showHandle; 2117 | int handleSize; 2118 | int touchPadding; 2119 | float minFrameSize; 2120 | float customRatioX; 2121 | float customRatioY; 2122 | float frameStrokeWeight; 2123 | float guideStrokeWeight; 2124 | boolean isCropEnabled; 2125 | int handleColor; 2126 | int guideColor; 2127 | float initialFrameScale; 2128 | float angle; 2129 | boolean isAnimationEnabled; 2130 | int animationDuration; 2131 | int exifRotation; 2132 | Uri sourceUri; 2133 | Uri saveUri; 2134 | Bitmap.CompressFormat compressFormat; 2135 | int compressQuality; 2136 | boolean isDebug; 2137 | int outputMaxWidth; 2138 | int outputMaxHeight; 2139 | int outputWidth; 2140 | int outputHeight; 2141 | boolean isHandleShadowEnabled; 2142 | int inputImageWidth; 2143 | int inputImageHeight; 2144 | int outputImageWidth; 2145 | int outputImageHeight; 2146 | 2147 | SavedState(Parcelable superState) { 2148 | super(superState); 2149 | } 2150 | 2151 | private SavedState(Parcel in) { 2152 | super(in); 2153 | image = in.readParcelable(Bitmap.class.getClassLoader()); 2154 | mode = (CropMode) in.readSerializable(); 2155 | backgroundColor = in.readInt(); 2156 | overlayColor = in.readInt(); 2157 | frameColor = in.readInt(); 2158 | guideShowMode = (ShowMode) in.readSerializable(); 2159 | handleShowMode = (ShowMode) in.readSerializable(); 2160 | showGuide = (in.readInt() != 0); 2161 | showHandle = (in.readInt() != 0); 2162 | handleSize = in.readInt(); 2163 | touchPadding = in.readInt(); 2164 | minFrameSize = in.readFloat(); 2165 | customRatioX = in.readFloat(); 2166 | customRatioY = in.readFloat(); 2167 | frameStrokeWeight = in.readFloat(); 2168 | guideStrokeWeight = in.readFloat(); 2169 | isCropEnabled = (in.readInt() != 0); 2170 | handleColor = in.readInt(); 2171 | guideColor = in.readInt(); 2172 | initialFrameScale = in.readFloat(); 2173 | angle = in.readFloat(); 2174 | isAnimationEnabled = (in.readInt() != 0); 2175 | animationDuration = in.readInt(); 2176 | exifRotation = in.readInt(); 2177 | sourceUri = in.readParcelable(Uri.class.getClassLoader()); 2178 | saveUri = in.readParcelable(Uri.class.getClassLoader()); 2179 | compressFormat = (Bitmap.CompressFormat) in.readSerializable(); 2180 | compressQuality = in.readInt(); 2181 | isDebug = (in.readInt() != 0); 2182 | outputMaxWidth = in.readInt(); 2183 | outputMaxHeight = in.readInt(); 2184 | outputWidth = in.readInt(); 2185 | outputHeight = in.readInt(); 2186 | isHandleShadowEnabled = (in.readInt() != 0); 2187 | inputImageWidth = in.readInt(); 2188 | inputImageHeight = in.readInt(); 2189 | outputImageWidth = in.readInt(); 2190 | outputImageHeight = in.readInt(); 2191 | } 2192 | 2193 | @Override 2194 | public void writeToParcel(Parcel out, int flag) { 2195 | super.writeToParcel(out, flag); 2196 | out.writeParcelable(image, flag); 2197 | out.writeSerializable(mode); 2198 | out.writeInt(backgroundColor); 2199 | out.writeInt(overlayColor); 2200 | out.writeInt(frameColor); 2201 | out.writeSerializable(guideShowMode); 2202 | out.writeSerializable(handleShowMode); 2203 | out.writeInt(showGuide ? 1 : 0); 2204 | out.writeInt(showHandle ? 1 : 0); 2205 | out.writeInt(handleSize); 2206 | out.writeInt(touchPadding); 2207 | out.writeFloat(minFrameSize); 2208 | out.writeFloat(customRatioX); 2209 | out.writeFloat(customRatioY); 2210 | out.writeFloat(frameStrokeWeight); 2211 | out.writeFloat(guideStrokeWeight); 2212 | out.writeInt(isCropEnabled ? 1 : 0); 2213 | out.writeInt(handleColor); 2214 | out.writeInt(guideColor); 2215 | out.writeFloat(initialFrameScale); 2216 | out.writeFloat(angle); 2217 | out.writeInt(isAnimationEnabled ? 1 : 0); 2218 | out.writeInt(animationDuration); 2219 | out.writeInt(exifRotation); 2220 | out.writeParcelable(sourceUri, flag); 2221 | out.writeParcelable(saveUri, flag); 2222 | out.writeSerializable(compressFormat); 2223 | out.writeInt(compressQuality); 2224 | out.writeInt(isDebug ? 1 : 0); 2225 | out.writeInt(outputMaxWidth); 2226 | out.writeInt(outputMaxHeight); 2227 | out.writeInt(outputWidth); 2228 | out.writeInt(outputHeight); 2229 | out.writeInt(isHandleShadowEnabled ? 1 : 0); 2230 | out.writeInt(inputImageWidth); 2231 | out.writeInt(inputImageHeight); 2232 | out.writeInt(outputImageWidth); 2233 | out.writeInt(outputImageHeight); 2234 | } 2235 | 2236 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 2237 | public SavedState createFromParcel(final Parcel inParcel) { 2238 | return new SavedState(inParcel); 2239 | } 2240 | 2241 | public SavedState[] newArray(final int inSize) { 2242 | return new SavedState[inSize]; 2243 | } 2244 | }; 2245 | } 2246 | } 2247 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/animation/SimpleValueAnimator.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.animation; 2 | @SuppressWarnings("unused") 3 | public interface SimpleValueAnimator { 4 | void startAnimation(long duration); 5 | void cancelAnimation(); 6 | boolean isAnimationStarted(); 7 | void addAnimatorListener(com.jeanboy.cropview.animation.SimpleValueAnimatorListener animatorListener); 8 | } 9 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/animation/SimpleValueAnimatorListener.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.animation; 2 | 3 | public interface SimpleValueAnimatorListener { 4 | void onAnimationStarted(); 5 | void onAnimationUpdated(float scale); 6 | void onAnimationFinished(); 7 | } 8 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/animation/ValueAnimatorV14.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.animation; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ValueAnimator; 5 | import android.annotation.TargetApi; 6 | import android.os.Build; 7 | import android.view.animation.Interpolator; 8 | 9 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 10 | public class ValueAnimatorV14 implements SimpleValueAnimator, 11 | Animator.AnimatorListener, 12 | ValueAnimator.AnimatorUpdateListener { 13 | private static final int DEFAULT_ANIMATION_DURATION = 150; 14 | private ValueAnimator animator; 15 | private SimpleValueAnimatorListener animatorListener = new SimpleValueAnimatorListener() { 16 | @Override 17 | public void onAnimationStarted() { 18 | 19 | } 20 | 21 | @Override 22 | public void onAnimationUpdated(float scale) { 23 | 24 | } 25 | 26 | @Override 27 | public void onAnimationFinished() { 28 | 29 | } 30 | }; 31 | 32 | public ValueAnimatorV14(Interpolator interpolator) { 33 | animator = ValueAnimator.ofFloat(0.0f, 1.0f); 34 | animator.addListener(this); 35 | animator.addUpdateListener(this); 36 | animator.setInterpolator(interpolator); 37 | } 38 | 39 | @Override 40 | public void startAnimation(long duration) { 41 | if (duration >= 0) { 42 | animator.setDuration(duration); 43 | } else { 44 | animator.setDuration(DEFAULT_ANIMATION_DURATION); 45 | } 46 | animator.start(); 47 | } 48 | 49 | @Override 50 | public void cancelAnimation() { 51 | animator.cancel(); 52 | } 53 | 54 | @Override 55 | public boolean isAnimationStarted() { 56 | return animator.isStarted(); 57 | } 58 | 59 | @Override 60 | public void addAnimatorListener(SimpleValueAnimatorListener animatorListener) { 61 | if (animatorListener != null) this.animatorListener = animatorListener; 62 | } 63 | 64 | @Override 65 | public void onAnimationStart(Animator animation) { 66 | animatorListener.onAnimationStarted(); 67 | } 68 | 69 | @Override 70 | public void onAnimationEnd(Animator animation) { 71 | animatorListener.onAnimationFinished(); 72 | } 73 | 74 | @Override 75 | public void onAnimationCancel(Animator animation) { 76 | animatorListener.onAnimationFinished(); 77 | } 78 | 79 | @Override 80 | public void onAnimationRepeat(Animator animation) { 81 | 82 | } 83 | 84 | @Override 85 | public void onAnimationUpdate(ValueAnimator animation) { 86 | animatorListener.onAnimationUpdated(animation.getAnimatedFraction()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/animation/ValueAnimatorV8.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.animation; 2 | 3 | import android.os.SystemClock; 4 | import android.view.animation.Interpolator; 5 | 6 | import java.util.concurrent.Executors; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | public class ValueAnimatorV8 implements SimpleValueAnimator { 11 | private static final int FRAME_RATE = 30; 12 | private static final int UPDATE_SPAN = Math.round((float) 1000 / (float) FRAME_RATE); 13 | private static final int DEFAULT_ANIMATION_DURATION = 150; 14 | 15 | private Interpolator mInterpolator; 16 | ScheduledExecutorService service; 17 | long start; 18 | boolean isAnimationStarted = false; 19 | long duration; 20 | private SimpleValueAnimatorListener animatorListener = new SimpleValueAnimatorListener() { 21 | @Override 22 | public void onAnimationStarted() { 23 | 24 | } 25 | 26 | @Override 27 | public void onAnimationUpdated(float scale) { 28 | 29 | } 30 | 31 | @Override 32 | public void onAnimationFinished() { 33 | 34 | } 35 | 36 | }; 37 | 38 | private final Runnable runnable = new Runnable() { 39 | @Override 40 | public void run() { 41 | long elapsed = SystemClock.uptimeMillis() - start; 42 | if (elapsed > duration) { 43 | isAnimationStarted = false; 44 | animatorListener.onAnimationFinished(); 45 | service.shutdown(); 46 | return; 47 | } 48 | float scale = Math.min(mInterpolator.getInterpolation((float) elapsed / duration), 1); 49 | animatorListener.onAnimationUpdated(scale); 50 | } 51 | }; 52 | 53 | public ValueAnimatorV8(Interpolator interpolator) { 54 | this.mInterpolator = interpolator; 55 | } 56 | 57 | @Override 58 | public void startAnimation(long duration) { 59 | if (duration >= 0) { 60 | this.duration = duration; 61 | } else { 62 | this.duration = DEFAULT_ANIMATION_DURATION; 63 | } 64 | isAnimationStarted = true; 65 | animatorListener.onAnimationStarted(); 66 | start = SystemClock.uptimeMillis(); 67 | service = Executors.newSingleThreadScheduledExecutor(); 68 | service.scheduleAtFixedRate(runnable, 0, UPDATE_SPAN, TimeUnit.MILLISECONDS); 69 | } 70 | 71 | @Override 72 | public void cancelAnimation() { 73 | isAnimationStarted = false; 74 | service.shutdown(); 75 | animatorListener.onAnimationFinished(); 76 | } 77 | 78 | @Override 79 | public boolean isAnimationStarted() { 80 | return isAnimationStarted; 81 | } 82 | 83 | @Override 84 | public void addAnimatorListener(SimpleValueAnimatorListener animatorListener) { 85 | if (animatorListener != null) this.animatorListener = animatorListener; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/callback/Callback.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.callback; 2 | 3 | public interface Callback { 4 | void onError(); 5 | } 6 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/callback/CropCallback.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.callback; 2 | 3 | import android.graphics.Bitmap; 4 | 5 | public interface CropCallback extends Callback { 6 | void onSuccess(Bitmap cropped); 7 | void onError(); 8 | } 9 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/callback/LoadCallback.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.callback; 2 | 3 | 4 | public interface LoadCallback extends Callback{ 5 | void onSuccess(); 6 | void onError(); 7 | } 8 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/callback/SaveCallback.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.callback; 2 | 3 | 4 | import android.net.Uri; 5 | 6 | public interface SaveCallback extends Callback{ 7 | void onSuccess(Uri outputUri); 8 | void onError(); 9 | } 10 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/cropper/CropActivity.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.cropper; 2 | 3 | import android.content.Intent; 4 | import android.graphics.Bitmap; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.support.v7.widget.Toolbar; 9 | import android.util.Log; 10 | import android.view.View; 11 | 12 | import com.jeanboy.cropview.CropImageView; 13 | import com.jeanboy.cropview.R; 14 | import com.jeanboy.cropview.callback.CropCallback; 15 | import com.jeanboy.cropview.callback.LoadCallback; 16 | import com.jeanboy.cropview.callback.SaveCallback; 17 | 18 | import java.io.File; 19 | 20 | public class CropActivity extends AppCompatActivity { 21 | 22 | 23 | private Toolbar toolbar; 24 | 25 | private CropImageView cropImageView; 26 | 27 | private LoadingDialog loadingDialog; 28 | 29 | private Uri imgUri; 30 | 31 | private int aspect_x, aspect_Y; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_crop); 37 | 38 | setupToolbar(); 39 | setupView(); 40 | } 41 | 42 | @Override 43 | public boolean onSupportNavigateUp() { 44 | onBackPressed(); 45 | return true; 46 | } 47 | 48 | private void setupToolbar() { 49 | if (toolbar == null) { 50 | toolbar = (Toolbar) findViewById(R.id.toolbar); 51 | if (toolbar != null) { 52 | toolbar.setTitle(""); 53 | setSupportActionBar(toolbar); 54 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 55 | getSupportActionBar().setTitle("截取图片"); 56 | } 57 | } 58 | } 59 | 60 | public void setupView() { 61 | aspect_x = getIntent().getExtras().getInt(CropperParams.ASPECT_X, CropperParams.DEFAULT_ASPECT); 62 | aspect_Y = getIntent().getExtras().getInt(CropperParams.ASPECT_Y, CropperParams.DEFAULT_ASPECT); 63 | imgUri = getIntent().getExtras().getParcelable(CropperParams.PICK_URI); 64 | 65 | cropImageView = (CropImageView) findViewById(R.id.cropImageView); 66 | loadingDialog = new LoadingDialog(this); 67 | cropImageView.setCustomRatio(aspect_x, aspect_Y); 68 | if (aspect_x <= 0 && aspect_x <= 0) { 69 | cropImageView.setCropMode(CropImageView.CropMode.FREE); 70 | } 71 | loadingDialog.show(); 72 | cropImageView.startLoad(imgUri, mLoadCallback); 73 | } 74 | 75 | public void cropImage() {//裁切图片 76 | loadingDialog.show(); 77 | cropImageView.startCrop(createSaveUri(), mCropCallback, mSaveCallback); 78 | } 79 | 80 | public void rotateLeft(View v) {//向左旋转 81 | cropImageView.rotateImage(CropImageView.RotateDegrees.ROTATE_M90D); 82 | } 83 | 84 | public void rotateRight(View v) {//向右旋转 85 | cropImageView.rotateImage(CropImageView.RotateDegrees.ROTATE_90D); 86 | } 87 | 88 | public void done(View v) {//完成按钮 89 | cropImage(); 90 | } 91 | 92 | public Uri createSaveUri() {//创建裁切完文件保存目录,默认保存在/mnt/sdcard/Android/data/<包名>/cache 93 | return Uri.fromFile(new File(getExternalCacheDir().getAbsolutePath(), "cropped_" + System.currentTimeMillis() + ".jpg")); 94 | } 95 | 96 | // Callbacks /////////////////////////////////////////////////////////////////////////////////// 97 | 98 | private final LoadCallback mLoadCallback = new LoadCallback() { 99 | @Override 100 | public void onSuccess() { 101 | loadingDialog.dismiss(); 102 | } 103 | 104 | @Override 105 | public void onError() { 106 | loadingDialog.dismiss(); 107 | } 108 | }; 109 | 110 | private final CropCallback mCropCallback = new CropCallback() { 111 | @Override 112 | public void onSuccess(Bitmap cropped) { 113 | } 114 | 115 | @Override 116 | public void onError() { 117 | } 118 | }; 119 | 120 | private final SaveCallback mSaveCallback = new SaveCallback() { 121 | @Override 122 | public void onSuccess(Uri outputUri) { 123 | loadingDialog.dismiss(); 124 | Log.d("================", "====" + outputUri); 125 | setResult(RESULT_OK, new Intent().putExtra(CropperParams.PICK_URI, outputUri)); 126 | finish(); 127 | } 128 | 129 | @Override 130 | public void onError() { 131 | loadingDialog.dismiss(); 132 | } 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/cropper/CropperHandler.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.cropper; 2 | 3 | import android.app.Activity; 4 | import android.net.Uri; 5 | 6 | /** 7 | * Created by Next on 2016/8/3. 8 | */ 9 | public interface CropperHandler { 10 | 11 | Activity getActivity(); 12 | 13 | CropperParams getParams(); 14 | 15 | void onCropped(Uri uri); 16 | 17 | void onCropCancel(); 18 | 19 | void onCropFailed(String msg); 20 | 21 | 22 | } 23 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/cropper/CropperManager.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.cropper; 2 | 3 | import android.app.Activity; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.net.Uri; 8 | import android.os.Build; 9 | import android.provider.MediaStore; 10 | 11 | import com.jeanboy.cropview.util.Utils; 12 | 13 | import java.io.File; 14 | import java.io.FileNotFoundException; 15 | 16 | /** 17 | * Created by Next on 2016/8/3. 18 | */ 19 | public class CropperManager { 20 | 21 | 22 | public static final String MSG_FILE_NOT_FOUND = "图片不可用"; 23 | public static final String MSG_ERROR = "操作失败"; 24 | 25 | private static CropperManager instance; 26 | 27 | private CropperHandler cropperHandler; 28 | 29 | private Uri cameraCacheUri; 30 | 31 | private CropperManager() { 32 | } 33 | 34 | public static CropperManager getInstance() { 35 | if (instance == null) { 36 | synchronized (CropperManager.class) { 37 | if (instance == null) { 38 | instance = new CropperManager(); 39 | } 40 | } 41 | } 42 | return instance; 43 | } 44 | 45 | public void build(CropperHandler cropperHandler) { 46 | this.cropperHandler = cropperHandler; 47 | } 48 | 49 | public void destroy() { 50 | this.instance = null; 51 | this.cropperHandler = null; 52 | } 53 | 54 | public void pickFromCamera() { 55 | if (cropperHandler == null) return; 56 | createCameraUri();//生成相机缓存文件 57 | 58 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 59 | ContentValues contentValues = new ContentValues(1); 60 | contentValues.put(MediaStore.Images.Media.DATA, cameraCacheUri.getPath()); 61 | contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); 62 | Uri uri = cropperHandler.getActivity().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); 63 | cropperHandler.getActivity().startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE). 64 | putExtra(MediaStore.EXTRA_OUTPUT, uri), CropperParams.REQUEST_PICK_CAMERA); 65 | } else { 66 | cropperHandler.getActivity().startActivityForResult(new Intent(MediaStore.ACTION_IMAGE_CAPTURE). 67 | putExtra(MediaStore.EXTRA_OUTPUT, cameraCacheUri), 68 | CropperParams.REQUEST_PICK_CAMERA); 69 | } 70 | } 71 | 72 | public void pickFromGallery() { 73 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { 74 | cropperHandler.getActivity().startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"), 75 | CropperParams.REQUEST_PICK_IMAGE); 76 | } else { 77 | Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); 78 | intent.addCategory(Intent.CATEGORY_OPENABLE); 79 | intent.setType("image/*"); 80 | cropperHandler.getActivity().startActivityForResult(intent, CropperParams.REQUEST_KITKAT_PICK_IMAGE); 81 | } 82 | } 83 | 84 | public void handlerResult(int requestCode, int resultCode, Intent result) { 85 | if (cropperHandler == null) return; 86 | if (resultCode == Activity.RESULT_CANCELED) { 87 | cropperHandler.onCropCancel(); 88 | } else if (resultCode == Activity.RESULT_OK) { 89 | switch (requestCode) { 90 | case CropperParams.REQUEST_PICK_CAMERA://相机拍完照回调处理,去裁切 91 | notifyImageToGallery(cropperHandler.getActivity(), cameraCacheUri); 92 | cropperHandler.getActivity().startActivityForResult(new Intent(cropperHandler.getActivity(), CropActivity.class) 93 | .putExtra(CropperParams.PICK_URI, cameraCacheUri) 94 | .putExtra(CropperParams.ASPECT_X, cropperHandler.getParams().aspectX) 95 | .putExtra(CropperParams.ASPECT_Y, cropperHandler.getParams().aspectY), 96 | CropperParams.REQUEST_CROPPED); 97 | break; 98 | case CropperParams.REQUEST_PICK_IMAGE: 99 | if (result != null) { 100 | cropperHandler.getActivity().startActivityForResult(new Intent(cropperHandler.getActivity(), CropActivity.class) 101 | .putExtra(CropperParams.PICK_URI, result.getData()) 102 | .putExtra(CropperParams.ASPECT_X, cropperHandler.getParams().aspectX) 103 | .putExtra(CropperParams.ASPECT_Y, cropperHandler.getParams().aspectY), 104 | CropperParams.REQUEST_CROPPED); 105 | } else { 106 | cropperHandler.onCropFailed(MSG_FILE_NOT_FOUND); 107 | } 108 | break; 109 | case CropperParams.REQUEST_KITKAT_PICK_IMAGE: 110 | if (result != null) { 111 | cropperHandler.getActivity().startActivityForResult(new Intent(cropperHandler.getActivity(), CropActivity.class) 112 | .putExtra(CropperParams.PICK_URI, Utils.ensureUriPermission(cropperHandler.getActivity(), result)) 113 | .putExtra(CropperParams.ASPECT_X, cropperHandler.getParams().aspectX) 114 | .putExtra(CropperParams.ASPECT_Y, cropperHandler.getParams().aspectY), 115 | CropperParams.REQUEST_CROPPED); 116 | } else { 117 | cropperHandler.onCropFailed(MSG_FILE_NOT_FOUND); 118 | } 119 | break; 120 | case CropperParams.REQUEST_CROPPED: 121 | if (result != null) { 122 | Uri uri = result.getExtras().getParcelable(CropperParams.PICK_URI); 123 | cropperHandler.onCropped(uri); 124 | } else { 125 | cropperHandler.onCropFailed(MSG_ERROR); 126 | } 127 | break; 128 | } 129 | } 130 | } 131 | 132 | public void createCameraUri() { 133 | //创建相机拍照文件保存目录,默认保存在/mnt/sdcard/Android/data/<包名>/cache 134 | cameraCacheUri = Uri.fromFile(cropperHandler.getActivity().getExternalCacheDir()).buildUpon().appendPath(getCameraFileName()) 135 | .build(); 136 | } 137 | 138 | private String getCameraFileName() { 139 | return "cropper_" + System.currentTimeMillis() + ".jpg"; 140 | } 141 | 142 | /** 143 | * 同步图片到系统图库 144 | * 145 | * @param context 146 | * @param uri 147 | */ 148 | public void notifyImageToGallery(Context context, Uri uri) { 149 | //把文件插入到系统图库 150 | try { 151 | File file = new File(uri.getPath()); 152 | MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), null); 153 | } catch (FileNotFoundException e) { 154 | e.printStackTrace(); 155 | } 156 | // 通知图库更新 157 | context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, uri)); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/cropper/CropperParams.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.cropper; 2 | 3 | /** 4 | * Created by Next on 2016/8/3. 5 | */ 6 | public class CropperParams { 7 | 8 | public CropperParams(int aspectX, int aspectY) { 9 | this.aspectX = aspectX; 10 | this.aspectY = aspectY; 11 | } 12 | 13 | public static final String PICK_URI = "pick_uri"; 14 | public static final String ASPECT_X = "aspectX"; 15 | public static final String ASPECT_Y = "aspectY"; 16 | public static final int REQUEST_PICK_CAMERA = 10010; 17 | public static final int REQUEST_PICK_IMAGE = 10011; 18 | public static final int REQUEST_KITKAT_PICK_IMAGE = 10012; 19 | public static final int REQUEST_CROPPED = 10013; 20 | 21 | public static final int DEFAULT_ASPECT = 1; 22 | 23 | public int aspectX = DEFAULT_ASPECT; 24 | public int aspectY = DEFAULT_ASPECT; 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/cropper/LoadingDialog.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.cropper; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | 6 | import com.jeanboy.cropview.R; 7 | 8 | 9 | /** 10 | * Created by Next on 2016/8/3. 11 | */ 12 | public class LoadingDialog extends Dialog { 13 | 14 | public LoadingDialog(Context context) { 15 | super(context, R.style.DialogTheme_Loading); 16 | setContentView(R.layout.dialog_loading); 17 | 18 | } 19 | 20 | @Override 21 | public void onBackPressed() { 22 | cancel(); 23 | dismiss(); 24 | super.onBackPressed(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/util/Logger.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.util; 2 | 3 | @SuppressWarnings("unused") 4 | public class Logger { 5 | private static final String TAG = "CropView"; 6 | public static boolean enabled = false; 7 | 8 | public static void e(String msg) { 9 | if (!enabled) return; 10 | android.util.Log.e(TAG, msg); 11 | } 12 | 13 | public static void e(String msg, Throwable e) { 14 | if (!enabled) return; 15 | android.util.Log.e(TAG, msg, e); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lib-cropview/src/main/java/com/jeanboy/cropview/util/Utils.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropview.util; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.ContentUris; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.database.Cursor; 8 | import android.graphics.Bitmap; 9 | import android.graphics.BitmapFactory; 10 | import android.graphics.Matrix; 11 | import android.media.ExifInterface; 12 | import android.net.Uri; 13 | import android.opengl.GLES10; 14 | import android.os.Build; 15 | import android.os.Environment; 16 | import android.os.ParcelFileDescriptor; 17 | import android.provider.DocumentsContract; 18 | import android.provider.MediaStore; 19 | 20 | import java.io.Closeable; 21 | import java.io.File; 22 | import java.io.FileDescriptor; 23 | import java.io.FileInputStream; 24 | import java.io.FileNotFoundException; 25 | import java.io.FileOutputStream; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | 29 | @SuppressWarnings("unused") 30 | public class Utils { 31 | private static final String TAG = Utils.class.getSimpleName(); 32 | private static final int SIZE_DEFAULT = 2048; 33 | private static final int SIZE_LIMIT = 4096; 34 | public static int sInputImageWidth = 0; 35 | public static int sInputImageHeight = 0; 36 | 37 | public static int getExifRotation(File file) { 38 | if (file == null) return 0; 39 | try { 40 | ExifInterface exif = new ExifInterface(file.getAbsolutePath()); 41 | return getRotateDegreeFromOrientation( 42 | exif.getAttributeInt( 43 | ExifInterface.TAG_ORIENTATION, 44 | ExifInterface.ORIENTATION_UNDEFINED 45 | ) 46 | ); 47 | } catch (IOException e) { 48 | Logger.e("An error occurred while getting the exif data: " + e.getMessage(), e); 49 | } 50 | return 0; 51 | } 52 | 53 | public static int getExifRotation(Context context, Uri uri){ 54 | Cursor cursor = null; 55 | String[] projection = {MediaStore.Images.ImageColumns.ORIENTATION}; 56 | try { 57 | cursor = context.getContentResolver().query(uri, projection, null, null, null); 58 | if (cursor == null || !cursor.moveToFirst()) { 59 | return 0; 60 | } 61 | return cursor.getInt(0); 62 | } catch (RuntimeException ignored) { 63 | return 0; 64 | } finally { 65 | if (cursor != null) { 66 | cursor.close(); 67 | } 68 | } 69 | } 70 | 71 | public static int getExifOrientation(Context context, Uri uri) { 72 | String authority = uri.getAuthority().toLowerCase(); 73 | int orientation; 74 | if (authority.endsWith("media")) { 75 | orientation = getExifRotation(context, uri); 76 | } else { 77 | orientation = getExifRotation(getFileFromUri(context, uri)); 78 | } 79 | return orientation; 80 | } 81 | 82 | public static int getRotateDegreeFromOrientation(int orientation) { 83 | int degree = 0; 84 | switch (orientation) { 85 | case ExifInterface.ORIENTATION_ROTATE_90: 86 | degree = 90; 87 | break; 88 | case ExifInterface.ORIENTATION_ROTATE_180: 89 | degree = 180; 90 | break; 91 | case ExifInterface.ORIENTATION_ROTATE_270: 92 | degree = 270; 93 | break; 94 | default: 95 | break; 96 | } 97 | return degree; 98 | } 99 | 100 | public static Matrix getMatrixFromExifOrientation(int orientation) { 101 | Matrix matrix = new Matrix(); 102 | switch (orientation) { 103 | case ExifInterface.ORIENTATION_UNDEFINED: 104 | break; 105 | case ExifInterface.ORIENTATION_NORMAL: 106 | break; 107 | case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 108 | matrix.postScale(-1.0f, 1.0f); 109 | break; 110 | case ExifInterface.ORIENTATION_ROTATE_180: 111 | matrix.postRotate(180.0f); 112 | break; 113 | case ExifInterface.ORIENTATION_FLIP_VERTICAL: 114 | matrix.postScale(1.0f, -1.0f); 115 | break; 116 | case ExifInterface.ORIENTATION_ROTATE_90: 117 | matrix.postRotate(90.0f); 118 | break; 119 | case ExifInterface.ORIENTATION_TRANSVERSE: 120 | matrix.postRotate(-90.0f); 121 | matrix.postScale(1.0f, -1.0f); 122 | break; 123 | case ExifInterface.ORIENTATION_TRANSPOSE: 124 | matrix.postRotate(90.0f); 125 | matrix.postScale(1.0f, -1.0f); 126 | break; 127 | case ExifInterface.ORIENTATION_ROTATE_270: 128 | matrix.postRotate(-90.0f); 129 | break; 130 | } 131 | return matrix; 132 | } 133 | 134 | public static int getExifOrientationFromAngle(int angle) { 135 | int normalizedAngle = angle % 360; 136 | switch (normalizedAngle) { 137 | case 0: 138 | return ExifInterface.ORIENTATION_NORMAL; 139 | case 90: 140 | return ExifInterface.ORIENTATION_ROTATE_90; 141 | case 180: 142 | return ExifInterface.ORIENTATION_ROTATE_180; 143 | case 270: 144 | return ExifInterface.ORIENTATION_ROTATE_270; 145 | default: 146 | return ExifInterface.ORIENTATION_NORMAL; 147 | } 148 | } 149 | 150 | @SuppressWarnings("ResourceType") 151 | @TargetApi(Build.VERSION_CODES.KITKAT) 152 | public static Uri ensureUriPermission(Context context, Intent intent) { 153 | Uri uri = intent.getData(); 154 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 155 | final int takeFlags = intent.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION; 156 | context.getContentResolver().takePersistableUriPermission(uri, takeFlags); 157 | } 158 | return uri; 159 | } 160 | 161 | /** 162 | * Get image file from uri 163 | * 164 | * @param context The context 165 | * @param uri The Uri of the image 166 | * @return Image file 167 | */ 168 | @TargetApi(Build.VERSION_CODES.KITKAT) 169 | public static File getFileFromUri(final Context context, final Uri uri) { 170 | String filePath = null; 171 | final boolean isKitkat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 172 | // DocumentProvider 173 | if (isKitkat && DocumentsContract.isDocumentUri(context, uri)) { 174 | // ExternalStorageProvider 175 | if (isExternalStorageDocument(uri)) { 176 | final String docId = DocumentsContract.getDocumentId(uri); 177 | final String[] split = docId.split(":"); 178 | final String type = split[0]; 179 | 180 | if ("primary".equalsIgnoreCase(type)) { 181 | filePath = Environment.getExternalStorageDirectory() + "/" + split[1]; 182 | } 183 | } 184 | // DownloadsProvider 185 | else if (isDownloadsDocument(uri)) { 186 | final String id = DocumentsContract.getDocumentId(uri); 187 | final Uri contentUri = ContentUris.withAppendedId( 188 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 189 | filePath = getDataColumn(context, contentUri, null, null); 190 | } 191 | // MediaProvider 192 | else if (isMediaDocument(uri)) { 193 | final String docId = DocumentsContract.getDocumentId(uri); 194 | final String[] split = docId.split(":"); 195 | final String type = split[0]; 196 | Uri contentUri = null; 197 | if ("image".equals(type)) { 198 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 199 | } else if ("video".equals(type)) { 200 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 201 | } else if ("audio".equals(type)) { 202 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 203 | } 204 | final String selection = "_id=?"; 205 | final String[] selectionArgs = new String[]{ 206 | split[1] 207 | }; 208 | filePath = getDataColumn(context, contentUri, selection, selectionArgs); 209 | } else if (isGoogleDriveDocument(uri)) { 210 | return getGoogleDriveFile(context, uri); 211 | } 212 | } 213 | // MediaStore (and general) 214 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 215 | if (isGooglePhotosUri(uri)) { 216 | filePath = uri.getLastPathSegment(); 217 | } else { 218 | filePath = getDataColumn(context, uri, null, null); 219 | } 220 | } 221 | // File 222 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 223 | filePath = uri.getPath(); 224 | } 225 | if (filePath != null) { 226 | return new File(filePath); 227 | } 228 | return null; 229 | } 230 | 231 | /** 232 | * Get the value of the data column for this Uri. This is useful for 233 | * MediaStore Uris, and other file-based ContentProviders. 234 | * 235 | * @param context The context. 236 | * @param uri The Uri to query. 237 | * @param selection (Optional) Filter used in the query. 238 | * @param selectionArgs (Optional) Selection arguments used in the query. 239 | * @return The value of the _data column, which is typically a file path. 240 | */ 241 | public static String getDataColumn(Context context, Uri uri, String selection, 242 | String[] selectionArgs) { 243 | Cursor cursor = null; 244 | final String[] projection = { 245 | MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME 246 | }; 247 | try { 248 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 249 | null); 250 | if (cursor != null && cursor.moveToFirst()) { 251 | final int columnIndex = 252 | (uri.toString().startsWith("content://com.google.android.gallery3d")) ? 253 | cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) : 254 | cursor.getColumnIndex(MediaStore.MediaColumns.DATA); 255 | if (columnIndex != -1) { 256 | return cursor.getString(columnIndex); 257 | } 258 | } 259 | } finally { 260 | if (cursor != null) { 261 | cursor.close(); 262 | } 263 | } 264 | return null; 265 | } 266 | 267 | 268 | /** 269 | * @param uri The Uri to check. 270 | * @return Whether the Uri authority is ExternalStorageProvider. 271 | */ 272 | public static boolean isExternalStorageDocument(Uri uri) { 273 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 274 | } 275 | 276 | /** 277 | * @param uri The Uri to check. 278 | * @return Whether the Uri authority is DownloadsProvider. 279 | */ 280 | public static boolean isDownloadsDocument(Uri uri) { 281 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 282 | } 283 | 284 | /** 285 | * @param uri The Uri to check. 286 | * @return Whether the Uri authority is MediaProvider. 287 | */ 288 | public static boolean isMediaDocument(Uri uri) { 289 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 290 | } 291 | 292 | /** 293 | * @param uri The Uri to check. 294 | * @return Whether the Uri authority is Google Photos. 295 | */ 296 | public static boolean isGooglePhotosUri(Uri uri) { 297 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 298 | } 299 | 300 | /** 301 | * @param uri The Uri to check 302 | * @return Whether the Uri authority is Google Drive. 303 | */ 304 | public static boolean isGoogleDriveDocument(Uri uri) { 305 | return "com.google.android.apps.docs.storage".equals(uri.getAuthority()); 306 | } 307 | 308 | /** 309 | * @param context The context 310 | * @param uri The Uri of Google Drive file 311 | * @return Google Drive file 312 | */ 313 | private static File getGoogleDriveFile(Context context, Uri uri) { 314 | if (uri == null) return null; 315 | FileInputStream input = null; 316 | FileOutputStream output = null; 317 | String filePath = new File(context.getCacheDir(), "tmp").getAbsolutePath(); 318 | try { 319 | ParcelFileDescriptor pfd = context.getContentResolver().openFileDescriptor(uri, "r"); 320 | if (pfd == null) return null; 321 | FileDescriptor fd = pfd.getFileDescriptor(); 322 | input = new FileInputStream(fd); 323 | output = new FileOutputStream(filePath); 324 | int read; 325 | byte[] bytes = new byte[4096]; 326 | while ((read = input.read(bytes)) != -1) { 327 | output.write(bytes, 0, read); 328 | } 329 | return new File(filePath); 330 | } catch (IOException ignored) { 331 | } finally { 332 | closeQuietly(input); 333 | closeQuietly(output); 334 | } 335 | return null; 336 | } 337 | 338 | public static Bitmap decodeSampledBitmapFromUri(Context context, Uri sourceUri, int requestSize) { 339 | InputStream is = null; 340 | try { 341 | is = context.getContentResolver().openInputStream(sourceUri); 342 | } catch (FileNotFoundException e) { 343 | e.printStackTrace(); 344 | } 345 | BitmapFactory.Options options = new BitmapFactory.Options(); 346 | options.inSampleSize = Utils.calculateInSampleSize(context, sourceUri, requestSize); 347 | options.inJustDecodeBounds = false; 348 | return BitmapFactory.decodeStream(is, null, options); 349 | } 350 | 351 | public static int calculateInSampleSize(Context context, Uri sourceUri, int requestSize) { 352 | InputStream is = null; 353 | // check image size 354 | final BitmapFactory.Options options = new BitmapFactory.Options(); 355 | options.inJustDecodeBounds = true; 356 | try { 357 | is = context.getContentResolver().openInputStream(sourceUri); 358 | BitmapFactory.decodeStream(is, null, options); 359 | } catch (FileNotFoundException ignored) { 360 | } finally { 361 | closeQuietly(is); 362 | } 363 | int inSampleSize = 1; 364 | sInputImageWidth = options.outWidth; 365 | sInputImageHeight = options.outHeight; 366 | while (options.outWidth / inSampleSize > requestSize 367 | || options.outHeight / inSampleSize > requestSize) { 368 | inSampleSize *= 2; 369 | } 370 | return inSampleSize; 371 | } 372 | 373 | public static Bitmap getScaledBitmapForHeight(Bitmap bitmap, int outHeight) { 374 | float currentWidth = bitmap.getWidth(); 375 | float currentHeight = bitmap.getHeight(); 376 | float ratio = currentWidth / currentHeight; 377 | int outWidth = Math.round(outHeight * ratio); 378 | return getScaledBitmap(bitmap, outWidth, outHeight); 379 | } 380 | 381 | public static Bitmap getScaledBitmapForWidth(Bitmap bitmap, int outWidth) { 382 | float currentWidth = bitmap.getWidth(); 383 | float currentHeight = bitmap.getHeight(); 384 | float ratio = currentWidth / currentHeight; 385 | int outHeight = Math.round(outWidth / ratio); 386 | return getScaledBitmap(bitmap, outWidth, outHeight); 387 | } 388 | 389 | public static Bitmap getScaledBitmap(Bitmap bitmap, int outWidth, int outHeight) { 390 | int currentWidth = bitmap.getWidth(); 391 | int currentHeight = bitmap.getHeight(); 392 | Matrix scaleMatrix = new Matrix(); 393 | scaleMatrix.postScale( 394 | (float) outWidth / (float) currentWidth, 395 | (float) outHeight / (float) currentHeight 396 | ); 397 | return Bitmap.createBitmap(bitmap, 0, 0, currentWidth, currentHeight, scaleMatrix, true); 398 | } 399 | 400 | public static int getMaxSize() { 401 | int maxSize = SIZE_DEFAULT; 402 | int[] arr = new int[1]; 403 | GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, arr, 0); 404 | if (arr[0] > 0) { 405 | maxSize = Math.min(arr[0], SIZE_LIMIT); 406 | } 407 | return maxSize; 408 | } 409 | 410 | public static void closeQuietly(Closeable closeable) { 411 | if (closeable == null) return; 412 | try { 413 | closeable.close(); 414 | } catch (Throwable ignored) { 415 | } 416 | } 417 | } 418 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable-v21/ic_crop_done.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable-v21/ic_crop_rotate_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable-v21/ic_crop_rotate_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable-v21/ic_select_photo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable/ic_crop_done.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable/ic_crop_rotate_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable/ic_crop_rotate_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/drawable/ic_select_photo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/layout/activity_crop.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 30 | 31 | 50 | 51 | 55 | 56 | 61 | 62 | 70 | 71 | 79 | 80 | 81 | 82 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/layout/dialog_loading.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /lib-cropview/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resource/ScreenRecord.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/resource/ScreenRecord.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':simple', ':lib-cropview' 2 | -------------------------------------------------------------------------------- /simple/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /simple/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.jeanboy.cropper" 9 | minSdkVersion 15 10 | targetSdkVersion 25 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:25.3.1' 26 | compile("com.github.hotchemi:permissionsdispatcher:2.4.0") { 27 | // if you don't use android.app.Fragment you can exclude support for them 28 | exclude module: "support-v13" 29 | } 30 | annotationProcessor "com.github.hotchemi:permissionsdispatcher-processor:2.4.0" 31 | compile project(path: ':lib-cropview') 32 | } 33 | -------------------------------------------------------------------------------- /simple/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Develop\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /simple/src/androidTest/java/com/jeanboy/cropper/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropper; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /simple/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /simple/src/main/java/com/jeanboy/cropper/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.jeanboy.cropper; 2 | 3 | import android.Manifest; 4 | import android.app.Activity; 5 | import android.content.DialogInterface; 6 | import android.content.Intent; 7 | import android.graphics.Bitmap; 8 | import android.net.Uri; 9 | import android.os.Bundle; 10 | import android.provider.MediaStore; 11 | import android.support.annotation.NonNull; 12 | import android.support.v7.app.AlertDialog; 13 | import android.support.v7.app.AppCompatActivity; 14 | import android.util.Log; 15 | import android.view.View; 16 | import android.widget.ImageView; 17 | 18 | import com.jeanboy.cropview.cropper.CropperHandler; 19 | import com.jeanboy.cropview.cropper.CropperManager; 20 | import com.jeanboy.cropview.cropper.CropperParams; 21 | 22 | import java.io.IOException; 23 | 24 | import permissions.dispatcher.NeedsPermission; 25 | import permissions.dispatcher.OnShowRationale; 26 | import permissions.dispatcher.PermissionRequest; 27 | import permissions.dispatcher.RuntimePermissions; 28 | 29 | 30 | @RuntimePermissions 31 | public class MainActivity extends AppCompatActivity implements CropperHandler { 32 | 33 | 34 | private ImageView iv_cropped; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | iv_cropped = (ImageView) findViewById(R.id.iv_cropped); 41 | 42 | CropperManager.getInstance().build(this); 43 | } 44 | 45 | public void fromCamera(View v) { 46 | MainActivityPermissionsDispatcher.pickFromCameraWithCheck(this); 47 | } 48 | 49 | public void fromGallery(View v) { 50 | MainActivityPermissionsDispatcher.pickFromGalleryWithCheck(this); 51 | } 52 | 53 | @Override 54 | public Activity getActivity() { 55 | return this; 56 | } 57 | 58 | @Override 59 | public CropperParams getParams() { 60 | return new CropperParams(1, 1); 61 | } 62 | 63 | @Override 64 | public void onCropped(Uri uri) { 65 | Log.d("=====onCropped======", "======裁切成功=======" + uri); 66 | try { 67 | Bitmap bm = MediaStore.Images.Media.getBitmap(getContentResolver(), uri); 68 | iv_cropped.setImageBitmap(bm); 69 | } catch (IOException e) { 70 | e.printStackTrace(); 71 | } 72 | 73 | } 74 | 75 | @Override 76 | public void onCropCancel() { 77 | Log.d("=====onCropCancel====", "======裁切取消====="); 78 | } 79 | 80 | @Override 81 | public void onCropFailed(String msg) { 82 | Log.d("=====onCropFailed===", "=======裁切失败======" + msg); 83 | } 84 | 85 | @Override 86 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 87 | super.onActivityResult(requestCode, resultCode, data); 88 | CropperManager.getInstance().handlerResult(requestCode, resultCode, data); 89 | } 90 | 91 | @Override 92 | protected void onDestroy() { 93 | super.onDestroy(); 94 | CropperManager.getInstance().destroy(); 95 | } 96 | 97 | /*-------------------权限处理-----------------*/ 98 | 99 | @Override 100 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 101 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 102 | MainActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults); 103 | } 104 | 105 | @NeedsPermission({Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}) 106 | public void pickFromCamera() { 107 | CropperManager.getInstance().pickFromCamera(); 108 | } 109 | 110 | @NeedsPermission({Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}) 111 | public void pickFromGallery() { 112 | CropperManager.getInstance().pickFromGallery(); 113 | } 114 | 115 | @OnShowRationale({Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}) 116 | public void showRationaleForCamera(PermissionRequest request) { 117 | showRationaleDialog("该操作需要使用摄像头,请允许!", request); 118 | } 119 | 120 | @OnShowRationale({Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}) 121 | public void showRationaleForPick(PermissionRequest request) {//权限说明 122 | showRationaleDialog("该操作需要操作内存卡,请允许!", request); 123 | } 124 | 125 | 126 | private void showRationaleDialog(String msg, final PermissionRequest request) { 127 | new AlertDialog.Builder(getActivity()) 128 | .setPositiveButton("允许", new DialogInterface.OnClickListener() { 129 | @Override 130 | public void onClick(@NonNull DialogInterface dialog, int which) { 131 | request.proceed(); 132 | } 133 | }) 134 | .setNegativeButton("拒绝", new DialogInterface.OnClickListener() { 135 | @Override 136 | public void onClick(@NonNull DialogInterface dialog, int which) { 137 | request.cancel(); 138 | } 139 | }) 140 | .setCancelable(false) 141 | .setMessage(msg) 142 | .show(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /simple/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 |