├── settings.gradle ├── image ├── 1.gif └── 2.gif ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── sample ├── src │ └── main │ │ ├── res │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ ├── drawable-hdpi │ │ │ └── ic_image_white.png │ │ ├── drawable-xhdpi │ │ │ └── ic_image_white.png │ │ ├── drawable-xxhdpi │ │ │ └── ic_image_white.png │ │ ├── drawable-xxxhdpi │ │ │ └── ic_image_white.png │ │ ├── values │ │ │ ├── dimens.xml │ │ │ ├── strings.xml │ │ │ ├── colors.xml │ │ │ └── styles.xml │ │ ├── drawable │ │ │ └── decoration_white.xml │ │ ├── layout │ │ │ ├── item_main_image.xml │ │ │ ├── content_main.xml │ │ │ └── activity_main.xml │ │ └── menu │ │ │ └── menu_main.xml │ │ ├── java │ │ └── com │ │ │ └── yanzhenjie │ │ │ └── durban │ │ │ └── sample │ │ │ ├── Utils.java │ │ │ ├── GridAdapter.java │ │ │ └── MainActivity.java │ │ └── AndroidManifest.xml └── build.gradle ├── durban ├── src │ └── main │ │ ├── res │ │ ├── drawable-hdpi │ │ │ ├── durban_ic_back_white.png │ │ │ ├── durban_ic_done_white.png │ │ │ ├── durban_ic_scale_in_white.png │ │ │ ├── durban_ic_scale_out_white.png │ │ │ ├── durban_ic_rotation_90_left_white.png │ │ │ └── durban_ic_rotation_90_right_white.png │ │ ├── drawable-xhdpi │ │ │ ├── durban_ic_back_white.png │ │ │ ├── durban_ic_done_white.png │ │ │ ├── durban_ic_scale_in_white.png │ │ │ ├── durban_ic_scale_out_white.png │ │ │ ├── durban_ic_rotation_90_left_white.png │ │ │ └── durban_ic_rotation_90_right_white.png │ │ ├── drawable-xxhdpi │ │ │ ├── durban_ic_back_white.png │ │ │ ├── durban_ic_done_white.png │ │ │ ├── durban_ic_scale_in_white.png │ │ │ ├── durban_ic_scale_out_white.png │ │ │ ├── durban_ic_rotation_90_left_white.png │ │ │ └── durban_ic_rotation_90_right_white.png │ │ ├── drawable-xxxhdpi │ │ │ ├── durban_ic_back_white.png │ │ │ ├── durban_ic_done_white.png │ │ │ ├── durban_ic_scale_in_white.png │ │ │ ├── durban_ic_scale_out_white.png │ │ │ ├── durban_ic_rotation_90_left_white.png │ │ │ └── durban_ic_rotation_90_right_white.png │ │ ├── drawable │ │ │ └── durban_vector_ic_crop_logo.xml │ │ ├── values-zh │ │ │ └── strings.xml │ │ ├── values-zh-rHK │ │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ │ └── strings.xml │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── dimens.xml │ │ │ ├── colors.xml │ │ │ ├── attrs.xml │ │ │ └── styles.xml │ │ ├── menu │ │ │ └── durban_menu_activity.xml │ │ └── layout │ │ │ ├── durban_crop_view.xml │ │ │ └── durban_activity_photobox.xml │ │ ├── java │ │ └── com │ │ │ └── yanzhenjie │ │ │ └── durban │ │ │ ├── callback │ │ │ ├── CropBoundsChangeListener.java │ │ │ ├── OverlayViewChangeListener.java │ │ │ ├── BitmapCropCallback.java │ │ │ └── BitmapLoadCallback.java │ │ │ ├── error │ │ │ └── StorageError.java │ │ │ ├── util │ │ │ ├── CubicEasing.java │ │ │ ├── DurbanUtils.java │ │ │ ├── FileUtils.java │ │ │ ├── FastBitmapDrawable.java │ │ │ ├── RectUtils.java │ │ │ ├── RotationGestureDetector.java │ │ │ ├── BitmapLoadUtils.java │ │ │ ├── EglUtils.java │ │ │ └── ImageHeaderParser.java │ │ │ ├── model │ │ │ ├── ImageState.java │ │ │ ├── CropParameters.java │ │ │ ├── ExifInfo.java │ │ │ └── AspectRatio.java │ │ │ ├── DurbanConfig.java │ │ │ ├── view │ │ │ ├── CropView.java │ │ │ ├── GestureCropImageView.java │ │ │ └── TransformImageView.java │ │ │ ├── Controller.java │ │ │ ├── task │ │ │ ├── BitmapLoadTask.java │ │ │ └── BitmapCropTask.java │ │ │ ├── Durban.java │ │ │ └── DurbanActivity.java │ │ └── AndroidManifest.xml └── build.gradle ├── gradle.properties ├── config.gradle ├── gradlew.bat ├── maven.gradle ├── README-CN.md ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':durban', ':sample' -------------------------------------------------------------------------------- /image/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/image/1.gif -------------------------------------------------------------------------------- /image/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/image/2.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | local.properties 2 | *.idea 3 | *.iml 4 | build 5 | captures 6 | .gradle 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_image_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/drawable-hdpi/ic_image_white.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_image_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/drawable-xhdpi/ic_image_white.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_image_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/drawable-xxhdpi/ic_image_white.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_image_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/drawable-xxxhdpi/ic_image_white.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_back_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_back_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_done_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_back_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_back_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_done_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_back_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_back_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_done_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_scale_in_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_scale_in_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_back_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_back_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_done_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_done_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_scale_out_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_scale_out_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_scale_in_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_scale_in_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_scale_out_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_scale_out_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_scale_in_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_scale_in_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_scale_out_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_scale_out_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_scale_in_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_scale_in_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_scale_out_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_scale_out_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_rotation_90_left_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_rotation_90_left_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-hdpi/durban_ic_rotation_90_right_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-hdpi/durban_ic_rotation_90_right_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_rotation_90_left_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_rotation_90_left_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xhdpi/durban_ic_rotation_90_right_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xhdpi/durban_ic_rotation_90_right_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_rotation_90_left_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_rotation_90_left_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxhdpi/durban_ic_rotation_90_right_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxhdpi/durban_ic_rotation_90_right_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_rotation_90_left_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_rotation_90_left_white.png -------------------------------------------------------------------------------- /durban/src/main/res/drawable-xxxhdpi/durban_ic_rotation_90_right_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanzhenjie/Durban/HEAD/durban/src/main/res/drawable-xxxhdpi/durban_ic_rotation_90_right_white.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Mar 15 16:17:48 EET 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip 7 | -------------------------------------------------------------------------------- /durban/src/main/res/drawable/durban_vector_ic_crop_logo.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 5dp 19 | -------------------------------------------------------------------------------- /durban/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: rootProject.ext.plugins.library 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.android.compileSdkVersion 5 | buildToolsVersion rootProject.ext.android.buildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion rootProject.ext.android.libraryMinSdkVersion 9 | targetSdkVersion rootProject.ext.android.targetSdkVersion 10 | 11 | vectorDrawables.useSupportLibrary = true 12 | } 13 | 14 | compileOptions { 15 | sourceCompatibility JavaVersion.VERSION_1_7 16 | targetCompatibility JavaVersion.VERSION_1_7 17 | } 18 | 19 | resourcePrefix 'durban_' 20 | } 21 | 22 | dependencies { 23 | compile rootProject.ext.dependencies.appcompat 24 | compile rootProject.ext.dependencies.loading 25 | } 26 | 27 | apply from: "../maven.gradle" 28 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Durban 19 | Album 20 | 21 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: rootProject.ext.plugins.application 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.android.compileSdkVersion 5 | buildToolsVersion rootProject.ext.android.buildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId rootProject.ext.android.applicationId 9 | minSdkVersion rootProject.ext.android.sampleMinSdkVersion 10 | targetSdkVersion rootProject.ext.android.targetSdkVersion 11 | versionCode rootProject.ext.android.versionCode 12 | versionName rootProject.ext.android.versionName 13 | } 14 | } 15 | 16 | dependencies { 17 | compile fileTree(dir: 'libs', include: ['*.jar']) 18 | compile rootProject.ext.dependencies.appcompat 19 | compile rootProject.ext.dependencies.design 20 | compile rootProject.ext.dependencies.album 21 | compile rootProject.ext.dependencies.durban 22 | } 23 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/callback/CropBoundsChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.callback; 17 | 18 | 19 | /** 20 | * Update by Yan Zhenjie on 2017/5/23. 21 | */ 22 | public interface CropBoundsChangeListener { 23 | 24 | void onCropAspectRatioChanged(float cropRatio); 25 | 26 | } -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/callback/OverlayViewChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.callback; 17 | 18 | import android.graphics.RectF; 19 | 20 | /** 21 | * Update by Yan Zhenjie on 2017/5/23. 22 | */ 23 | public interface OverlayViewChangeListener { 24 | 25 | void onCropRectUpdated(RectF cropRect); 26 | 27 | } -------------------------------------------------------------------------------- /sample/src/main/res/drawable/decoration_white.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 21 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_main_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | -------------------------------------------------------------------------------- /durban/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 图片裁剪 20 | 确定 21 | 22 | 旋转 23 | 缩放 24 | 25 | 26 | -------------------------------------------------------------------------------- /durban/src/main/res/values-zh-rHK/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 图片裁剪 20 | 确定 21 | 22 | 旋轉 23 | 縮放 24 | 25 | 26 | -------------------------------------------------------------------------------- /durban/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 图片裁剪 20 | 确定 21 | 22 | 旋轉 23 | 縮放 24 | 25 | 26 | -------------------------------------------------------------------------------- /durban/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ImageCrop 20 | OK 21 | 22 | Rotation 23 | Scale 24 | 25 | 26 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | #2196F3 19 | #1E88E5 20 | @color/colorPrimary 21 | #FF2B2B2B 22 | 23 | #FFFFFFFF 24 | 25 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/error/StorageError.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.error; 17 | 18 | /** 19 | * Created by Yan Zhenjie on 2017/5/23. 20 | */ 21 | public class StorageError extends Exception { 22 | 23 | public StorageError(String message) { 24 | super(message); 25 | } 26 | 27 | public StorageError(Throwable cause) { 28 | super(cause); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/callback/BitmapCropCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.callback; 17 | 18 | import android.support.annotation.NonNull; 19 | 20 | /** 21 | * Update by Yan Zhenjie on 2017/5/23. 22 | */ 23 | public interface BitmapCropCallback { 24 | 25 | void onBitmapCropped(@NonNull String imagePath, int imageWidth, int imageHeight); 26 | 27 | void onCropFailure(@NonNull Throwable t); 28 | } -------------------------------------------------------------------------------- /durban/src/main/res/menu/durban_menu_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 25 | 26 | -------------------------------------------------------------------------------- /durban/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 1dp 20 | 10dp 21 | 16dp 22 | 30dp 23 | 70dp 24 | 100dp 25 | 200dp 26 | 27 | 28 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/callback/BitmapLoadCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.callback; 17 | 18 | import android.graphics.Bitmap; 19 | import android.support.annotation.NonNull; 20 | 21 | import com.yanzhenjie.durban.model.ExifInfo; 22 | 23 | 24 | /** 25 | * Update by Yan Zhenjie on 2017/5/23. 26 | */ 27 | public interface BitmapLoadCallback { 28 | 29 | void onSuccessfully(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo); 30 | 31 | void onFailure(); 32 | 33 | } -------------------------------------------------------------------------------- /sample/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 24 | 25 | 28 | 29 | 32 | 33 | 36 | 37 | 38 | 42 | 43 | 47 | 48 | 52 | 53 | 56 | 57 | 61 | 62 | 66 | 67 | 71 | 72 | 76 | 77 | 81 | 82 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/util/RectUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.util; 17 | 18 | import android.graphics.RectF; 19 | 20 | /** 21 | * Update by Yan Zhenjie on 2017/5/23. 22 | */ 23 | public class RectUtils { 24 | 25 | /** 26 | * Gets a float array of the 2D coordinates representing a rectangles 27 | * corners. 28 | * The order of the corners in the float array is: 29 | * 0------->1 30 | * ^ | 31 | * | | 32 | * | v 33 | * 3<-------2 34 | * 35 | * @param r the rectangle to get the corners of 36 | * @return the float array of corners (8 floats) 37 | */ 38 | public static float[] getCornersFromRect(RectF r) { 39 | return new float[]{ 40 | r.left, r.top, 41 | r.right, r.top, 42 | r.right, r.bottom, 43 | r.left, r.bottom 44 | }; 45 | } 46 | 47 | /** 48 | * Gets a float array of two lengths representing a rectangles width and height 49 | * The order of the corners in the input float array is: 50 | * 0------->1 51 | * ^ | 52 | * | | 53 | * | v 54 | * 3<-------2 55 | * 56 | * @param corners the float array of corners (8 floats) 57 | * @return the float array of width and height (2 floats) 58 | */ 59 | public static float[] getRectSidesFromCorners(float[] corners) { 60 | return new float[]{(float) Math.sqrt(Math.pow(corners[0] - corners[2], 2) + Math.pow(corners[1] - corners[3], 2)), 61 | (float) Math.sqrt(Math.pow(corners[2] - corners[4], 2) + Math.pow(corners[3] - corners[5], 2))}; 62 | } 63 | 64 | public static float[] getCenterFromRect(RectF r) { 65 | return new float[]{r.centerX(), r.centerY()}; 66 | } 67 | 68 | /** 69 | * Takes an array of 2D coordinates representing corners and returns the 70 | * smallest rectangle containing those coordinates. 71 | * 72 | * @param array array of 2D coordinates 73 | * @return smallest rectangle containing coordinates 74 | */ 75 | public static RectF trapToRect(float[] array) { 76 | RectF r = new RectF(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, 77 | Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); 78 | for (int i = 1; i < array.length; i += 2) { 79 | float x = Math.round(array[i - 1] * 10) / 10.f; 80 | float y = Math.round(array[i] * 10) / 10.f; 81 | r.left = (x < r.left) ? x : r.left; 82 | r.top = (y < r.top) ? y : r.top; 83 | r.right = (x > r.right) ? x : r.right; 84 | r.bottom = (y > r.bottom) ? y : r.bottom; 85 | } 86 | r.sort(); 87 | return r; 88 | } 89 | 90 | } -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/Controller.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban; 17 | 18 | import android.os.Parcel; 19 | import android.os.Parcelable; 20 | 21 | /** 22 | *

Control panel configuration.

23 | * Created by Yan Zhenjie on 2017/5/30. 24 | */ 25 | public class Controller implements Parcelable { 26 | 27 | private Controller(Parcel in) { 28 | this.enable = in.readByte() != 0; 29 | this.rotation = in.readByte() != 0; 30 | this.rotationTitle = in.readByte() != 0; 31 | this.scale = in.readByte() != 0; 32 | this.scaleTitle = in.readByte() != 0; 33 | } 34 | 35 | @Override 36 | public void writeToParcel(Parcel dest, int flags) { 37 | dest.writeByte((byte) (enable ? 1 : 0)); 38 | dest.writeByte((byte) (rotation ? 1 : 0)); 39 | dest.writeByte((byte) (rotationTitle ? 1 : 0)); 40 | dest.writeByte((byte) (scale ? 1 : 0)); 41 | dest.writeByte((byte) (scaleTitle ? 1 : 0)); 42 | } 43 | 44 | @Override 45 | public int describeContents() { 46 | return 0; 47 | } 48 | 49 | public static final Creator CREATOR = new Creator() { 50 | @Override 51 | public Controller createFromParcel(Parcel in) { 52 | return new Controller(in); 53 | } 54 | 55 | @Override 56 | public Controller[] newArray(int size) { 57 | return new Controller[size]; 58 | } 59 | }; 60 | 61 | /** 62 | * Create a Builder. 63 | */ 64 | public static Builder newBuilder() { 65 | return new Builder(); 66 | } 67 | 68 | private boolean enable; 69 | 70 | private boolean rotation; 71 | private boolean rotationTitle; 72 | 73 | private boolean scale; 74 | private boolean scaleTitle; 75 | 76 | private Controller(Builder builder) { 77 | this.enable = builder.enable; 78 | this.rotation = builder.rotation; 79 | this.rotationTitle = builder.rotationTitle; 80 | this.scale = builder.scale; 81 | this.scaleTitle = builder.scaleTitle; 82 | } 83 | 84 | public boolean isEnable() { 85 | return enable; 86 | } 87 | 88 | public boolean isRotation() { 89 | return rotation; 90 | } 91 | 92 | public boolean isRotationTitle() { 93 | return rotationTitle; 94 | } 95 | 96 | public boolean isScale() { 97 | return scale; 98 | } 99 | 100 | public boolean isScaleTitle() { 101 | return scaleTitle; 102 | } 103 | 104 | public static final class Builder { 105 | 106 | private boolean enable = true; 107 | 108 | private boolean rotation = true; 109 | private boolean rotationTitle = true; 110 | 111 | private boolean scale = true; 112 | private boolean scaleTitle = true; 113 | 114 | private Builder() { 115 | } 116 | 117 | public Builder enable(boolean enable) { 118 | this.enable = enable; 119 | return this; 120 | } 121 | 122 | public Builder rotation(boolean rotation) { 123 | this.rotation = rotation; 124 | return this; 125 | } 126 | 127 | public Builder rotationTitle(boolean rotationTitle) { 128 | this.rotationTitle = rotationTitle; 129 | return this; 130 | } 131 | 132 | public Builder scale(boolean scale) { 133 | this.scale = scale; 134 | return this; 135 | } 136 | 137 | public Builder scaleTitle(boolean scaleTitle) { 138 | this.scaleTitle = scaleTitle; 139 | return this; 140 | } 141 | 142 | public Controller build() { 143 | return new Controller(this); 144 | } 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | `Durban`是一个MD风格的图片裁剪工具,结合图片选择库[Album](https://github.com/yanzhenjie/Album)是最好的搭配。 2 | 3 | 它来自[uCrop](https://github.com/Yalantis/uCrop),我修改了大部分代码,让它使用起来更加友好,**重点是一次可以裁剪多张图片**,同时更适合和[Album](https://github.com/yanzhenjie/Album)结合使用。 4 | 5 | QQ技术交流群:[547839514](https://jq.qq.com/?_wv=1027&k=49Xx0JX) 6 | 图片选择库推荐:[https://github.com/yanzhenjie/Album](https://github.com/yanzhenjie/Album) 7 | 8 | **[English Document](./README.md)** 9 | 10 | # 截图 11 | 12 | 13 | # 依赖 14 | * Gradle: 15 | ```groovy 16 | compile 'com.yanzhenjie:durban:1.0.1' 17 | ``` 18 | 19 | * Maven: 20 | ```xml 21 | 22 | com.yanzhenjie 23 | durban 24 | 1.0.1 25 | pom 26 | 27 | ``` 28 | 29 | # 使用介绍 30 | 支持在`Activity`、`android.app.Fragment`、`android.support.v4.app.Fragment`中调用: 31 | ```java 32 | Durban.with(Activity); 33 | Durban.with(android.app.Fragment); 34 | Durban.with(android.support.v4.app.Fragment); 35 | ``` 36 | 37 | 调用示例: 38 | ```java 39 | Durban.with(this) 40 | // 裁剪界面的标题。 41 | .title("Crop") 42 | .statusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)) 43 | .toolBarColor(ContextCompat.getColor(this, R.color.colorPrimary)) 44 | .navigationBarColor(ContextCompat.getColor(this, R.color.colorPrimaryBlack)) 45 | // 图片路径list或者数组。 46 | .inputImagePaths(imagePathList) 47 | // 图片输出文件夹路径。 48 | .outputDirectory(cropDirectory) 49 | // 裁剪图片输出的最大宽高。 50 | .maxWidthHeight(500, 500) 51 | // 裁剪时的宽高比。 52 | .aspectRatio(1, 1) 53 | // 图片压缩格式:JPEG、PNG。 54 | .compressFormat(Durban.COMPRESS_JPEG) 55 | // 图片压缩质量,请参考:Bitmap#compress(Bitmap.CompressFormat, int, OutputStream) 56 | .compressQuality(90) 57 | // 裁剪时的手势支持:ROTATE, SCALE, ALL, NONE. 58 | .gesture(Durban.GESTURE_ALL) 59 | .controller( 60 | Controller.newBuilder() 61 | .enable(false) // 是否开启控制面板。 62 | .rotation(true) // 是否有旋转按钮。 63 | .rotationTitle(true) // 旋转控制按钮上面的标题。 64 | .scale(true) // 是否有缩放按钮。 65 | .scaleTitle(true) // 缩放控制按钮上面的标题。 66 | .build()) // 创建控制面板配置。 67 | .requestCode(200) 68 | .start(); 69 | ``` 70 | **注意**:`inputImagePaths()`方法的参数可以是路径list,也可以是路径数组: 71 | ```java 72 | // 例如: 73 | List imagePathList = ... 74 | Durban.with(this) 75 | ... 76 | .inputImagePaths(imagePathList) 77 | ... 78 | 79 | // 或者: 80 | String[] imagePathArray = ... 81 | Durban.with(this) 82 | ... 83 | .inputImagePaths(imagePathArray) 84 | ... 85 | 86 | // 或者: 87 | Durban.with(this) 88 | ... 89 | .inputImagePaths(path1, path2, path3...) 90 | ... 91 | ``` 92 | 93 | 然后重写`onActivityResult()`方法接受剪裁结果: 94 | ```java 95 | @Override 96 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 97 | switch (requestCode) { 98 | case 200: { 99 | // 解析剪切结果: 100 | if (resultCode != RESULT_OK) { 101 | ArrayList mImageList = Durban.parseResult(data); 102 | } else { 103 | // TODO 其它处理... 104 | } 105 | break; 106 | } 107 | } 108 | } 109 | ``` 110 | 111 | ## 意外的语言配置 112 | Durban支持英文、简体中文和繁体中文,如果需要支持其它语言,你可以在`values-xxx`(xxx的意思是语言标记,比如:values-zh、values-zh-rHK)复写Durban的`string.xml`的资源,并且在`Application#onCreate()`中配置语言: 113 | ```java 114 | Durban.initialize( 115 | DurbanConfig.newBuilder(this) 116 | .setLocale(...) 117 | .build() 118 | ); 119 | ``` 120 | 121 | # 混淆规则 122 | Durban可以被完全混淆,如果出现了问题请添加如下混淆规则: 123 | ```txt 124 | -dontwarn com.yanzhenjie.curban.** 125 | -keep class com.yanzhenjie.curban.**{*;} 126 | 127 | -dontwarn com.yanzhenjie.loading.** 128 | -keep class com.yanzhenjie.loading.**{*;} 129 | ``` 130 | 131 | # 感谢 132 | 1. [uCrop](https://github.com/Yalantis/uCrop) 133 | 2. [Album](https://github.com/yanzhenjie/Album) 134 | 135 | # License 136 | ```text 137 | Copyright 2017 Yan Zhenjie 138 | 139 | Licensed under the Apache License, Version 2.0 (the "License"); 140 | you may not use this file except in compliance with the License. 141 | You may obtain a copy of the License at 142 | 143 | http://www.apache.org/licenses/LICENSE-2.0 144 | 145 | Unless required by applicable law or agreed to in writing, software 146 | distributed under the License is distributed on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 148 | See the License for the specific language governing permissions and 149 | limitations under the License. 150 | ``` -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/task/BitmapLoadTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.task; 17 | 18 | import android.content.Context; 19 | import android.graphics.Bitmap; 20 | import android.graphics.BitmapFactory; 21 | import android.graphics.Matrix; 22 | import android.os.AsyncTask; 23 | import android.support.annotation.NonNull; 24 | 25 | import com.yanzhenjie.durban.callback.BitmapLoadCallback; 26 | import com.yanzhenjie.durban.model.ExifInfo; 27 | import com.yanzhenjie.durban.util.BitmapLoadUtils; 28 | import com.yanzhenjie.loading.dialog.LoadingDialog; 29 | 30 | /** 31 | *

32 | * Asynchronous loading of images. 33 | *

34 | * Create by Yan Zhenjie on 2017/5/22. 35 | */ 36 | public class BitmapLoadTask extends AsyncTask { 37 | 38 | static class BitmapWorkerResult { 39 | 40 | final Bitmap bitmap; 41 | final ExifInfo exifInfo; 42 | 43 | BitmapWorkerResult(Bitmap bitmapResult, ExifInfo exifInfo) { 44 | this.bitmap = bitmapResult; 45 | this.exifInfo = exifInfo; 46 | } 47 | } 48 | 49 | private final LoadingDialog mDialog; 50 | private final int mRequiredWidth; 51 | private final int mRequiredHeight; 52 | 53 | private final BitmapLoadCallback mCallback; 54 | 55 | public BitmapLoadTask( 56 | Context context, 57 | int requiredWidth, 58 | int requiredHeight, 59 | BitmapLoadCallback loadCallback) { 60 | mDialog = new LoadingDialog(context); 61 | mRequiredWidth = requiredWidth; 62 | mRequiredHeight = requiredHeight; 63 | mCallback = loadCallback; 64 | } 65 | 66 | @Override 67 | protected void onPreExecute() { 68 | if (!mDialog.isShowing()) mDialog.show(); 69 | } 70 | 71 | @Override 72 | protected void onPostExecute(@NonNull BitmapLoadTask.BitmapWorkerResult result) { 73 | if (mDialog.isShowing()) mDialog.dismiss(); 74 | if (result.bitmap == null) { 75 | mCallback.onFailure(); 76 | } else { 77 | mCallback.onSuccessfully(result.bitmap, result.exifInfo); 78 | } 79 | } 80 | 81 | @Override 82 | protected BitmapLoadTask.BitmapWorkerResult doInBackground(String... params) { 83 | String imagePath = params[0]; 84 | 85 | final BitmapFactory.Options options = new BitmapFactory.Options(); 86 | options.inJustDecodeBounds = true; 87 | BitmapFactory.decodeFile(imagePath, options); 88 | if (options.outWidth == -1 || options.outHeight == -1) 89 | return new BitmapLoadTask.BitmapWorkerResult(null, null); 90 | 91 | options.inSampleSize = BitmapLoadUtils.calculateInSampleSize(options, mRequiredWidth, mRequiredHeight); 92 | options.inJustDecodeBounds = false; 93 | 94 | Bitmap decodeSampledBitmap = null; 95 | 96 | boolean decodeAttemptSuccess = false; 97 | while (!decodeAttemptSuccess) { 98 | try { 99 | decodeSampledBitmap = BitmapFactory.decodeFile(imagePath, options); 100 | decodeAttemptSuccess = true; 101 | } catch (Throwable error) { 102 | options.inSampleSize *= 2; 103 | } 104 | } 105 | 106 | int exifOrientation = BitmapLoadUtils.getExifOrientation(imagePath); 107 | int exifDegrees = BitmapLoadUtils.exifToDegrees(exifOrientation); 108 | int exifTranslation = BitmapLoadUtils.exifToTranslation(exifOrientation); 109 | 110 | ExifInfo exifInfo = new ExifInfo(exifOrientation, exifDegrees, exifTranslation); 111 | 112 | Matrix matrix = new Matrix(); 113 | if (exifDegrees != 0) 114 | matrix.preRotate(exifDegrees); 115 | if (exifTranslation != 1) 116 | matrix.postScale(exifTranslation, 1); 117 | if (!matrix.isIdentity()) 118 | return new BitmapLoadTask.BitmapWorkerResult(BitmapLoadUtils.transformBitmap(decodeSampledBitmap, matrix), exifInfo); 119 | return new BitmapLoadTask.BitmapWorkerResult(decodeSampledBitmap, exifInfo); 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/util/RotationGestureDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.util; 17 | 18 | import android.support.annotation.NonNull; 19 | import android.view.MotionEvent; 20 | 21 | /** 22 | * Update by Yan Zhenjie on 2017/5/23. 23 | */ 24 | public class RotationGestureDetector { 25 | 26 | private static final int INVALID_POINTER_INDEX = -1; 27 | 28 | private float fX, fY, sX, sY; 29 | 30 | private int mPointerIndex1, mPointerIndex2; 31 | private float mAngle; 32 | private boolean mIsFirstTouch; 33 | 34 | private OnRotationGestureListener mListener; 35 | 36 | public RotationGestureDetector(OnRotationGestureListener listener) { 37 | mListener = listener; 38 | mPointerIndex1 = INVALID_POINTER_INDEX; 39 | mPointerIndex2 = INVALID_POINTER_INDEX; 40 | } 41 | 42 | public float getAngle() { 43 | return mAngle; 44 | } 45 | 46 | public boolean onTouchEvent(@NonNull MotionEvent event) { 47 | switch (event.getActionMasked()) { 48 | case MotionEvent.ACTION_DOWN: 49 | sX = event.getX(); 50 | sY = event.getY(); 51 | mPointerIndex1 = event.findPointerIndex(event.getPointerId(0)); 52 | mAngle = 0; 53 | mIsFirstTouch = true; 54 | break; 55 | case MotionEvent.ACTION_POINTER_DOWN: 56 | fX = event.getX(); 57 | fY = event.getY(); 58 | mPointerIndex2 = event.findPointerIndex(event.getPointerId(event.getActionIndex())); 59 | mAngle = 0; 60 | mIsFirstTouch = true; 61 | break; 62 | case MotionEvent.ACTION_MOVE: 63 | if (mPointerIndex1 != INVALID_POINTER_INDEX && mPointerIndex2 != INVALID_POINTER_INDEX && event.getPointerCount 64 | () > mPointerIndex2) { 65 | float nfX, nfY, nsX, nsY; 66 | 67 | nsX = event.getX(mPointerIndex1); 68 | nsY = event.getY(mPointerIndex1); 69 | nfX = event.getX(mPointerIndex2); 70 | nfY = event.getY(mPointerIndex2); 71 | 72 | if (mIsFirstTouch) { 73 | mAngle = 0; 74 | mIsFirstTouch = false; 75 | } else { 76 | calculateAngleBetweenLines(fX, fY, sX, sY, nfX, nfY, nsX, nsY); 77 | } 78 | 79 | if (mListener != null) { 80 | mListener.onRotation(this); 81 | } 82 | fX = nfX; 83 | fY = nfY; 84 | sX = nsX; 85 | sY = nsY; 86 | } 87 | break; 88 | case MotionEvent.ACTION_UP: 89 | mPointerIndex1 = INVALID_POINTER_INDEX; 90 | break; 91 | case MotionEvent.ACTION_POINTER_UP: 92 | mPointerIndex2 = INVALID_POINTER_INDEX; 93 | break; 94 | } 95 | return true; 96 | } 97 | 98 | private float calculateAngleBetweenLines(float fx1, float fy1, float fx2, float fy2, 99 | float sx1, float sy1, float sx2, float sy2) { 100 | return calculateAngleDelta( 101 | (float) Math.toDegrees((float) Math.atan2((fy1 - fy2), (fx1 - fx2))), 102 | (float) Math.toDegrees((float) Math.atan2((sy1 - sy2), (sx1 - sx2)))); 103 | } 104 | 105 | private float calculateAngleDelta(float angleFrom, float angleTo) { 106 | mAngle = angleTo % 360.0f - angleFrom % 360.0f; 107 | 108 | if (mAngle < -180.0f) { 109 | mAngle += 360.0f; 110 | } else if (mAngle > 180.0f) { 111 | mAngle -= 360.0f; 112 | } 113 | 114 | return mAngle; 115 | } 116 | 117 | public static class SimpleOnRotationGestureListener implements OnRotationGestureListener { 118 | 119 | @Override 120 | public boolean onRotation(RotationGestureDetector rotationDetector) { 121 | return false; 122 | } 123 | } 124 | 125 | public interface OnRotationGestureListener { 126 | 127 | boolean onRotation(RotationGestureDetector rotationDetector); 128 | } 129 | 130 | } -------------------------------------------------------------------------------- /durban/src/main/res/layout/durban_activity_photobox.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 22 | 23 | 27 | 28 | 35 | 36 | 39 | 40 | 44 | 45 | 49 | 50 | 51 | 52 | 54 | 55 | 58 | 59 | 63 | 64 | 65 | 68 | 69 | 73 | 74 | 75 | 78 | 79 | 83 | 84 | 85 | 88 | 89 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 112 | 113 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `Durban` is an MD-style picture cropping tool that combines the image selection tool with [Album](https://github.com/yanzhenjie/Album) Use is the best. 2 | 3 | It comes from the [uCrop](https://github.com/Yalantis/uCrop), but I modified most of the code, let it use more friendly. 4 | 5 | Recommended image selection library: [https://github.com/yanzhenjie/Album](https://github.com/yanzhenjie/Album) 6 | 7 | [中文文档](./README-CN.md) 8 | 9 | # Screenshot 10 | 11 | 12 | # Dependencies 13 | * Gradle: 14 | ```groovy 15 | compile 'com.yanzhenjie:durban:1.0.1' 16 | ``` 17 | 18 | * Maven: 19 | ```xml 20 | 21 | com.yanzhenjie 22 | durban 23 | 1.0.1 24 | pom 25 | 26 | ``` 27 | 28 | # Usage 29 | Support `Activity`, `android.app.Fragment`, `android.support.v4.app.Fragment`: 30 | ```java 31 | Durban.with(Activity); 32 | Durban.with(android.app.Fragment); 33 | Durban.with(android.support.v4.app.Fragment); 34 | ``` 35 | 36 | How to call clipping: 37 | ```java 38 | Durban.with(this) 39 | // Che title of the UI. 40 | .title("Crop") 41 | .statusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)) 42 | .toolBarColor(ContextCompat.getColor(this, R.color.colorPrimary)) 43 | .navigationBarColor(ContextCompat.getColor(this, R.color.colorPrimaryBlack)) 44 | // Image path list/array. 45 | .inputImagePaths(imagePathList) 46 | // Image output directory. 47 | .outputDirectory(cropDirectory) 48 | // Image size limit. 49 | .maxWidthHeight(500, 500) 50 | // Aspect ratio. 51 | .aspectRatio(1, 1) 52 | // Output format: JPEG, PNG. 53 | .compressFormat(Durban.COMPRESS_JPEG) 54 | // Compress quality, see Bitmap#compress(Bitmap.CompressFormat, int, OutputStream) 55 | .compressQuality(90) 56 | // Gesture: ROTATE, SCALE, ALL, NONE. 57 | .gesture(Durban.GESTURE_ALL) 58 | .controller( 59 | Controller.newBuilder() // Create Builder of Controller. 60 | .enable(false) // Enable the control panel. 61 | .rotation(true) // Rotation button. 62 | .rotationTitle(true) // Rotation button title. 63 | .scale(true) // Scale button. 64 | .scaleTitle(true) // Scale button title. 65 | .build()) // Create Controller Config. 66 | .requestCode(200) 67 | .start(); 68 | ``` 69 | **Note:** `inputImagePaths()` method can be a picture path list, can also be a picture path array, such as: 70 | ```java 71 | // Such as: 72 | List imagePathList = ... 73 | Durban.with(this) 74 | ... 75 | .inputImagePaths(imagePathList) 76 | ... 77 | 78 | // OR: 79 | String[] imagePathArray = ... 80 | Durban.with(this) 81 | ... 82 | .inputImagePaths(imagePathArray) 83 | ... 84 | 85 | // OR: 86 | Durban.with(this) 87 | ... 88 | .inputImagePaths(path1, path2, path3...) 89 | ... 90 | ``` 91 | 92 | And then override the `onActivityResult()` method to accept the cropping results: 93 | ```java 94 | @Override 95 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 96 | switch (requestCode) { 97 | case 200: { 98 | // Analyze the list of paths after cropping. 99 | if (resultCode != RESULT_OK) { 100 | ArrayList mImageList = Durban.parseResult(data); 101 | } else { 102 | // TODO other... 103 | } 104 | break; 105 | } 106 | } 107 | } 108 | ``` 109 | 110 | ## Unexpected language configuration 111 | Durban supports English, Chinese Simplified and Traditional Chinese, and if you need to configure other languages, you can rewrite the resources in Durban's `String.xml` in `value-xxx`(xxx means other languages, such as: values-zh, values-zh-rHK), and use the following configuration language in `Application#onCreate()`: 112 | ```java 113 | Durban.initialize( 114 | DurbanConfig.newBuilder(this) 115 | .setLocale(...) 116 | .build() 117 | ); 118 | ``` 119 | 120 | # Proguard-rules 121 | Durban can be completely confusing, if there is a problem, add the rule to the proguard-rules: 122 | ```txt 123 | -dontwarn com.yanzhenjie.curban.** 124 | -keep class com.yanzhenjie.curban.**{*;} 125 | 126 | -dontwarn com.yanzhenjie.loading.** 127 | -keep class com.yanzhenjie.loading.**{*;} 128 | ``` 129 | 130 | # Thanks 131 | 1. [uCrop](https://github.com/Yalantis/uCrop) 132 | 2. [Album](https://github.com/yanzhenjie/Album) 133 | 134 | # License 135 | ```text 136 | Copyright 2017 Yan Zhenjie 137 | 138 | Licensed under the Apache License, Version 2.0 (the "License"); 139 | you may not use this file except in compliance with the License. 140 | You may obtain a copy of the License at 141 | 142 | http://www.apache.org/licenses/LICENSE-2.0 143 | 144 | Unless required by applicable law or agreed to in writing, software 145 | distributed under the License is distributed on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 147 | See the License for the specific language governing permissions and 148 | limitations under the License. 149 | ``` -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/util/BitmapLoadUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.util; 17 | 18 | import android.content.Context; 19 | import android.graphics.Bitmap; 20 | import android.graphics.BitmapFactory; 21 | import android.graphics.Matrix; 22 | import android.graphics.Point; 23 | import android.media.ExifInterface; 24 | import android.os.Build; 25 | import android.support.annotation.NonNull; 26 | import android.view.Display; 27 | import android.view.WindowManager; 28 | 29 | import java.io.FileInputStream; 30 | import java.io.IOException; 31 | import java.io.InputStream; 32 | 33 | /** 34 | * Update by Yan Zhenjie on 2017/5/23. 35 | */ 36 | public class BitmapLoadUtils { 37 | 38 | public static Bitmap transformBitmap(@NonNull Bitmap bitmap, @NonNull Matrix transformMatrix) { 39 | try { 40 | return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), transformMatrix, true); 41 | } catch (OutOfMemoryError error) { 42 | return bitmap; 43 | } 44 | } 45 | 46 | public static int calculateInSampleSize(@NonNull BitmapFactory.Options options, int reqWidth, int reqHeight) { 47 | // Raw height and width of image 48 | final int height = options.outHeight; 49 | final int width = options.outWidth; 50 | int inSampleSize = 1; 51 | 52 | if (height > reqHeight || width > reqWidth) { 53 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both 54 | // height and width lower or equal to the requested height and width. 55 | while ((height / inSampleSize) > reqHeight || (width / inSampleSize) > reqWidth) { 56 | inSampleSize *= 2; 57 | } 58 | } 59 | return inSampleSize; 60 | } 61 | 62 | public static int getExifOrientation(@NonNull String imagePath) { 63 | int orientation = ExifInterface.ORIENTATION_UNDEFINED; 64 | try { 65 | InputStream stream = new FileInputStream(imagePath); 66 | orientation = new ImageHeaderParser(stream).getOrientation(); 67 | stream.close(); 68 | } catch (IOException e) { 69 | e.printStackTrace(); 70 | } 71 | return orientation; 72 | } 73 | 74 | public static int exifToDegrees(int exifOrientation) { 75 | int rotation; 76 | switch (exifOrientation) { 77 | case ExifInterface.ORIENTATION_ROTATE_90: 78 | case ExifInterface.ORIENTATION_TRANSPOSE: 79 | rotation = 90; 80 | break; 81 | case ExifInterface.ORIENTATION_ROTATE_180: 82 | case ExifInterface.ORIENTATION_FLIP_VERTICAL: 83 | rotation = 180; 84 | break; 85 | case ExifInterface.ORIENTATION_ROTATE_270: 86 | case ExifInterface.ORIENTATION_TRANSVERSE: 87 | rotation = 270; 88 | break; 89 | default: 90 | rotation = 0; 91 | } 92 | return rotation; 93 | } 94 | 95 | public static int exifToTranslation(int exifOrientation) { 96 | int translation; 97 | switch (exifOrientation) { 98 | case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: 99 | case ExifInterface.ORIENTATION_FLIP_VERTICAL: 100 | case ExifInterface.ORIENTATION_TRANSPOSE: 101 | case ExifInterface.ORIENTATION_TRANSVERSE: 102 | translation = -1; 103 | break; 104 | default: 105 | translation = 1; 106 | } 107 | return translation; 108 | } 109 | 110 | /** 111 | * This method calculates maximum size of both width and height of exception. 112 | * It is twice the device screen diagonal for default implementation (extra quality to zoom image). 113 | * Size cannot exceed max texture size. 114 | * 115 | * @return - max exception size in pixels. 116 | */ 117 | public static int calculateMaxBitmapSize(@NonNull Context context) { 118 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 119 | Display display = wm.getDefaultDisplay(); 120 | 121 | Point size = new Point(); 122 | int width, height; 123 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) { 124 | display.getSize(size); 125 | width = size.x; 126 | height = size.y; 127 | } else { 128 | width = display.getWidth(); 129 | height = display.getHeight(); 130 | } 131 | 132 | // Twice the device screen diagonal as default 133 | int maxBitmapSize = (int) Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); 134 | 135 | // Check for max texture size via GL 136 | final int maxTextureSize = EglUtils.getMaxTextureSize(); 137 | if (maxTextureSize > 0) { 138 | maxBitmapSize = Math.min(maxBitmapSize, maxTextureSize); 139 | } 140 | return maxBitmapSize; 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/util/EglUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.util; 17 | 18 | import android.annotation.TargetApi; 19 | import android.opengl.EGL14; 20 | import android.opengl.EGLConfig; 21 | import android.opengl.EGLContext; 22 | import android.opengl.EGLDisplay; 23 | import android.opengl.EGLSurface; 24 | import android.opengl.GLES10; 25 | import android.opengl.GLES20; 26 | import android.os.Build; 27 | import android.util.Log; 28 | 29 | import javax.microedition.khronos.egl.EGL10; 30 | 31 | /** 32 | * Update by Yan Zhenjie on 2017/5/23. 33 | */ 34 | public class EglUtils { 35 | 36 | private static final String TAG = "EglUtils"; 37 | 38 | private EglUtils() { 39 | 40 | } 41 | 42 | public static int getMaxTextureSize() { 43 | try { 44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 45 | return getMaxTextureEgl14(); 46 | } else { 47 | return getMaxTextureEgl10(); 48 | } 49 | } catch (Exception e) { 50 | Log.d(TAG, "getMaxTextureSize: ", e); 51 | return 0; 52 | } 53 | } 54 | 55 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) 56 | private static int getMaxTextureEgl14() { 57 | EGLDisplay dpy = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); 58 | int[] vers = new int[2]; 59 | EGL14.eglInitialize(dpy, vers, 0, vers, 1); 60 | 61 | int[] configAttr = { 62 | EGL14.EGL_COLOR_BUFFER_TYPE, EGL14.EGL_RGB_BUFFER, 63 | EGL14.EGL_LEVEL, 0, 64 | EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, 65 | EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT, 66 | EGL14.EGL_NONE 67 | }; 68 | EGLConfig[] configs = new EGLConfig[1]; 69 | int[] numConfig = new int[1]; 70 | EGL14.eglChooseConfig(dpy, configAttr, 0, 71 | configs, 0, 1, numConfig, 0); 72 | if (numConfig[0] == 0) { 73 | return 0; 74 | } 75 | EGLConfig config = configs[0]; 76 | 77 | int[] surfAttr = { 78 | EGL14.EGL_WIDTH, 64, 79 | EGL14.EGL_HEIGHT, 64, 80 | EGL14.EGL_NONE 81 | }; 82 | EGLSurface surf = EGL14.eglCreatePbufferSurface(dpy, config, surfAttr, 0); 83 | 84 | int[] ctxAttribute = { 85 | EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, 86 | EGL14.EGL_NONE 87 | }; 88 | EGLContext ctx = EGL14.eglCreateContext(dpy, config, EGL14.EGL_NO_CONTEXT, ctxAttribute, 0); 89 | 90 | EGL14.eglMakeCurrent(dpy, surf, surf, ctx); 91 | 92 | int[] maxSize = new int[1]; 93 | GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxSize, 0); 94 | 95 | EGL14.eglMakeCurrent(dpy, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, 96 | EGL14.EGL_NO_CONTEXT); 97 | EGL14.eglDestroySurface(dpy, surf); 98 | EGL14.eglDestroyContext(dpy, ctx); 99 | EGL14.eglTerminate(dpy); 100 | 101 | return maxSize[0]; 102 | } 103 | 104 | @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) 105 | private static int getMaxTextureEgl10() { 106 | EGL10 egl = (EGL10) javax.microedition.khronos.egl.EGLContext.getEGL(); 107 | 108 | javax.microedition.khronos.egl.EGLDisplay dpy = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); 109 | int[] vers = new int[2]; 110 | egl.eglInitialize(dpy, vers); 111 | 112 | int[] configAttr = { 113 | EGL10.EGL_COLOR_BUFFER_TYPE, EGL10.EGL_RGB_BUFFER, 114 | EGL10.EGL_LEVEL, 0, 115 | EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT, 116 | EGL10.EGL_NONE 117 | }; 118 | javax.microedition.khronos.egl.EGLConfig[] configs = new javax.microedition.khronos.egl.EGLConfig[1]; 119 | int[] numConfig = new int[1]; 120 | egl.eglChooseConfig(dpy, configAttr, configs, 1, numConfig); 121 | if (numConfig[0] == 0) { 122 | return 0; 123 | } 124 | javax.microedition.khronos.egl.EGLConfig config = configs[0]; 125 | 126 | int[] surfAttr = { 127 | EGL10.EGL_WIDTH, 64, 128 | EGL10.EGL_HEIGHT, 64, 129 | EGL10.EGL_NONE 130 | }; 131 | javax.microedition.khronos.egl.EGLSurface surf = egl.eglCreatePbufferSurface(dpy, config, surfAttr); 132 | final int EGL_CONTEXT_CLIENT_VERSION = 0x3098; // missing in EGL10 133 | int[] ctxAttribute = { 134 | EGL_CONTEXT_CLIENT_VERSION, 1, 135 | EGL10.EGL_NONE 136 | }; 137 | javax.microedition.khronos.egl.EGLContext ctx = egl.eglCreateContext(dpy, config, EGL10.EGL_NO_CONTEXT, ctxAttribute); 138 | egl.eglMakeCurrent(dpy, surf, surf, ctx); 139 | int[] maxSize = new int[1]; 140 | GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0); 141 | egl.eglMakeCurrent(dpy, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, 142 | EGL10.EGL_NO_CONTEXT); 143 | egl.eglDestroySurface(dpy, surf); 144 | egl.eglDestroyContext(dpy, ctx); 145 | egl.eglTerminate(dpy); 146 | 147 | return maxSize[0]; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/view/GestureCropImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.view; 17 | 18 | import android.content.Context; 19 | import android.util.AttributeSet; 20 | import android.view.GestureDetector; 21 | import android.view.MotionEvent; 22 | import android.view.ScaleGestureDetector; 23 | 24 | import com.yanzhenjie.durban.util.RotationGestureDetector; 25 | 26 | /** 27 | * Update by Yan Zhenjie on 2017/5/23. 28 | */ 29 | public class GestureCropImageView extends CropImageView { 30 | 31 | private static final int DOUBLE_TAP_ZOOM_DURATION = 200; 32 | 33 | private ScaleGestureDetector mScaleDetector; 34 | private RotationGestureDetector mRotateDetector; 35 | private GestureDetector mGestureDetector; 36 | 37 | private float mMidPntX, mMidPntY; 38 | 39 | private boolean mIsRotateEnabled = true, mIsScaleEnabled = true; 40 | private int mDoubleTapScaleSteps = 5; 41 | 42 | public GestureCropImageView(Context context) { 43 | super(context); 44 | } 45 | 46 | public GestureCropImageView(Context context, AttributeSet attrs) { 47 | this(context, attrs, 0); 48 | } 49 | 50 | public GestureCropImageView(Context context, AttributeSet attrs, int defStyle) { 51 | super(context, attrs, defStyle); 52 | } 53 | 54 | public void setScaleEnabled(boolean scaleEnabled) { 55 | mIsScaleEnabled = scaleEnabled; 56 | } 57 | 58 | public boolean isScaleEnabled() { 59 | return mIsScaleEnabled; 60 | } 61 | 62 | public void setRotateEnabled(boolean rotateEnabled) { 63 | mIsRotateEnabled = rotateEnabled; 64 | } 65 | 66 | public boolean isRotateEnabled() { 67 | return mIsRotateEnabled; 68 | } 69 | 70 | public void setDoubleTapScaleSteps(int doubleTapScaleSteps) { 71 | mDoubleTapScaleSteps = doubleTapScaleSteps; 72 | } 73 | 74 | public int getDoubleTapScaleSteps() { 75 | return mDoubleTapScaleSteps; 76 | } 77 | 78 | /** 79 | * If it's ACTION_DOWN event - user touches the screen and all current animation must be canceled. 80 | * If it's ACTION_UP event - user removed all fingers from the screen and current image position must be corrected. 81 | * If there are more than 2 fingers - update focal point coordinates. 82 | * Pass the event to the gesture detectors if those are enabled. 83 | */ 84 | @Override 85 | public boolean onTouchEvent(MotionEvent event) { 86 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) { 87 | cancelAllAnimations(); 88 | } 89 | 90 | if (event.getPointerCount() > 1) { 91 | mMidPntX = (event.getX(0) + event.getX(1)) / 2; 92 | mMidPntY = (event.getY(0) + event.getY(1)) / 2; 93 | } 94 | 95 | mGestureDetector.onTouchEvent(event); 96 | 97 | if (mIsScaleEnabled) { 98 | mScaleDetector.onTouchEvent(event); 99 | } 100 | 101 | if (mIsRotateEnabled) { 102 | mRotateDetector.onTouchEvent(event); 103 | } 104 | 105 | if ((event.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { 106 | setImageToWrapCropBounds(); 107 | } 108 | return true; 109 | } 110 | 111 | @Override 112 | protected void init() { 113 | super.init(); 114 | setupGestureListeners(); 115 | } 116 | 117 | /** 118 | * This method calculates target scale value for double tap gesture. 119 | * User is able to zoom the image from min scale value 120 | * to the max scale value with {@link #mDoubleTapScaleSteps} double taps. 121 | */ 122 | protected float getDoubleTapTargetScale() { 123 | return getCurrentScale() * (float) Math.pow(getMaxScale() / getMinScale(), 1.0f / mDoubleTapScaleSteps); 124 | } 125 | 126 | private void setupGestureListeners() { 127 | mGestureDetector = new GestureDetector(getContext(), new GestureListener(), null, true); 128 | mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); 129 | mRotateDetector = new RotationGestureDetector(new RotateListener()); 130 | } 131 | 132 | private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { 133 | 134 | @Override 135 | public boolean onScale(ScaleGestureDetector detector) { 136 | postScale(detector.getScaleFactor(), mMidPntX, mMidPntY); 137 | return true; 138 | } 139 | } 140 | 141 | private class GestureListener extends GestureDetector.SimpleOnGestureListener { 142 | 143 | @Override 144 | public boolean onDoubleTap(MotionEvent e) { 145 | zoomImageToPosition(getDoubleTapTargetScale(), e.getX(), e.getY(), DOUBLE_TAP_ZOOM_DURATION); 146 | return super.onDoubleTap(e); 147 | } 148 | 149 | @Override 150 | public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { 151 | postTranslate(-distanceX, -distanceY); 152 | return true; 153 | } 154 | 155 | } 156 | 157 | private class RotateListener extends RotationGestureDetector.SimpleOnRotationGestureListener { 158 | 159 | @Override 160 | public boolean onRotation(RotationGestureDetector rotationDetector) { 161 | postRotate(rotationDetector.getAngle(), mMidPntX, mMidPntY); 162 | return true; 163 | } 164 | 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /sample/src/main/java/com/yanzhenjie/durban/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.sample; 17 | 18 | import android.content.Intent; 19 | import android.graphics.drawable.Drawable; 20 | import android.os.Bundle; 21 | import android.support.v4.content.ContextCompat; 22 | import android.support.v4.content.res.ResourcesCompat; 23 | import android.support.v7.app.AppCompatActivity; 24 | import android.support.v7.widget.GridLayoutManager; 25 | import android.support.v7.widget.RecyclerView; 26 | import android.support.v7.widget.Toolbar; 27 | import android.util.Log; 28 | import android.view.Menu; 29 | import android.view.MenuItem; 30 | 31 | import com.yanzhenjie.album.Album; 32 | import com.yanzhenjie.album.util.DisplayUtils; 33 | import com.yanzhenjie.album.widget.recyclerview.AlbumVerticalGirdDecoration; 34 | import com.yanzhenjie.durban.Controller; 35 | import com.yanzhenjie.durban.Durban; 36 | 37 | import java.util.ArrayList; 38 | 39 | /** 40 | * Created by Yan Zhenjie on 2017/5/22. 41 | */ 42 | public class MainActivity extends AppCompatActivity { 43 | 44 | RecyclerView mRecyclerView; 45 | GridAdapter mGridAdapter; 46 | ArrayList mImageList; 47 | 48 | @Override 49 | protected void onCreate(Bundle savedInstanceState) { 50 | super.onCreate(savedInstanceState); 51 | DisplayUtils.initScreen(this); 52 | setContentView(R.layout.activity_main); 53 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 54 | setSupportActionBar(toolbar); 55 | 56 | mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); 57 | mRecyclerView.setLayoutManager(new GridLayoutManager(this, 3)); 58 | 59 | Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.drawable.decoration_white, null); 60 | mRecyclerView.addItemDecoration(new AlbumVerticalGirdDecoration(drawable)); 61 | 62 | assert drawable != null; 63 | int itemSize = (DisplayUtils.screenWidth - (drawable.getIntrinsicWidth() * 4)) / 3; 64 | mGridAdapter = new GridAdapter(this, itemSize); 65 | mRecyclerView.setAdapter(mGridAdapter); 66 | 67 | mImageList = new ArrayList<>(); 68 | } 69 | 70 | /** 71 | * Select images. 72 | */ 73 | private void selectImage() { 74 | Album.album(this) 75 | .statusBarColor(ContextCompat.getColor(this, R.color.colorPrimary)) 76 | .toolBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)) 77 | .navigationBarColor(ContextCompat.getColor(this, R.color.colorPrimaryBlack)) 78 | .selectCount(3) 79 | .requestCode(100) 80 | .start(); 81 | } 82 | 83 | /** 84 | * Crop images. 85 | */ 86 | private void cropImage(ArrayList imagePathList) { 87 | String cropDirectory = Utils.getAppRootPath(this).getAbsolutePath(); 88 | 89 | Log.i("CropSample", "Save directory: " + cropDirectory); 90 | 91 | Durban.with(MainActivity.this) 92 | .statusBarColor(ContextCompat.getColor(this, R.color.colorPrimaryDark)) 93 | .toolBarColor(ContextCompat.getColor(this, R.color.colorPrimary)) 94 | .navigationBarColor(ContextCompat.getColor(this, R.color.colorPrimaryBlack)) 95 | // Image path list/array. 96 | .inputImagePaths(imagePathList) 97 | // Image output directory. 98 | .outputDirectory(cropDirectory) 99 | // Image size limit. 100 | .maxWidthHeight(500, 500) 101 | // Aspect ratio. 102 | .aspectRatio(1, 1) 103 | // Output format: JPEG, PNG. 104 | .compressFormat(Durban.COMPRESS_JPEG) 105 | // Compress quality, see Bitmap#compress(Bitmap.CompressFormat, int, OutputStream) 106 | .compressQuality(90) 107 | // Gesture: ROTATE, SCALE, ALL, NONE. 108 | .gesture(Durban.GESTURE_ALL) 109 | .controller(Controller.newBuilder() 110 | .enable(false) 111 | .rotation(true) 112 | .rotationTitle(true) 113 | .scale(true) 114 | .scaleTitle(true) 115 | .build()) 116 | .requestCode(200) 117 | .start(); 118 | } 119 | 120 | @Override 121 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 122 | if (resultCode != RESULT_OK) return; 123 | switch (requestCode) { 124 | case 100: { 125 | // Analyze the selection results of the album. 126 | ArrayList imagePathList = Album.parseResult(data); 127 | cropImage(imagePathList); 128 | break; 129 | } 130 | case 200: { 131 | // Analyze the list of paths after cropping. 132 | mImageList = Durban.parseResult(data); 133 | mGridAdapter.notifyDataSetChanged(mImageList); 134 | break; 135 | } 136 | } 137 | } 138 | 139 | @Override 140 | public boolean onCreateOptionsMenu(Menu menu) { 141 | getMenuInflater().inflate(R.menu.menu_main, menu); 142 | return true; 143 | } 144 | 145 | @Override 146 | public boolean onOptionsItemSelected(MenuItem item) { 147 | int id = item.getItemId(); 148 | switch (id) { 149 | case R.id.action_album: { 150 | selectImage(); 151 | break; 152 | } 153 | } 154 | return true; 155 | } 156 | } -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/task/BitmapCropTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.task; 17 | 18 | import android.content.Context; 19 | import android.graphics.Bitmap; 20 | import android.graphics.Matrix; 21 | import android.graphics.RectF; 22 | import android.media.ExifInterface; 23 | import android.os.AsyncTask; 24 | import android.support.annotation.NonNull; 25 | import android.support.annotation.Nullable; 26 | 27 | import com.yanzhenjie.durban.callback.BitmapCropCallback; 28 | import com.yanzhenjie.durban.error.StorageError; 29 | import com.yanzhenjie.durban.model.CropParameters; 30 | import com.yanzhenjie.durban.model.ImageState; 31 | import com.yanzhenjie.durban.util.FileUtils; 32 | import com.yanzhenjie.durban.util.ImageHeaderParser; 33 | import com.yanzhenjie.loading.dialog.LoadingDialog; 34 | 35 | import java.io.File; 36 | import java.io.FileOutputStream; 37 | import java.io.OutputStream; 38 | 39 | /** 40 | *

Cut and save the image asynchronously.

41 | * Create by Yan Zhenjie on 2017/5/22. 42 | */ 43 | public class BitmapCropTask extends AsyncTask { 44 | 45 | static class PathWorkerResult { 46 | 47 | final String path; 48 | final Exception exception; 49 | 50 | PathWorkerResult(String path, Exception exception) { 51 | this.path = path; 52 | this.exception = exception; 53 | } 54 | } 55 | 56 | private LoadingDialog mDialog; 57 | 58 | private Bitmap mViewBitmap; 59 | 60 | private final RectF mCropRect; 61 | private final RectF mCurrentImageRect; 62 | 63 | private float mCurrentScale, mCurrentAngle; 64 | private final int mMaxResultImageSizeX, mMaxResultImageSizeY; 65 | 66 | private final Bitmap.CompressFormat mCompressFormat; 67 | private final int mCompressQuality; 68 | private final String mInputImagePath; 69 | private final String mOutputDirectory; 70 | private final BitmapCropCallback mCallback; 71 | 72 | private int mCroppedImageWidth, mCroppedImageHeight; 73 | 74 | public BitmapCropTask(@NonNull Context context, 75 | @Nullable Bitmap viewBitmap, 76 | @NonNull ImageState imageState, 77 | @NonNull CropParameters cropParameters, 78 | @Nullable BitmapCropCallback cropCallback) { 79 | mDialog = new LoadingDialog(context); 80 | 81 | mViewBitmap = viewBitmap; 82 | mCropRect = imageState.getCropRect(); 83 | mCurrentImageRect = imageState.getCurrentImageRect(); 84 | 85 | mCurrentScale = imageState.getCurrentScale(); 86 | mCurrentAngle = imageState.getCurrentAngle(); 87 | mMaxResultImageSizeX = cropParameters.getMaxResultImageSizeX(); 88 | mMaxResultImageSizeY = cropParameters.getMaxResultImageSizeY(); 89 | 90 | mCompressFormat = cropParameters.getCompressFormat(); 91 | mCompressQuality = cropParameters.getCompressQuality(); 92 | 93 | mInputImagePath = cropParameters.getImagePath(); 94 | mOutputDirectory = cropParameters.getImageOutputPath(); 95 | mCallback = cropCallback; 96 | } 97 | 98 | @Override 99 | protected void onPreExecute() { 100 | if (!mDialog.isShowing()) mDialog.show(); 101 | } 102 | 103 | @Override 104 | protected void onPostExecute(BitmapCropTask.PathWorkerResult result) { 105 | if (mDialog.isShowing()) mDialog.dismiss(); 106 | 107 | if (mCallback != null) { 108 | if (result.exception == null) { 109 | mCallback.onBitmapCropped( 110 | result.path, 111 | mCroppedImageWidth, 112 | mCroppedImageHeight); 113 | } else { 114 | mCallback.onCropFailure(result.exception); 115 | } 116 | } 117 | } 118 | 119 | @Override 120 | protected BitmapCropTask.PathWorkerResult doInBackground(Void... params) { 121 | try { 122 | String imagePath = crop(); 123 | return new PathWorkerResult(imagePath, null); 124 | } catch (Exception e) { 125 | return new PathWorkerResult(null, e); 126 | } 127 | } 128 | 129 | private String crop() throws Exception { 130 | FileUtils.validateDirectory(mOutputDirectory); 131 | 132 | String fileName = FileUtils.randomImageName(mCompressFormat); 133 | String outputImagePath = new File(mOutputDirectory, fileName).getAbsolutePath(); 134 | 135 | // Downsize if needed 136 | if (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) { 137 | float cropWidth = mCropRect.width() / mCurrentScale; 138 | float cropHeight = mCropRect.height() / mCurrentScale; 139 | 140 | if (cropWidth > mMaxResultImageSizeX || cropHeight > mMaxResultImageSizeY) { 141 | 142 | float scaleX = mMaxResultImageSizeX / cropWidth; 143 | float scaleY = mMaxResultImageSizeY / cropHeight; 144 | float resizeScale = Math.min(scaleX, scaleY); 145 | 146 | Bitmap resizedBitmap = Bitmap.createScaledBitmap( 147 | mViewBitmap, 148 | Math.round(mViewBitmap.getWidth() * resizeScale), 149 | Math.round(mViewBitmap.getHeight() * resizeScale), 150 | false); 151 | 152 | if (mViewBitmap != resizedBitmap) 153 | mViewBitmap.recycle(); 154 | mViewBitmap = resizedBitmap; 155 | mCurrentScale /= resizeScale; 156 | } 157 | } 158 | 159 | // Rotate if needed 160 | if (mCurrentAngle != 0) { 161 | Matrix tempMatrix = new Matrix(); 162 | tempMatrix.setRotate(mCurrentAngle, mViewBitmap.getWidth() / 2, mViewBitmap.getHeight() / 2); 163 | 164 | Bitmap rotatedBitmap = Bitmap.createBitmap( 165 | mViewBitmap, 166 | 0, 167 | 0, 168 | mViewBitmap.getWidth(), 169 | mViewBitmap.getHeight(), 170 | tempMatrix, true); 171 | 172 | if (mViewBitmap != rotatedBitmap) 173 | mViewBitmap.recycle(); 174 | mViewBitmap = rotatedBitmap; 175 | } 176 | 177 | int cropOffsetX = Math.round((mCropRect.left - mCurrentImageRect.left) / mCurrentScale); 178 | int cropOffsetY = Math.round((mCropRect.top - mCurrentImageRect.top) / mCurrentScale); 179 | mCroppedImageWidth = Math.round(mCropRect.width() / mCurrentScale); 180 | mCroppedImageHeight = Math.round(mCropRect.height() / mCurrentScale); 181 | 182 | boolean shouldCrop = shouldCrop(mCroppedImageWidth, mCroppedImageHeight); 183 | 184 | if (shouldCrop) { 185 | Bitmap croppedBitmap = Bitmap.createBitmap( 186 | mViewBitmap, 187 | cropOffsetX, 188 | cropOffsetY, 189 | mCroppedImageWidth, 190 | mCroppedImageHeight); 191 | 192 | OutputStream outputStream = null; 193 | try { 194 | outputStream = new FileOutputStream(outputImagePath); 195 | croppedBitmap.compress(mCompressFormat, mCompressQuality, outputStream); 196 | } catch (Exception e) { 197 | throw new StorageError(""); 198 | } finally { 199 | croppedBitmap.recycle(); 200 | FileUtils.close(outputStream); 201 | } 202 | 203 | if (mCompressFormat.equals(Bitmap.CompressFormat.JPEG)) { 204 | ExifInterface originalExif = new ExifInterface(mInputImagePath); 205 | ImageHeaderParser.copyExif(originalExif, mCroppedImageWidth, mCroppedImageHeight, outputImagePath); 206 | } 207 | } else { 208 | FileUtils.copyFile(mInputImagePath, outputImagePath); 209 | } 210 | if (mViewBitmap != null && !mViewBitmap.isRecycled()) mViewBitmap.recycle(); 211 | 212 | return outputImagePath; 213 | } 214 | 215 | /** 216 | * Check whether an image should be cropped at all or just file can be copied to the destination path. 217 | * For each 1000 pixels there is one pixel of error due to matrix calculations etc. 218 | * 219 | * @param width - crop area width 220 | * @param height - crop area height 221 | * @return - true if image must be cropped, false - if original image fits requirements 222 | */ 223 | private boolean shouldCrop(int width, int height) { 224 | int pixelError = 1; 225 | pixelError += Math.round(Math.max(width, height) / 1000f); 226 | return (mMaxResultImageSizeX > 0 && mMaxResultImageSizeY > 0) 227 | || Math.abs(mCropRect.left - mCurrentImageRect.left) > pixelError 228 | || Math.abs(mCropRect.top - mCurrentImageRect.top) > pixelError 229 | || Math.abs(mCropRect.bottom - mCurrentImageRect.bottom) > pixelError 230 | || Math.abs(mCropRect.right - mCurrentImageRect.right) > pixelError; 231 | } 232 | 233 | } -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/Durban.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.graphics.Bitmap; 22 | import android.support.annotation.ColorInt; 23 | import android.support.annotation.IntDef; 24 | import android.support.annotation.IntRange; 25 | import android.support.annotation.NonNull; 26 | import android.support.v4.app.Fragment; 27 | 28 | import java.io.OutputStream; 29 | import java.lang.annotation.Retention; 30 | import java.lang.annotation.RetentionPolicy; 31 | import java.lang.reflect.Method; 32 | import java.util.ArrayList; 33 | import java.util.Collections; 34 | import java.util.Locale; 35 | 36 | /** 37 | *

Entrance.

38 | * Create by Yan Zhenjie on 2017/5/23. 39 | */ 40 | public class Durban { 41 | 42 | private static final String KEY_PREFIX = "AlbumCrop"; 43 | 44 | static final String KEY_INPUT_STATUS_COLOR = KEY_PREFIX + ".KEY_INPUT_STATUS_COLOR"; 45 | static final String KEY_INPUT_TOOLBAR_COLOR = KEY_PREFIX + ".KEY_INPUT_TOOLBAR_COLOR"; 46 | static final String KEY_INPUT_NAVIGATION_COLOR = KEY_PREFIX + ".KEY_INPUT_NAVIGATION_COLOR"; 47 | static final String KEY_INPUT_TITLE = KEY_PREFIX + ".KEY_INPUT_TITLE"; 48 | 49 | static final String KEY_INPUT_GESTURE = KEY_PREFIX + ".KEY_INPUT_GESTURE"; 50 | static final String KEY_INPUT_ASPECT_RATIO = KEY_PREFIX + ".KEY_INPUT_ASPECT_RATIO"; 51 | static final String KEY_INPUT_MAX_WIDTH_HEIGHT = KEY_PREFIX + ".KEY_INPUT_MAX_WIDTH_HEIGHT"; 52 | 53 | static final String KEY_INPUT_COMPRESS_FORMAT = KEY_PREFIX + ".KEY_INPUT_COMPRESS_FORMAT"; 54 | static final String KEY_INPUT_COMPRESS_QUALITY = KEY_PREFIX + ".KEY_INPUT_COMPRESS_QUALITY"; 55 | 56 | static final String KEY_INPUT_DIRECTORY = KEY_PREFIX + ".KEY_INPUT_DIRECTORY"; 57 | static final String KEY_INPUT_PATH_ARRAY = KEY_PREFIX + ".KEY_INPUT_PATH_ARRAY"; 58 | 59 | static final String KEY_INPUT_CONTROLLER = KEY_PREFIX + ".KEY_INPUT_CONTROLLER"; 60 | 61 | static final String KEY_OUTPUT_IMAGE_LIST = KEY_PREFIX + ".KEY_OUTPUT_IMAGE_LIST"; 62 | 63 | /** 64 | * Do not allow any gestures. 65 | */ 66 | public static final int GESTURE_NONE = 0; 67 | /** 68 | * Allow scaling. 69 | */ 70 | public static final int GESTURE_SCALE = 1; 71 | /** 72 | * Allow rotation. 73 | */ 74 | public static final int GESTURE_ROTATE = 2; 75 | /** 76 | * Allow rotation and scaling. 77 | */ 78 | public static final int GESTURE_ALL = 3; 79 | 80 | @IntDef({GESTURE_NONE, GESTURE_SCALE, GESTURE_ROTATE, GESTURE_ALL}) 81 | @Retention(RetentionPolicy.SOURCE) 82 | public @interface GestureTypes { 83 | } 84 | 85 | /** 86 | * JPEG format. 87 | */ 88 | public static final int COMPRESS_JPEG = 0; 89 | /** 90 | * PNG format. 91 | */ 92 | public static final int COMPRESS_PNG = 1; 93 | 94 | @IntDef({COMPRESS_JPEG, COMPRESS_PNG}) 95 | @Retention(RetentionPolicy.SOURCE) 96 | public @interface FormatTypes { 97 | } 98 | 99 | private static DurbanConfig sDurbanConfig; 100 | 101 | /** 102 | * Initialize Album. 103 | * 104 | * @param durbanConfig {@link DurbanConfig}. 105 | */ 106 | public static void initialize(DurbanConfig durbanConfig) { 107 | sDurbanConfig = durbanConfig; 108 | } 109 | 110 | /** 111 | * Get the durban configuration. 112 | * 113 | * @return {@link DurbanConfig}. 114 | */ 115 | public static DurbanConfig getDurbanConfig() { 116 | if (sDurbanConfig == null) { 117 | initialize(DurbanConfig.newBuilder(null) 118 | .setLocale(Locale.getDefault()) 119 | .build() 120 | ); 121 | } 122 | return sDurbanConfig; 123 | } 124 | 125 | 126 | private Object o; 127 | private Intent mCropIntent; 128 | 129 | public static Durban with(Activity activity) { 130 | return new Durban(activity); 131 | } 132 | 133 | public static Durban with(Fragment fragment) { 134 | return new Durban(fragment); 135 | } 136 | 137 | public static Durban with(android.app.Fragment fragment) { 138 | return new Durban(fragment); 139 | } 140 | 141 | private Durban(Object o) { 142 | this.o = o; 143 | mCropIntent = new Intent(getContext(o), DurbanActivity.class); 144 | } 145 | 146 | /** 147 | * The color of the StatusBar. 148 | */ 149 | public Durban statusBarColor(@ColorInt int color) { 150 | mCropIntent.putExtra(KEY_INPUT_STATUS_COLOR, color); 151 | return this; 152 | } 153 | 154 | /** 155 | * The color of the Toolbar. 156 | */ 157 | public Durban toolBarColor(@ColorInt int color) { 158 | mCropIntent.putExtra(KEY_INPUT_TOOLBAR_COLOR, color); 159 | return this; 160 | } 161 | 162 | /** 163 | * Set the color of the NavigationBar. 164 | */ 165 | public Durban navigationBarColor(@ColorInt int color) { 166 | mCropIntent.putExtra(KEY_INPUT_NAVIGATION_COLOR, color); 167 | return this; 168 | } 169 | 170 | /** 171 | * The title of the interface. 172 | */ 173 | public Durban title(String title) { 174 | mCropIntent.putExtra(KEY_INPUT_TITLE, title); 175 | return this; 176 | } 177 | 178 | /** 179 | * The gestures that allow operation. 180 | * 181 | * @param gesture gesture sign. 182 | * @see #GESTURE_NONE 183 | * @see #GESTURE_ALL 184 | * @see #GESTURE_ROTATE 185 | * @see #GESTURE_SCALE 186 | */ 187 | public Durban gesture(@GestureTypes int gesture) { 188 | mCropIntent.putExtra(KEY_INPUT_GESTURE, gesture); 189 | return this; 190 | } 191 | 192 | /** 193 | * The aspect ratio column of the crop box. 194 | * 195 | * @param x aspect ratio X. 196 | * @param y aspect ratio Y. 197 | */ 198 | public Durban aspectRatio(float x, float y) { 199 | mCropIntent.putExtra(KEY_INPUT_ASPECT_RATIO, new float[]{x, y}); 200 | return this; 201 | } 202 | 203 | /** 204 | * Use the aspect ratio column of the original image. 205 | */ 206 | public Durban aspectRatioWithSourceImage() { 207 | return aspectRatio(0F, 0F); 208 | } 209 | 210 | /** 211 | * Set maximum size for result cropped image. 212 | * 213 | * @param width max cropped image width. 214 | * @param height max cropped image height. 215 | */ 216 | public Durban maxWidthHeight(@IntRange(from = 100) int width, @IntRange(from = 100) int height) { 217 | mCropIntent.putExtra(KEY_INPUT_MAX_WIDTH_HEIGHT, new int[]{width, height}); 218 | return this; 219 | } 220 | 221 | /** 222 | * The compression format of the cropped image. 223 | * 224 | * @param format image format. 225 | * @see #COMPRESS_JPEG 226 | * @see #COMPRESS_PNG 227 | */ 228 | public Durban compressFormat(@FormatTypes int format) { 229 | mCropIntent.putExtra(KEY_INPUT_COMPRESS_FORMAT, format); 230 | return this; 231 | } 232 | 233 | /** 234 | * The compression quality of the cropped image. 235 | * 236 | * @param quality see {@link Bitmap#compress(Bitmap.CompressFormat, int, OutputStream)}. 237 | * @see Bitmap#compress(Bitmap.CompressFormat, int, OutputStream) 238 | */ 239 | public Durban compressQuality(int quality) { 240 | mCropIntent.putExtra(KEY_INPUT_COMPRESS_QUALITY, quality); 241 | return this; 242 | } 243 | 244 | /** 245 | * Set the output directory of the cropped picture. 246 | */ 247 | public Durban outputDirectory(String folder) { 248 | mCropIntent.putExtra(KEY_INPUT_DIRECTORY, folder); 249 | return this; 250 | } 251 | 252 | /** 253 | * The pictures to be cropped. 254 | */ 255 | public Durban inputImagePaths(String... imagePathArray) { 256 | ArrayList arrayList = new ArrayList<>(); 257 | Collections.addAll(arrayList, imagePathArray); 258 | mCropIntent.putStringArrayListExtra(KEY_INPUT_PATH_ARRAY, arrayList); 259 | return this; 260 | } 261 | 262 | /** 263 | * The pictures to be cropped. 264 | */ 265 | public Durban inputImagePaths(ArrayList imagePathList) { 266 | mCropIntent.putStringArrayListExtra(KEY_INPUT_PATH_ARRAY, imagePathList); 267 | return this; 268 | } 269 | 270 | /** 271 | * Control panel configuration. 272 | */ 273 | public Durban controller(Controller controller) { 274 | mCropIntent.putExtra(KEY_INPUT_CONTROLLER, controller); 275 | return this; 276 | } 277 | 278 | /** 279 | * Request code, callback to {@code onActivityResult()}. 280 | */ 281 | public Durban requestCode(int requestCode) { 282 | mCropIntent.putExtra("requestCode", requestCode); 283 | return this; 284 | } 285 | 286 | /** 287 | * Start cropping. 288 | */ 289 | public void start() { 290 | try { 291 | Method method = o.getClass().getMethod("startActivityForResult", Intent.class, int.class); 292 | if (!method.isAccessible()) method.setAccessible(true); 293 | method.invoke(o, mCropIntent, mCropIntent.getIntExtra("requestCode", 1)); 294 | } catch (Exception e) { 295 | e.printStackTrace(); 296 | } 297 | } 298 | 299 | /** 300 | * Analyze the crop results. 301 | */ 302 | public static ArrayList parseResult(@NonNull Intent intent) { 303 | return intent.getStringArrayListExtra(KEY_OUTPUT_IMAGE_LIST); 304 | } 305 | 306 | protected static 307 | @NonNull 308 | Context getContext(Object o) { 309 | if (o instanceof Activity) return (Context) o; 310 | else if (o instanceof Fragment) return ((Fragment) o).getContext(); 311 | else if (o instanceof android.app.Fragment) ((android.app.Fragment) o).getActivity(); 312 | throw new IllegalArgumentException(o.getClass() + " is not supported."); 313 | } 314 | 315 | } -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/view/TransformImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.view; 17 | 18 | import android.content.Context; 19 | import android.graphics.Bitmap; 20 | import android.graphics.Matrix; 21 | import android.graphics.RectF; 22 | import android.graphics.drawable.Drawable; 23 | import android.net.Uri; 24 | import android.support.annotation.IntRange; 25 | import android.support.annotation.NonNull; 26 | import android.support.annotation.Nullable; 27 | import android.util.AttributeSet; 28 | import android.util.Log; 29 | 30 | import com.yanzhenjie.durban.callback.BitmapLoadCallback; 31 | import com.yanzhenjie.durban.model.ExifInfo; 32 | import com.yanzhenjie.durban.task.BitmapLoadTask; 33 | import com.yanzhenjie.durban.util.BitmapLoadUtils; 34 | import com.yanzhenjie.durban.util.FastBitmapDrawable; 35 | import com.yanzhenjie.durban.util.RectUtils; 36 | 37 | /** 38 | *

39 | * This class provides base logic to setup the image, transform it with matrix (move, scale, rotate), 40 | * and methods to get current matrix state. 41 | *

42 | * Update by Yan Zhenjie on 2017/5/23. 43 | */ 44 | public class TransformImageView extends android.support.v7.widget.AppCompatImageView { 45 | 46 | private static final String TAG = "TransformImageView"; 47 | 48 | private static final int RECT_CORNER_POINTS_COORDS = 8; 49 | private static final int RECT_CENTER_POINT_COORDS = 2; 50 | private static final int MATRIX_VALUES_COUNT = 9; 51 | 52 | protected final float[] mCurrentImageCorners = new float[RECT_CORNER_POINTS_COORDS]; 53 | protected final float[] mCurrentImageCenter = new float[RECT_CENTER_POINT_COORDS]; 54 | 55 | private final float[] mMatrixValues = new float[MATRIX_VALUES_COUNT]; 56 | 57 | protected Matrix mCurrentImageMatrix = new Matrix(); 58 | protected int mThisWidth, mThisHeight; 59 | 60 | protected TransformImageListener mTransformImageListener; 61 | 62 | private float[] mInitialImageCorners; 63 | private float[] mInitialImageCenter; 64 | 65 | protected boolean mBitmapDecoded = false; 66 | protected boolean mBitmapLaidOut = false; 67 | 68 | private int mMaxBitmapSize = 0; 69 | 70 | private String mImagePath, mOutputDirectory; 71 | private ExifInfo mExifInfo; 72 | 73 | /** 74 | * Interface for rotation and scale change notifying. 75 | */ 76 | public interface TransformImageListener { 77 | 78 | void onLoadComplete(); 79 | 80 | void onLoadFailure(); 81 | 82 | void onRotate(float currentAngle); 83 | 84 | void onScale(float currentScale); 85 | } 86 | 87 | public TransformImageView(Context context) { 88 | this(context, null); 89 | } 90 | 91 | public TransformImageView(Context context, AttributeSet attrs) { 92 | this(context, attrs, 0); 93 | } 94 | 95 | public TransformImageView(Context context, AttributeSet attrs, int defStyle) { 96 | super(context, attrs, defStyle); 97 | init(); 98 | } 99 | 100 | public void setTransformImageListener(TransformImageListener transformImageListener) { 101 | mTransformImageListener = transformImageListener; 102 | } 103 | 104 | @Override 105 | public void setScaleType(ScaleType scaleType) { 106 | if (scaleType == ScaleType.MATRIX) { 107 | super.setScaleType(scaleType); 108 | } else { 109 | Log.w(TAG, "Invalid ScaleType. Only ScaleType.MATRIX can be used"); 110 | } 111 | } 112 | 113 | /** 114 | * Setter for {@link #mMaxBitmapSize} value. 115 | * Be sure to call it before {@link #setImageURI(Uri)} or other image setters. 116 | * 117 | * @param maxBitmapSize - max size for both width and height of exception that will be used in the view. 118 | */ 119 | public void setMaxBitmapSize(int maxBitmapSize) { 120 | mMaxBitmapSize = maxBitmapSize; 121 | } 122 | 123 | public int getMaxBitmapSize() { 124 | if (mMaxBitmapSize <= 0) { 125 | mMaxBitmapSize = BitmapLoadUtils.calculateMaxBitmapSize(getContext()); 126 | } 127 | return mMaxBitmapSize; 128 | } 129 | 130 | @Override 131 | public void setImageBitmap(final Bitmap bitmap) { 132 | setImageDrawable(new FastBitmapDrawable(bitmap)); 133 | } 134 | 135 | public String getImagePath() { 136 | return mImagePath; 137 | } 138 | 139 | public String getOutputDirectory() { 140 | return mOutputDirectory; 141 | } 142 | 143 | public ExifInfo getExifInfo() { 144 | return mExifInfo; 145 | } 146 | 147 | public void setOutputDirectory(String outputDirectory) { 148 | this.mOutputDirectory = outputDirectory; 149 | } 150 | 151 | /** 152 | * This method takes an Uri as a parameter, then calls method to decode it into Bitmap with specified size. 153 | * 154 | * @param inputImagePath - image Uri 155 | * @throws Exception - can throw exception if having problems with decoding Uri or OOM. 156 | */ 157 | public void setImagePath(@NonNull String inputImagePath) throws Exception { 158 | this.mImagePath = inputImagePath; 159 | int maxBitmapSize = getMaxBitmapSize(); 160 | 161 | new BitmapLoadTask( 162 | getContext(), 163 | maxBitmapSize, 164 | maxBitmapSize, 165 | new BitmapLoadCallback() { 166 | @Override 167 | public void onSuccessfully(@NonNull Bitmap bitmap, @NonNull ExifInfo exifInfo) { 168 | mExifInfo = exifInfo; 169 | mBitmapDecoded = true; 170 | setImageBitmap(bitmap); 171 | } 172 | 173 | @Override 174 | public void onFailure() { 175 | if (mTransformImageListener != null) { 176 | mTransformImageListener.onLoadFailure(); 177 | } 178 | } 179 | } 180 | ).execute(inputImagePath); 181 | } 182 | 183 | /** 184 | * @return - current image scale value. 185 | * [1.0f - for original image, 2.0f - for 200% scaled image, etc.] 186 | */ 187 | public float getCurrentScale() { 188 | return getMatrixScale(mCurrentImageMatrix); 189 | } 190 | 191 | /** 192 | * This method calculates scale value for given Matrix object. 193 | */ 194 | public float getMatrixScale(@NonNull Matrix matrix) { 195 | return (float) Math.sqrt(Math.pow(getMatrixValue(matrix, Matrix.MSCALE_X), 2) 196 | + Math.pow(getMatrixValue(matrix, Matrix.MSKEW_Y), 2)); 197 | } 198 | 199 | /** 200 | * @return - current image rotation angle. 201 | */ 202 | public float getCurrentAngle() { 203 | return getMatrixAngle(mCurrentImageMatrix); 204 | } 205 | 206 | /** 207 | * This method calculates rotation angle for given Matrix object. 208 | */ 209 | public float getMatrixAngle(@NonNull Matrix matrix) { 210 | return (float) -(Math.atan2(getMatrixValue(matrix, Matrix.MSKEW_X), 211 | getMatrixValue(matrix, Matrix.MSCALE_X)) * (180 / Math.PI)); 212 | } 213 | 214 | @Override 215 | public void setImageMatrix(Matrix matrix) { 216 | super.setImageMatrix(matrix); 217 | mCurrentImageMatrix.set(matrix); 218 | updateCurrentImagePoints(); 219 | } 220 | 221 | @Nullable 222 | public Bitmap getViewBitmap() { 223 | if (getDrawable() == null || !(getDrawable() instanceof FastBitmapDrawable)) return null; 224 | else return ((FastBitmapDrawable) getDrawable()).getBitmap(); 225 | } 226 | 227 | /** 228 | * This method translates current image. 229 | * 230 | * @param deltaX - horizontal shift 231 | * @param deltaY - vertical shift 232 | */ 233 | public void postTranslate(float deltaX, float deltaY) { 234 | if (deltaX != 0 || deltaY != 0) { 235 | mCurrentImageMatrix.postTranslate(deltaX, deltaY); 236 | setImageMatrix(mCurrentImageMatrix); 237 | } 238 | } 239 | 240 | /** 241 | * This method scales current image. 242 | * 243 | * @param deltaScale - scale value 244 | * @param px - scale center X 245 | * @param py - scale center Y 246 | */ 247 | public void postScale(float deltaScale, float px, float py) { 248 | if (deltaScale != 0) { 249 | mCurrentImageMatrix.postScale(deltaScale, deltaScale, px, py); 250 | setImageMatrix(mCurrentImageMatrix); 251 | if (mTransformImageListener != null) { 252 | mTransformImageListener.onScale(getMatrixScale(mCurrentImageMatrix)); 253 | } 254 | } 255 | } 256 | 257 | /** 258 | * This method rotates current image. 259 | * 260 | * @param deltaAngle - rotation angle 261 | * @param px - rotation center X 262 | * @param py - rotation center Y 263 | */ 264 | public void postRotate(float deltaAngle, float px, float py) { 265 | if (deltaAngle != 0) { 266 | mCurrentImageMatrix.postRotate(deltaAngle, px, py); 267 | setImageMatrix(mCurrentImageMatrix); 268 | if (mTransformImageListener != null) { 269 | mTransformImageListener.onRotate(getMatrixAngle(mCurrentImageMatrix)); 270 | } 271 | } 272 | } 273 | 274 | protected void init() { 275 | setScaleType(ScaleType.MATRIX); 276 | } 277 | 278 | @Override 279 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 280 | super.onLayout(changed, left, top, right, bottom); 281 | if (changed || (mBitmapDecoded && !mBitmapLaidOut)) { 282 | 283 | left = getPaddingLeft(); 284 | top = getPaddingTop(); 285 | right = getWidth() - getPaddingRight(); 286 | bottom = getHeight() - getPaddingBottom(); 287 | mThisWidth = right - left; 288 | mThisHeight = bottom - top; 289 | 290 | onImageLaidOut(); 291 | } 292 | } 293 | 294 | /** 295 | * When image is laid out {@link #mInitialImageCenter} and {@link #mInitialImageCenter} 296 | * must be set. 297 | */ 298 | protected void onImageLaidOut() { 299 | final Drawable drawable = getDrawable(); 300 | if (drawable == null) { 301 | return; 302 | } 303 | 304 | float w = drawable.getIntrinsicWidth(); 305 | float h = drawable.getIntrinsicHeight(); 306 | 307 | Log.d(TAG, String.format("Image size: [%d:%d]", (int) w, (int) h)); 308 | 309 | RectF initialImageRect = new RectF(0, 0, w, h); 310 | mInitialImageCorners = RectUtils.getCornersFromRect(initialImageRect); 311 | mInitialImageCenter = RectUtils.getCenterFromRect(initialImageRect); 312 | 313 | mBitmapLaidOut = true; 314 | 315 | if (mTransformImageListener != null) { 316 | mTransformImageListener.onLoadComplete(); 317 | } 318 | } 319 | 320 | /** 321 | * This method returns Matrix value for given index. 322 | * 323 | * @param matrix - valid Matrix object 324 | * @param valueIndex - index of needed value. See {@link Matrix#MSCALE_X} and others. 325 | * @return - matrix value for index 326 | */ 327 | protected float getMatrixValue(@NonNull Matrix matrix, @IntRange(from = 0, to = MATRIX_VALUES_COUNT) int valueIndex) { 328 | matrix.getValues(mMatrixValues); 329 | return mMatrixValues[valueIndex]; 330 | } 331 | 332 | /** 333 | * This method logs given matrix X, Y, scale, and angle values. 334 | * Can be used for debug. 335 | */ 336 | @SuppressWarnings("unused") 337 | protected void printMatrix(@NonNull String logPrefix, @NonNull Matrix matrix) { 338 | float x = getMatrixValue(matrix, Matrix.MTRANS_X); 339 | float y = getMatrixValue(matrix, Matrix.MTRANS_Y); 340 | float rScale = getMatrixScale(matrix); 341 | float rAngle = getMatrixAngle(matrix); 342 | Log.d(TAG, logPrefix + ": matrix: { x: " + x + ", y: " + y + ", scale: " + rScale + ", angle: " + rAngle + " }"); 343 | } 344 | 345 | /** 346 | * This method updates current image corners and center points that are stored in 347 | * {@link #mCurrentImageCorners} and {@link #mCurrentImageCenter} arrays. 348 | * Those are used for several calculations. 349 | */ 350 | private void updateCurrentImagePoints() { 351 | mCurrentImageMatrix.mapPoints(mCurrentImageCorners, mInitialImageCorners); 352 | mCurrentImageMatrix.mapPoints(mCurrentImageCenter, mInitialImageCenter); 353 | } 354 | 355 | } 356 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/util/ImageHeaderParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban.util; 17 | 18 | import android.media.ExifInterface; 19 | import android.text.TextUtils; 20 | import android.util.Log; 21 | 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | import java.nio.ByteBuffer; 25 | import java.nio.ByteOrder; 26 | import java.nio.charset.Charset; 27 | 28 | /** 29 | * Update by Yan Zhenjie on 2017/5/23. 30 | */ 31 | public class ImageHeaderParser { 32 | private static final String TAG = "ImageHeaderParser"; 33 | /** 34 | * A constant indicating we were unable to parse the orientation from the image either because 35 | * no exif segment containing orientation data existed, or because of an I/O error attempting to 36 | * read the exif segment. 37 | */ 38 | public static final int UNKNOWN_ORIENTATION = -1; 39 | 40 | private static final int EXIF_MAGIC_NUMBER = 0xFFD8; 41 | // "MM". 42 | private static final int MOTOROLA_TIFF_MAGIC_NUMBER = 0x4D4D; 43 | // "II". 44 | private static final int INTEL_TIFF_MAGIC_NUMBER = 0x4949; 45 | private static final String JPEG_EXIF_SEGMENT_PREAMBLE = "Exif\0\0"; 46 | private static final byte[] JPEG_EXIF_SEGMENT_PREAMBLE_BYTES = 47 | JPEG_EXIF_SEGMENT_PREAMBLE.getBytes(Charset.forName("UTF-8")); 48 | private static final int SEGMENT_SOS = 0xDA; 49 | private static final int MARKER_EOI = 0xD9; 50 | private static final int SEGMENT_START_ID = 0xFF; 51 | private static final int EXIF_SEGMENT_TYPE = 0xE1; 52 | private static final int ORIENTATION_TAG_TYPE = 0x0112; 53 | private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8}; 54 | 55 | private final Reader reader; 56 | 57 | public ImageHeaderParser(InputStream is) { 58 | reader = new StreamReader(is); 59 | } 60 | 61 | /** 62 | * Parse the orientation from the image header. If it doesn't handle this image type (or this is 63 | * not an image) it will return a default value rather than throwing an exception. 64 | * 65 | * @return The exif orientation if present or -1 if the header couldn't be parsed or doesn't 66 | * contain an orientation 67 | * @throws IOException 68 | */ 69 | public int getOrientation() throws IOException { 70 | final int magicNumber = reader.getUInt16(); 71 | 72 | if (!handles(magicNumber)) { 73 | if (Log.isLoggable(TAG, Log.DEBUG)) { 74 | Log.d(TAG, "Parser doesn't handle magic number: " + magicNumber); 75 | } 76 | return UNKNOWN_ORIENTATION; 77 | } else { 78 | int exifSegmentLength = moveToExifSegmentAndGetLength(); 79 | if (exifSegmentLength == -1) { 80 | if (Log.isLoggable(TAG, Log.DEBUG)) { 81 | Log.d(TAG, "Failed to parse exif segment length, or exif segment not found"); 82 | } 83 | return UNKNOWN_ORIENTATION; 84 | } 85 | 86 | byte[] exifData = new byte[exifSegmentLength]; 87 | return parseExifSegment(exifData, exifSegmentLength); 88 | } 89 | } 90 | 91 | private int parseExifSegment(byte[] tempArray, int exifSegmentLength) throws IOException { 92 | int read = reader.read(tempArray, exifSegmentLength); 93 | if (read != exifSegmentLength) { 94 | if (Log.isLoggable(TAG, Log.DEBUG)) { 95 | Log.d(TAG, "Unable to read exif segment data" 96 | + ", length: " + exifSegmentLength 97 | + ", actually read: " + read); 98 | } 99 | return UNKNOWN_ORIENTATION; 100 | } 101 | 102 | boolean hasJpegExifPreamble = hasJpegExifPreamble(tempArray, exifSegmentLength); 103 | if (hasJpegExifPreamble) { 104 | return parseExifSegment(new RandomAccessReader(tempArray, exifSegmentLength)); 105 | } else { 106 | if (Log.isLoggable(TAG, Log.DEBUG)) { 107 | Log.d(TAG, "Missing jpeg exif preamble"); 108 | } 109 | return UNKNOWN_ORIENTATION; 110 | } 111 | } 112 | 113 | private boolean hasJpegExifPreamble(byte[] exifData, int exifSegmentLength) { 114 | boolean result = 115 | exifData != null && exifSegmentLength > JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; 116 | if (result) { 117 | for (int i = 0; i < JPEG_EXIF_SEGMENT_PREAMBLE_BYTES.length; i++) { 118 | if (exifData[i] != JPEG_EXIF_SEGMENT_PREAMBLE_BYTES[i]) { 119 | result = false; 120 | break; 121 | } 122 | } 123 | } 124 | return result; 125 | } 126 | 127 | /** 128 | * Moves reader to the start of the exif segment and returns the length of the exif segment or 129 | * {@code -1} if no exif segment is found. 130 | */ 131 | private int moveToExifSegmentAndGetLength() throws IOException { 132 | short segmentId, segmentType; 133 | int segmentLength; 134 | while (true) { 135 | segmentId = reader.getUInt8(); 136 | if (segmentId != SEGMENT_START_ID) { 137 | if (Log.isLoggable(TAG, Log.DEBUG)) { 138 | Log.d(TAG, "Unknown segmentId=" + segmentId); 139 | } 140 | return -1; 141 | } 142 | 143 | segmentType = reader.getUInt8(); 144 | 145 | if (segmentType == SEGMENT_SOS) { 146 | return -1; 147 | } else if (segmentType == MARKER_EOI) { 148 | if (Log.isLoggable(TAG, Log.DEBUG)) { 149 | Log.d(TAG, "Found MARKER_EOI in exif segment"); 150 | } 151 | return -1; 152 | } 153 | 154 | // Segment length includes bytes for segment length. 155 | segmentLength = reader.getUInt16() - 2; 156 | 157 | if (segmentType != EXIF_SEGMENT_TYPE) { 158 | long skipped = reader.skip(segmentLength); 159 | if (skipped != segmentLength) { 160 | if (Log.isLoggable(TAG, Log.DEBUG)) { 161 | Log.d(TAG, "Unable to skip enough data" 162 | + ", type: " + segmentType 163 | + ", wanted to skip: " + segmentLength 164 | + ", but actually skipped: " + skipped); 165 | } 166 | return -1; 167 | } 168 | } else { 169 | return segmentLength; 170 | } 171 | } 172 | } 173 | 174 | private static int parseExifSegment(RandomAccessReader segmentData) { 175 | final int headerOffsetSize = JPEG_EXIF_SEGMENT_PREAMBLE.length(); 176 | 177 | short byteOrderIdentifier = segmentData.getInt16(headerOffsetSize); 178 | final ByteOrder byteOrder; 179 | if (byteOrderIdentifier == MOTOROLA_TIFF_MAGIC_NUMBER) { 180 | byteOrder = ByteOrder.BIG_ENDIAN; 181 | } else if (byteOrderIdentifier == INTEL_TIFF_MAGIC_NUMBER) { 182 | byteOrder = ByteOrder.LITTLE_ENDIAN; 183 | } else { 184 | if (Log.isLoggable(TAG, Log.DEBUG)) { 185 | Log.d(TAG, "Unknown endianness = " + byteOrderIdentifier); 186 | } 187 | byteOrder = ByteOrder.BIG_ENDIAN; 188 | } 189 | 190 | segmentData.order(byteOrder); 191 | 192 | int firstIfdOffset = segmentData.getInt32(headerOffsetSize + 4) + headerOffsetSize; 193 | int tagCount = segmentData.getInt16(firstIfdOffset); 194 | 195 | int tagOffset, tagType, formatCode, componentCount; 196 | for (int i = 0; i < tagCount; i++) { 197 | tagOffset = calcTagOffset(firstIfdOffset, i); 198 | tagType = segmentData.getInt16(tagOffset); 199 | 200 | // We only want orientation. 201 | if (tagType != ORIENTATION_TAG_TYPE) { 202 | continue; 203 | } 204 | 205 | formatCode = segmentData.getInt16(tagOffset + 2); 206 | 207 | // 12 is max format code. 208 | if (formatCode < 1 || formatCode > 12) { 209 | if (Log.isLoggable(TAG, Log.DEBUG)) { 210 | Log.d(TAG, "Got invalid format code = " + formatCode); 211 | } 212 | continue; 213 | } 214 | 215 | componentCount = segmentData.getInt32(tagOffset + 4); 216 | 217 | if (componentCount < 0) { 218 | if (Log.isLoggable(TAG, Log.DEBUG)) { 219 | Log.d(TAG, "Negative tiff component count"); 220 | } 221 | continue; 222 | } 223 | 224 | if (Log.isLoggable(TAG, Log.DEBUG)) { 225 | Log.d(TAG, "Got tagIndex=" + i + " tagType=" + tagType + " formatCode=" + formatCode 226 | + " componentCount=" + componentCount); 227 | } 228 | 229 | final int byteCount = componentCount + BYTES_PER_FORMAT[formatCode]; 230 | 231 | if (byteCount > 4) { 232 | if (Log.isLoggable(TAG, Log.DEBUG)) { 233 | Log.d(TAG, "Got byte count > 4, not orientation, continuing, formatCode=" + formatCode); 234 | } 235 | continue; 236 | } 237 | 238 | final int tagValueOffset = tagOffset + 8; 239 | 240 | if (tagValueOffset < 0 || tagValueOffset > segmentData.length()) { 241 | if (Log.isLoggable(TAG, Log.DEBUG)) { 242 | Log.d(TAG, "Illegal tagValueOffset=" + tagValueOffset + " tagType=" + tagType); 243 | } 244 | continue; 245 | } 246 | 247 | if (byteCount < 0 || tagValueOffset + byteCount > segmentData.length()) { 248 | if (Log.isLoggable(TAG, Log.DEBUG)) { 249 | Log.d(TAG, "Illegal number of bytes for TI tag data tagType=" + tagType); 250 | } 251 | continue; 252 | } 253 | 254 | //assume componentCount == 1 && fmtCode == 3 255 | return segmentData.getInt16(tagValueOffset); 256 | } 257 | 258 | return -1; 259 | } 260 | 261 | private static int calcTagOffset(int ifdOffset, int tagIndex) { 262 | return ifdOffset + 2 + 12 * tagIndex; 263 | } 264 | 265 | private static boolean handles(int imageMagicNumber) { 266 | return (imageMagicNumber & EXIF_MAGIC_NUMBER) == EXIF_MAGIC_NUMBER 267 | || imageMagicNumber == MOTOROLA_TIFF_MAGIC_NUMBER 268 | || imageMagicNumber == INTEL_TIFF_MAGIC_NUMBER; 269 | } 270 | 271 | private static class RandomAccessReader { 272 | private final ByteBuffer data; 273 | 274 | public RandomAccessReader(byte[] data, int length) { 275 | this.data = (ByteBuffer) ByteBuffer.wrap(data) 276 | .order(ByteOrder.BIG_ENDIAN) 277 | .limit(length); 278 | } 279 | 280 | public void order(ByteOrder byteOrder) { 281 | this.data.order(byteOrder); 282 | } 283 | 284 | public int length() { 285 | return data.remaining(); 286 | } 287 | 288 | public int getInt32(int offset) { 289 | return data.getInt(offset); 290 | } 291 | 292 | public short getInt16(int offset) { 293 | return data.getShort(offset); 294 | } 295 | } 296 | 297 | private interface Reader { 298 | int getUInt16() throws IOException; 299 | 300 | short getUInt8() throws IOException; 301 | 302 | long skip(long total) throws IOException; 303 | 304 | int read(byte[] buffer, int byteCount) throws IOException; 305 | } 306 | 307 | private static class StreamReader implements Reader { 308 | private final InputStream is; 309 | 310 | // Motorola / big endian byte order. 311 | public StreamReader(InputStream is) { 312 | this.is = is; 313 | } 314 | 315 | @Override 316 | public int getUInt16() throws IOException { 317 | return (is.read() << 8 & 0xFF00) | (is.read() & 0xFF); 318 | } 319 | 320 | @Override 321 | public short getUInt8() throws IOException { 322 | return (short) (is.read() & 0xFF); 323 | } 324 | 325 | @Override 326 | public long skip(long total) throws IOException { 327 | if (total < 0) { 328 | return 0; 329 | } 330 | 331 | long toSkip = total; 332 | while (toSkip > 0) { 333 | long skipped = is.skip(toSkip); 334 | if (skipped > 0) { 335 | toSkip -= skipped; 336 | } else { 337 | // Skip has no specific contract as to what happens when you reach the end of 338 | // the stream. To differentiate between temporarily not having more data and 339 | // having finished the stream, we read a single byte when we fail to skip any 340 | // amount of data. 341 | int testEofByte = is.read(); 342 | if (testEofByte == -1) { 343 | break; 344 | } else { 345 | toSkip--; 346 | } 347 | } 348 | } 349 | return total - toSkip; 350 | } 351 | 352 | @Override 353 | public int read(byte[] buffer, int byteCount) throws IOException { 354 | int toRead = byteCount; 355 | int read; 356 | while (toRead > 0 && ((read = is.read(buffer, byteCount - toRead, toRead)) != -1)) { 357 | toRead -= read; 358 | } 359 | return byteCount - toRead; 360 | } 361 | } 362 | 363 | public static void copyExif(ExifInterface originalExif, int width, int height, String imageOutputPath) { 364 | String[] attributes = new String[]{ 365 | ExifInterface.TAG_APERTURE, 366 | ExifInterface.TAG_DATETIME, 367 | ExifInterface.TAG_DATETIME_DIGITIZED, 368 | ExifInterface.TAG_EXPOSURE_TIME, 369 | ExifInterface.TAG_FLASH, 370 | ExifInterface.TAG_FOCAL_LENGTH, 371 | ExifInterface.TAG_GPS_ALTITUDE, 372 | ExifInterface.TAG_GPS_ALTITUDE_REF, 373 | ExifInterface.TAG_GPS_DATESTAMP, 374 | ExifInterface.TAG_GPS_LATITUDE, 375 | ExifInterface.TAG_GPS_LATITUDE_REF, 376 | ExifInterface.TAG_GPS_LONGITUDE, 377 | ExifInterface.TAG_GPS_LONGITUDE_REF, 378 | ExifInterface.TAG_GPS_PROCESSING_METHOD, 379 | ExifInterface.TAG_GPS_TIMESTAMP, 380 | ExifInterface.TAG_ISO, 381 | ExifInterface.TAG_MAKE, 382 | ExifInterface.TAG_MODEL, 383 | ExifInterface.TAG_SUBSEC_TIME, 384 | ExifInterface.TAG_SUBSEC_TIME_DIG, 385 | ExifInterface.TAG_SUBSEC_TIME_ORIG, 386 | ExifInterface.TAG_WHITE_BALANCE 387 | }; 388 | 389 | try { 390 | ExifInterface newExif = new ExifInterface(imageOutputPath); 391 | String value; 392 | for (String attribute : attributes) { 393 | value = originalExif.getAttribute(attribute); 394 | if (!TextUtils.isEmpty(value)) { 395 | newExif.setAttribute(attribute, value); 396 | } 397 | } 398 | newExif.setAttribute(ExifInterface.TAG_IMAGE_WIDTH, String.valueOf(width)); 399 | newExif.setAttribute(ExifInterface.TAG_IMAGE_LENGTH, String.valueOf(height)); 400 | newExif.setAttribute(ExifInterface.TAG_ORIENTATION, "0"); 401 | 402 | newExif.saveAttributes(); 403 | 404 | } catch (IOException e) { 405 | Log.d(TAG, e.getMessage()); 406 | } 407 | } 408 | 409 | } 410 | 411 | -------------------------------------------------------------------------------- /durban/src/main/java/com/yanzhenjie/durban/DurbanActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © Yan Zhenjie 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.yanzhenjie.durban; 17 | 18 | import android.Manifest; 19 | import android.content.Intent; 20 | import android.content.pm.PackageManager; 21 | import android.graphics.Bitmap; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.support.annotation.NonNull; 25 | import android.support.v4.app.ActivityCompat; 26 | import android.support.v4.content.ContextCompat; 27 | import android.support.v4.view.ViewCompat; 28 | import android.support.v7.app.ActionBar; 29 | import android.support.v7.app.AppCompatActivity; 30 | import android.support.v7.widget.Toolbar; 31 | import android.text.TextUtils; 32 | import android.util.Log; 33 | import android.view.Menu; 34 | import android.view.MenuItem; 35 | import android.view.View; 36 | import android.view.Window; 37 | import android.view.WindowManager; 38 | import android.view.animation.AccelerateInterpolator; 39 | 40 | import com.yanzhenjie.durban.callback.BitmapCropCallback; 41 | import com.yanzhenjie.durban.util.DurbanUtils; 42 | import com.yanzhenjie.durban.view.CropView; 43 | import com.yanzhenjie.durban.view.GestureCropImageView; 44 | import com.yanzhenjie.durban.view.OverlayView; 45 | import com.yanzhenjie.durban.view.TransformImageView; 46 | 47 | import java.util.ArrayList; 48 | import java.util.Locale; 49 | 50 | /** 51 | * Update by Yan Zhenjie on 2017/5/23. 52 | */ 53 | public class DurbanActivity extends AppCompatActivity { 54 | 55 | private static final int PERMISSION_CODE_STORAGE = 1; 56 | 57 | private int mStatusColor; 58 | private int mNavigationColor; 59 | private int mToolbarColor; 60 | private String mTitle; 61 | 62 | private int mGesture; 63 | private float[] mAspectRatio; 64 | private int[] mMaxWidthHeight; 65 | 66 | private Bitmap.CompressFormat mCompressFormat; 67 | private int mCompressQuality; 68 | 69 | private String mOutputDirectory; 70 | private ArrayList mInputPathList; 71 | 72 | private Controller mController; 73 | 74 | private CropView mCropView; 75 | private GestureCropImageView mCropImageView; 76 | 77 | private ArrayList mOutputPathList; 78 | 79 | @Override 80 | public void onCreate(Bundle savedInstanceState) { 81 | super.onCreate(savedInstanceState); 82 | Locale locale = Durban.getDurbanConfig().getLocale(); 83 | DurbanUtils.applyLanguageForContext(this, locale); 84 | 85 | setContentView(R.layout.durban_activity_photobox); 86 | final Intent intent = getIntent(); 87 | 88 | initArgument(intent); 89 | initFrameViews(); 90 | initContentViews(); 91 | initControllerViews(); 92 | cropNextImage(); 93 | } 94 | 95 | @Override 96 | protected void onStop() { 97 | super.onStop(); 98 | if (mCropImageView != null) mCropImageView.cancelAllAnimations(); 99 | } 100 | 101 | private void initArgument(Intent intent) { 102 | mStatusColor = ContextCompat.getColor(this, R.color.durban_ColorPrimaryDark); 103 | mToolbarColor = ContextCompat.getColor(this, R.color.durban_ColorPrimary); 104 | mNavigationColor = ContextCompat.getColor(this, R.color.durban_ColorPrimaryBlack); 105 | 106 | mStatusColor = intent.getIntExtra(Durban.KEY_INPUT_STATUS_COLOR, mStatusColor); 107 | mToolbarColor = intent.getIntExtra(Durban.KEY_INPUT_TOOLBAR_COLOR, mToolbarColor); 108 | mNavigationColor = intent.getIntExtra(Durban.KEY_INPUT_NAVIGATION_COLOR, mNavigationColor); 109 | mTitle = intent.getStringExtra(Durban.KEY_INPUT_TITLE); 110 | if (TextUtils.isEmpty(mTitle)) mTitle = getString(R.string.durban_title_crop); 111 | 112 | mGesture = intent.getIntExtra(Durban.KEY_INPUT_GESTURE, Durban.GESTURE_ALL); 113 | mAspectRatio = intent.getFloatArrayExtra(Durban.KEY_INPUT_ASPECT_RATIO); 114 | if (mAspectRatio == null) mAspectRatio = new float[]{0, 0}; 115 | mMaxWidthHeight = intent.getIntArrayExtra(Durban.KEY_INPUT_MAX_WIDTH_HEIGHT); 116 | if (mMaxWidthHeight == null) mMaxWidthHeight = new int[]{500, 500}; 117 | 118 | //noinspection JavacQuirks 119 | int compressFormat = intent.getIntExtra(Durban.KEY_INPUT_COMPRESS_FORMAT, 0); 120 | mCompressFormat = compressFormat == Durban.COMPRESS_PNG ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG; 121 | mCompressQuality = intent.getIntExtra(Durban.KEY_INPUT_COMPRESS_QUALITY, 90); 122 | 123 | mOutputDirectory = intent.getStringExtra(Durban.KEY_INPUT_DIRECTORY); 124 | if (TextUtils.isEmpty(mOutputDirectory)) mOutputDirectory = getFilesDir().getAbsolutePath(); 125 | mInputPathList = intent.getStringArrayListExtra(Durban.KEY_INPUT_PATH_ARRAY); 126 | 127 | mController = intent.getParcelableExtra(Durban.KEY_INPUT_CONTROLLER); 128 | if (mController == null) mController = Controller.newBuilder().build(); 129 | 130 | mOutputPathList = new ArrayList<>(); 131 | } 132 | 133 | private void initFrameViews() { 134 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 135 | final Window window = getWindow(); 136 | if (window != null) { 137 | window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); 138 | window.setStatusBarColor(mStatusColor); 139 | window.setNavigationBarColor(mNavigationColor); 140 | } 141 | } 142 | 143 | final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 144 | toolbar.setBackgroundColor(mToolbarColor); 145 | setSupportActionBar(toolbar); 146 | 147 | final ActionBar actionBar = getSupportActionBar(); 148 | assert actionBar != null; 149 | actionBar.setDefaultDisplayHomeAsUpEnabled(true); 150 | actionBar.setTitle(mTitle); 151 | } 152 | 153 | private void initContentViews() { 154 | mCropView = (CropView) findViewById(R.id.crop_view); 155 | mCropImageView = mCropView.getCropImageView(); 156 | mCropImageView.setOutputDirectory(mOutputDirectory); 157 | mCropImageView.setTransformImageListener(mImageListener); 158 | mCropImageView.setScaleEnabled(mGesture == Durban.GESTURE_ALL || mGesture == Durban.GESTURE_SCALE); 159 | mCropImageView.setRotateEnabled(mGesture == Durban.GESTURE_ALL || mGesture == Durban.GESTURE_ROTATE); 160 | // Durban image view options 161 | mCropImageView.setMaxBitmapSize(GestureCropImageView.DEFAULT_MAX_BITMAP_SIZE); 162 | mCropImageView.setMaxScaleMultiplier(GestureCropImageView.DEFAULT_MAX_SCALE_MULTIPLIER); 163 | mCropImageView.setImageToWrapCropBoundsAnimDuration(GestureCropImageView.DEFAULT_IMAGE_TO_CROP_BOUNDS_ANIM_DURATION); 164 | 165 | // Overlay view options 166 | OverlayView overlayView = mCropView.getOverlayView(); 167 | overlayView.setFreestyleCropMode(OverlayView.FREESTYLE_CROP_MODE_DISABLE); 168 | overlayView.setDimmedColor(ContextCompat.getColor(this, R.color.durban_CropDimmed)); 169 | overlayView.setCircleDimmedLayer(false); 170 | overlayView.setShowCropFrame(true); 171 | overlayView.setCropFrameColor(ContextCompat.getColor(this, R.color.durban_CropFrameLine)); 172 | overlayView.setCropFrameStrokeWidth(getResources().getDimensionPixelSize(R.dimen.durban_dp_1)); 173 | overlayView.setShowCropGrid(true); 174 | overlayView.setCropGridRowCount(2); 175 | overlayView.setCropGridColumnCount(2); 176 | overlayView.setCropGridColor(ContextCompat.getColor(this, R.color.durban_CropGridLine)); 177 | overlayView.setCropGridStrokeWidth(getResources().getDimensionPixelSize(R.dimen.durban_dp_1)); 178 | 179 | // Aspect ratio options 180 | if (mAspectRatio[0] > 0 && mAspectRatio[1] > 0) mCropImageView.setTargetAspectRatio(mAspectRatio[0] / mAspectRatio[1]); 181 | else mCropImageView.setTargetAspectRatio(GestureCropImageView.SOURCE_IMAGE_ASPECT_RATIO); 182 | 183 | // Result exception max size options 184 | if (mMaxWidthHeight[0] > 0 && mMaxWidthHeight[1] > 0) { 185 | mCropImageView.setMaxResultImageSizeX(mMaxWidthHeight[0]); 186 | mCropImageView.setMaxResultImageSizeY(mMaxWidthHeight[1]); 187 | } 188 | } 189 | 190 | private TransformImageView.TransformImageListener mImageListener = new TransformImageView.TransformImageListener() { 191 | @Override 192 | public void onRotate(float currentAngle) { 193 | } 194 | 195 | @Override 196 | public void onScale(float currentScale) { 197 | } 198 | 199 | @Override 200 | public void onLoadComplete() { 201 | ViewCompat.animate(mCropView) 202 | .alpha(1) 203 | .setDuration(300) 204 | .setInterpolator(new AccelerateInterpolator()); 205 | } 206 | 207 | @Override 208 | public void onLoadFailure() { 209 | cropNextImage(); 210 | } 211 | }; 212 | 213 | private void initControllerViews() { 214 | View controllerRoot = findViewById(R.id.iv_controller_root); 215 | 216 | View rotationTitle = findViewById(R.id.tv_controller_title_rotation); 217 | View rotationLeft = findViewById(R.id.layout_controller_rotation_left); 218 | View rotationRight = findViewById(R.id.layout_controller_rotation_right); 219 | View scaleTitle = findViewById(R.id.tv_controller_title_scale); 220 | View scaleBig = findViewById(R.id.layout_controller_scale_big); 221 | View scaleSmall = findViewById(R.id.layout_controller_scale_small); 222 | 223 | controllerRoot.setVisibility(mController.isEnable() ? View.VISIBLE : View.GONE); 224 | 225 | rotationTitle.setVisibility(mController.isRotationTitle() ? View.VISIBLE : View.INVISIBLE); 226 | rotationLeft.setVisibility(mController.isRotation() ? View.VISIBLE : View.GONE); 227 | rotationRight.setVisibility(mController.isRotation() ? View.VISIBLE : View.GONE); 228 | scaleTitle.setVisibility(mController.isScaleTitle() ? View.VISIBLE : View.INVISIBLE); 229 | scaleBig.setVisibility(mController.isScale() ? View.VISIBLE : View.GONE); 230 | scaleSmall.setVisibility(mController.isScale() ? View.VISIBLE : View.GONE); 231 | 232 | if (!mController.isRotationTitle() && !mController.isScaleTitle()) 233 | findViewById(R.id.layout_controller_title_root).setVisibility(View.GONE); 234 | if (!mController.isRotation()) 235 | rotationTitle.setVisibility(View.GONE); 236 | if (!mController.isScale()) 237 | scaleTitle.setVisibility(View.GONE); 238 | 239 | rotationLeft.setOnClickListener(mControllerClick); 240 | rotationRight.setOnClickListener(mControllerClick); 241 | scaleBig.setOnClickListener(mControllerClick); 242 | scaleSmall.setOnClickListener(mControllerClick); 243 | } 244 | 245 | private View.OnClickListener mControllerClick = new View.OnClickListener() { 246 | @Override 247 | public void onClick(View v) { 248 | int id = v.getId(); 249 | if (id == R.id.layout_controller_rotation_left) { 250 | mCropImageView.postRotate(-90); 251 | mCropImageView.setImageToWrapCropBounds(); 252 | } else if (id == R.id.layout_controller_rotation_right) { 253 | mCropImageView.postRotate(90); 254 | mCropImageView.setImageToWrapCropBounds(); 255 | } else if (id == R.id.layout_controller_scale_big) { 256 | mCropImageView.zoomOutImage(mCropImageView.getCurrentScale() 257 | + ((mCropImageView.getMaxScale() - mCropImageView.getMinScale()) / 10)); 258 | mCropImageView.setImageToWrapCropBounds(); 259 | } else if (id == R.id.layout_controller_scale_small) { 260 | mCropImageView.zoomInImage(mCropImageView.getCurrentScale() 261 | - ((mCropImageView.getMaxScale() - mCropImageView.getMinScale()) / 10)); 262 | mCropImageView.setImageToWrapCropBounds(); 263 | } 264 | } 265 | }; 266 | 267 | /** 268 | * Start cropping and request permission if there is no permission. 269 | */ 270 | private void cropNextImage() { 271 | resetRotation(); 272 | requestStoragePermission(PERMISSION_CODE_STORAGE); 273 | } 274 | 275 | /** 276 | * Restore the rotation angle. 277 | */ 278 | private void resetRotation() { 279 | mCropImageView.postRotate(-mCropImageView.getCurrentAngle()); 280 | mCropImageView.setImageToWrapCropBounds(); 281 | } 282 | 283 | private void requestStoragePermission(int requestCode) { 284 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 285 | int permissionResult = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); 286 | if (permissionResult == PackageManager.PERMISSION_GRANTED) { 287 | onRequestPermissionsResult( 288 | requestCode, 289 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 290 | new int[]{PackageManager.PERMISSION_GRANTED}); 291 | } else { 292 | ActivityCompat.requestPermissions( 293 | this, 294 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 295 | requestCode); 296 | } 297 | } else { 298 | onRequestPermissionsResult( 299 | requestCode, 300 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 301 | new int[]{PackageManager.PERMISSION_GRANTED}); 302 | } 303 | } 304 | 305 | @Override 306 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 307 | switch (requestCode) { 308 | case PERMISSION_CODE_STORAGE: { 309 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 310 | cropNextImageWithPermission(); 311 | } else { 312 | Log.e("Durban", "Storage device permission is denied."); 313 | setResultFailure(); 314 | } 315 | break; 316 | } 317 | } 318 | } 319 | 320 | private void cropNextImageWithPermission() { 321 | if (mInputPathList != null) { 322 | if (mInputPathList.size() > 0) { 323 | String currentPath = mInputPathList.remove(0); 324 | try { 325 | mCropImageView.setImagePath(currentPath); 326 | } catch (Exception e) { 327 | cropNextImage(); 328 | } 329 | } else if (mOutputPathList.size() > 0) setResultSuccessful(); 330 | else setResultFailure(); 331 | } else { 332 | Log.e("Durban", "The file list is empty."); 333 | setResultFailure(); 334 | } 335 | } 336 | 337 | @Override 338 | public boolean onCreateOptionsMenu(final Menu menu) { 339 | getMenuInflater().inflate(R.menu.durban_menu_activity, menu); 340 | return true; 341 | } 342 | 343 | @Override 344 | public boolean onOptionsItemSelected(MenuItem item) { 345 | if (item.getItemId() == R.id.menu_action_ok) { 346 | cropAndSaveImage(); 347 | } else if (item.getItemId() == android.R.id.home) { 348 | setResultFailure(); 349 | } 350 | return true; 351 | } 352 | 353 | private void cropAndSaveImage() { 354 | mCropImageView.cropAndSaveImage(mCompressFormat, mCompressQuality, cropCallback); 355 | } 356 | 357 | private BitmapCropCallback cropCallback = new BitmapCropCallback() { 358 | @Override 359 | public void onBitmapCropped(@NonNull String imagePath, int imageWidth, int imageHeight) { 360 | mOutputPathList.add(imagePath); 361 | cropNextImage(); 362 | } 363 | 364 | @Override 365 | public void onCropFailure(@NonNull Throwable t) { 366 | cropNextImage(); 367 | } 368 | }; 369 | 370 | private void setResultSuccessful() { 371 | Intent intent = new Intent(); 372 | intent.putStringArrayListExtra(Durban.KEY_OUTPUT_IMAGE_LIST, mOutputPathList); 373 | setResult(RESULT_OK, intent); 374 | finish(); 375 | } 376 | 377 | private void setResultFailure() { 378 | Intent intent = new Intent(); 379 | intent.putStringArrayListExtra(Durban.KEY_OUTPUT_IMAGE_LIST, mOutputPathList); 380 | setResult(RESULT_CANCELED, intent); 381 | finish(); 382 | } 383 | 384 | } --------------------------------------------------------------------------------