├── .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 |   
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 |
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 |
5 |
--------------------------------------------------------------------------------
/lib-cropview/src/main/res/values/attrs_crop_image_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/lib-cropview/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #000000
5 | #000000
6 | #ffffff
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib-cropview/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/lib-cropview/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
21 |
22 |
30 |
31 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/simple/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/simple/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/simple/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/simple/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/simple/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/simple/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/simple/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/simple/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/simple/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jeanboydev/Android-CropView/b702ecb5e03e8c5848f65b8e405432920bedd957/simple/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/simple/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/simple/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/simple/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/simple/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CropView
3 |
4 |
--------------------------------------------------------------------------------
/simple/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
17 |
18 |
--------------------------------------------------------------------------------
/simple/src/test/java/com/jeanboy/cropper/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.jeanboy.cropper;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect() throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------