├── .gitignore
├── .idea
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── gradle.xml
├── jarRepositories.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── aar
└── crop_image.aar
├── app
├── .gitignore
├── build.gradle
├── libs
│ └── crop_image.aar
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── org
│ │ └── liaohailong
│ │ └── cropimageview
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── org
│ │ │ └── liaohailong
│ │ │ └── cropimageview
│ │ │ ├── ContentKits.java
│ │ │ ├── MainActivity.kt
│ │ │ └── PermissionUtil.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ └── activity_main.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── 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
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── provider_paths.xml
│ └── test
│ └── java
│ └── org
│ └── liaohailong
│ └── cropimageview
│ └── ExampleUnitTest.kt
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── images
├── example01.gif
├── example02.gif
└── example03.png
├── library
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── org
│ │ └── liaohailong
│ │ └── library
│ │ └── ExampleInstrumentedTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── org
│ │ │ └── liaohailong
│ │ │ └── library
│ │ │ ├── CropImageActivity.java
│ │ │ ├── CropOptions.java
│ │ │ └── CropPhotoView.java
│ └── res
│ │ ├── drawable
│ │ ├── selector_green_item_click_bg.xml
│ │ ├── selector_item_click_bg.xml
│ │ ├── shape_round_rect_green.xml
│ │ └── view_progress_bg.xml
│ │ ├── layout
│ │ └── activity_crop_image.xml
│ │ └── values
│ │ ├── colors.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── org
│ └── liaohailong
│ └── library
│ └── ExampleUnitTest.java
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | CropImageView
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | xmlns:android
33 |
34 | ^$
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | xmlns:.*
44 |
45 | ^$
46 |
47 |
48 | BY_NAME
49 |
50 |
51 |
52 |
53 |
54 |
55 | .*:id
56 |
57 | http://schemas.android.com/apk/res/android
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | .*:name
67 |
68 | http://schemas.android.com/apk/res/android
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | name
78 |
79 | ^$
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | style
89 |
90 | ^$
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | ^$
102 |
103 |
104 | BY_NAME
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | http://schemas.android.com/apk/res/android
114 |
115 |
116 | ANDROID_ATTRIBUTE_ORDER
117 |
118 |
119 |
120 |
121 |
122 |
123 | .*
124 |
125 | .*
126 |
127 |
128 | BY_NAME
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
22 |
23 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CropImageView
2 | 仿微信图片裁剪功能,支持对图片两指缩放,旋转
3 |
4 | 图片裁剪成正方形
5 |
6 |
7 |
8 | 图片裁剪成长方形
9 |
10 |
11 |
12 | 使用方式:下载aar包,项目依赖比较快:https://github.com/liaohailong190/CropImageView/tree/master/aar
13 |
14 |
15 |
16 | 在Activity中调用以下代码,调起图片裁剪界面
17 | ```kotlin
18 | val uri = Uri.fromFile(this) // 资源图片uri
19 | val output = Uri.fromFile(generateImageFile()) // 输出图片uri
20 | val width = resources.displayMetrics.widthPixels // 输出宽度 px
21 | val height = (width * cropRatio).toInt() // 输出高度 px
22 | // 裁剪的宽高比例,通过width和height来控制->width/height
23 | val options: CropOptions = CropOptions.Factory.create(
24 | uri,
25 | output,
26 | width,
27 | height,
28 | Bitmap.CompressFormat.JPEG
29 | )
30 | CropImageActivity.showForResult(this@MainActivity, options, REQUEST_CROP)
31 | ```
32 |
33 | 在调起的Activitiy中复写onActivityResult函数,获取裁剪返回值
34 | ```kotlin
35 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
36 | super.onActivityResult(requestCode, resultCode, data)
37 |
38 | if (resultCode == Activity.RESULT_OK) {
39 | when (requestCode) {
40 | REQUEST_CROP -> {
41 | data?.apply {
42 | var uri = getData()
43 | // uri is the crop result , do something you want...
44 | }
45 | }
46 | }
47 | }
48 | }
49 | ```
50 |
--------------------------------------------------------------------------------
/aar/crop_image.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/aar/crop_image.aar
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 |
8 | defaultConfig {
9 | applicationId "org.liaohailong.cropimageview"
10 | minSdkVersion 21
11 | targetSdkVersion 29
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 |
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_1_8
19 | targetCompatibility JavaVersion.VERSION_1_8
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | }
29 |
30 | repositories {
31 | flatDir {
32 | dirs 'libs'
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation fileTree(dir: "libs", include: ["*.jar"])
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
39 | implementation 'androidx.core:core-ktx:1.3.1'
40 | implementation 'androidx.appcompat:appcompat:1.2.0'
41 | implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
42 | testImplementation 'junit:junit:4.12'
43 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
44 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
45 |
46 | // implementation(name:'crop_image', ext:'aar')
47 | implementation project(path: ':library')
48 | }
--------------------------------------------------------------------------------
/app/libs/crop_image.aar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/libs/crop_image.aar
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/org/liaohailong/cropimageview/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package org.liaohailong.cropimageview
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("org.liaohailong.cropimageview", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/org/liaohailong/cropimageview/ContentKits.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.cropimageview;
2 |
3 | import android.content.ContentUris;
4 | import android.content.ContentValues;
5 | import android.content.Context;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.os.Build;
9 | import android.os.Environment;
10 | import android.provider.DocumentsContract;
11 | import android.provider.MediaStore;
12 |
13 | import androidx.annotation.NonNull;
14 |
15 | import java.io.File;
16 |
17 | /**
18 | * 根据不同的uri 类型 获取资源路径工具类
19 | * 从android官方demo中找到的工具类
20 | *
21 | * Created by Tesla on 4/7/2017.
22 | */
23 | public class ContentKits {
24 |
25 | private ContentKits() {
26 | }
27 |
28 | /**
29 | * Get a file path from a Uri. This will get the the path for Storage Access
30 | * Framework Documents, as well as the _data field for the MediaStore and
31 | * other file-based ContentProviders.
32 | *
33 | * @param context The context.
34 | * @param uri The Uri to query.
35 | * @author paulburke
36 | */
37 | public static String getPath(final Context context, final Uri uri) {
38 | if (null == uri) {
39 | return "";
40 | }
41 | // DocumentProvider
42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
43 | DocumentsContract.isDocumentUri(context, uri)) {
44 | // ExternalStorageProvider
45 | if (isExternalStorageDocument(uri)) {
46 | final String docId = DocumentsContract.getDocumentId(uri);
47 | final String[] split = docId.split(":");
48 | final String type = split[0];
49 |
50 | if ("primary".equalsIgnoreCase(type)) {
51 | return Environment.getExternalStorageDirectory() + "/" + split[1];
52 | }
53 | } else if (isDownloadsDocument(uri)) {
54 | // DownloadsProvider
55 | final String id = DocumentsContract.getDocumentId(uri);
56 | final Uri contentUri = ContentUris.withAppendedId(
57 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
58 | return getDataColumn(context, contentUri, null, null);
59 | } else if (isMediaDocument(uri)) {
60 | // MediaProvider
61 | final String docId = DocumentsContract.getDocumentId(uri);
62 | final String[] split = docId.split(":");
63 | final String type = split[0];
64 |
65 | Uri contentUri = null;
66 | if ("image".equals(type)) {
67 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
68 | } else if ("video".equals(type)) {
69 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
70 | } else if ("audio".equals(type)) {
71 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
72 | }
73 |
74 | final String selection = "_id=?";
75 | final String[] selectionArgs = new String[]{
76 | split[1]
77 | };
78 |
79 | return getDataColumn(context, contentUri, selection, selectionArgs);
80 | }
81 | } else if ("content".equalsIgnoreCase(uri.getScheme())) {
82 | // MediaStore (and general)
83 | // Return the remote address
84 | if (isGooglePhotosUri(uri))
85 | return uri.getLastPathSegment();
86 |
87 | return getDataColumn(context, uri, null, null);
88 | } else if ("file".equalsIgnoreCase(uri.getScheme())) {
89 | // File
90 | return uri.getPath();
91 | }
92 |
93 | return "";
94 | }
95 |
96 | /**
97 | * *
98 | * Get the value of the data column for this Uri. This is useful for
99 | * MediaStore Uris, and other file-based ContentProviders.
100 | *
101 | * @param context The context.
102 | * @param uri The Uri to query.
103 | * @param selection (Optional) Filter used in the query.
104 | * @param selectionArgs (Optional) Selection arguments used in the query.
105 | * @return The value of the _data column, which is typically a file path.
106 | */
107 | public static String getDataColumn(Context context, Uri uri, String selection,
108 | String[] selectionArgs) {
109 | Cursor cursor = null;
110 | final String column = "_data";
111 | final String[] projection = {
112 | column
113 | };
114 | try {
115 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
116 | null);
117 | if (cursor != null && cursor.moveToFirst()) {
118 | final int index = cursor.getColumnIndexOrThrow(column);
119 | return cursor.getString(index);
120 | }
121 | } finally {
122 | if (cursor != null) {
123 | cursor.close();
124 | }
125 | }
126 | return "";
127 | }
128 |
129 | /**
130 | * @param uri The Uri to check.
131 | * @return Whether the Uri authority is ExternalStorageProvider.
132 | */
133 | public static boolean isExternalStorageDocument(Uri uri) {
134 | return "com.android.externalstorage.documents".equals(uri.getAuthority());
135 | }
136 |
137 | /**
138 | * @param uri The Uri to check.
139 | * @return Whether the Uri authority is DownloadsProvider.
140 | */
141 | public static boolean isDownloadsDocument(Uri uri) {
142 | return "com.android.providers.downloads.documents".equals(uri.getAuthority());
143 | }
144 |
145 | /**
146 | * @param uri The Uri to check.
147 | * @return Whether the Uri authority is MediaProvider.
148 | */
149 | public static boolean isMediaDocument(Uri uri) {
150 | return "com.android.providers.media.documents".equals(uri.getAuthority());
151 | }
152 |
153 | /**
154 | * @param uri The Uri to check.
155 | * @return Whether the Uri authority is Google Photos.
156 | */
157 | public static boolean isGooglePhotosUri(Uri uri) {
158 | return "com.google.android.apps.photos.content".equals(uri.getAuthority());
159 | }
160 |
161 | /**
162 | * 转换 content:// uri
163 | *
164 | * @param imageFile 文件
165 | */
166 | public static Uri getImageContentUri(@NonNull Context context, File imageFile) {
167 | String filePath = imageFile.getAbsolutePath();
168 | Cursor cursor = context.getContentResolver().query(
169 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
170 | new String[]{MediaStore.Images.Media._ID},
171 | MediaStore.Images.Media.DATA + "=? ",
172 | new String[]{filePath}, null);
173 | if (cursor != null && cursor.moveToFirst()) {
174 | int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
175 | Uri baseUri = Uri.parse("content://media/external/images/media");
176 | cursor.close();
177 | return Uri.withAppendedPath(baseUri, "" + id);
178 | } else {
179 | if (imageFile.exists()) {
180 | ContentValues values = new ContentValues();
181 | values.put(MediaStore.Images.Media.DATA, filePath);
182 | return context.getContentResolver().insert(
183 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
184 | } else {
185 | return null;
186 | }
187 | }
188 | }
189 |
190 | /**
191 | * 是否是来自存储卡上
192 | * @param uri 文件uri
193 | * @return true表示是内存路径
194 | */
195 | public static boolean isStorage(Uri uri) {
196 | String path = uri.getPath();
197 | return path != null && path.startsWith("/storage");
198 | }
199 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/liaohailong/cropimageview/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package org.liaohailong.cropimageview
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.app.AlertDialog
6 | import android.content.Context
7 | import android.content.Intent
8 | import android.graphics.Bitmap
9 | import android.graphics.BitmapFactory
10 | import android.net.Uri
11 | import android.os.*
12 | import android.provider.MediaStore
13 | import android.widget.Toast
14 | import androidx.core.content.FileProvider
15 | import androidx.fragment.app.FragmentActivity
16 | import kotlinx.android.synthetic.main.activity_main.*
17 | import org.liaohailong.library.CropImageActivity
18 | import org.liaohailong.library.CropOptions
19 | import java.io.File
20 | import java.io.FileInputStream
21 | import java.io.FileOutputStream
22 |
23 | /**
24 | * Describe:
25 | *
26 | *
27 | * @author liaohailong
28 | * Time: 2020/9/29 13:56
29 | */
30 | class MainActivity : FragmentActivity() {
31 | companion object {
32 | const val REQUEST_ALBUM = 0
33 | const val REQUEST_CAPTURE = 1
34 | const val REQUEST_CROP = 2
35 | }
36 |
37 | private var displayFile: File? = null
38 | private var captureFile: File? = null
39 | private var cropUri: Uri? = null
40 | private val mainHandler = Handler(Looper.getMainLooper())
41 |
42 | override fun onCreate(savedInstanceState: Bundle?) {
43 | super.onCreate(savedInstanceState)
44 | setContentView(R.layout.activity_main)
45 |
46 | val permissions = arrayOf(
47 | Manifest.permission.WRITE_EXTERNAL_STORAGE,
48 | Manifest.permission.CAMERA
49 | )
50 | PermissionUtil.requestIfNot(
51 | this,
52 | listOf(*permissions),
53 | 0
54 | )
55 |
56 | open_album.setOnClickListener {
57 | val intent = Intent()
58 | intent.action = Intent.ACTION_PICK
59 | intent.type = "image/*"
60 | startActivityForResult(intent, REQUEST_ALBUM)
61 | }
62 | jump_crop.setOnClickListener {
63 | displayFile?.apply {
64 | val items = arrayOf("1:1", "16:9", "9:16", "4:3", "3:4", "1:2", "2:1")
65 | AlertDialog.Builder(this@MainActivity, 0)
66 | .setTitle("请选择裁剪宽高比")
67 | .setIcon(R.mipmap.ic_launcher)
68 | .setSingleChoiceItems(items, 0) { dialog, which ->
69 | val cropRatio = when (which) {
70 | 0 -> 1f / 1f
71 | 1 -> 16f / 9f
72 | 2 -> 9f / 16f
73 | 3 -> 4f / 3f
74 | 4 -> 3f / 4f
75 | 5 -> 1f / 2f
76 | 6 -> 2f / 1f
77 | else -> 1.0f
78 | }
79 | dialog.dismiss()
80 | val uri = Uri.fromFile(this) // 资源图片uri
81 | val output = Uri.fromFile(generateImageFile()) // 输出图片uri
82 | val width = resources.displayMetrics.widthPixels // 输出宽度 px
83 | val height = (width * cropRatio).toInt() // 输出高度 px
84 | // 裁剪的宽高比例,通过width和height来控制->width/height
85 | val options: CropOptions = CropOptions.Factory.create(
86 | uri,
87 | output,
88 | width,
89 | height,
90 | Bitmap.CompressFormat.JPEG
91 | )
92 | CropImageActivity.showForResult(this@MainActivity, options, REQUEST_CROP)
93 | }.create().show()
94 | }
95 | }
96 | take_photo.setOnClickListener {
97 | //创建打开本地相机的意图对象
98 | val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
99 | //设置图片的保存位置(兼容Android7.0)
100 | captureFile = generateImageFile()
101 | captureFile?.apply {
102 | val fileUri = getUriForFile(this@MainActivity, this)
103 | //指定图片保存位置
104 | intent.putExtra(MediaStore.EXTRA_OUTPUT, fileUri)
105 | }
106 | //开启意图
107 | startActivityForResult(intent, REQUEST_CAPTURE)
108 | }
109 | }
110 |
111 | private fun setImageByPath(path: String) {
112 | val bitmap = BitmapFactory.decodeFile(path)
113 | crop_img.setImageBitmap(bitmap)
114 | }
115 |
116 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
117 | super.onActivityResult(requestCode, resultCode, data)
118 |
119 | if (resultCode == Activity.RESULT_OK) {
120 | when (requestCode) {
121 | REQUEST_ALBUM -> {
122 | data?.apply {
123 | val uri = getData() ?: return
124 | AsyncTask.THREAD_POOL_EXECUTOR.execute {
125 | displayFile = generateImageFile()
126 | if (copyResult(uri, displayFile!!)) {
127 | open_album.post {
128 | setImageByPath(displayFile!!.absolutePath)
129 | }
130 | }
131 | }
132 | }
133 | }
134 | REQUEST_CAPTURE -> {
135 | AsyncTask.THREAD_POOL_EXECUTOR.execute {
136 | val uri = Uri.fromFile(captureFile!!)
137 | displayFile = generateImageFile()
138 | if (copyResult(uri, displayFile!!)) {
139 | open_album.post {
140 | setImageByPath(displayFile!!.absolutePath)
141 | }
142 | }
143 | }
144 | }
145 | REQUEST_CROP -> {
146 | data?.apply {
147 | var uri = getData()
148 | uri = uri ?: cropUri
149 | displayFile = generateImageFile()
150 | if (copyResult(uri!!, displayFile!!)) {
151 | open_album.post {
152 | setImageByPath(displayFile!!.absolutePath)
153 | }
154 | }
155 | }
156 | }
157 | }
158 | }
159 | }
160 |
161 | private fun generateImageFile(): File {
162 | val dir = externalCacheDir
163 | val fileName = "IMG_${System.currentTimeMillis()}.jpg"
164 | return File(dir, fileName)
165 | }
166 |
167 | private fun copyResult(uri: Uri, dstFile: File): Boolean {
168 | val storage = ContentKits.isStorage(uri)
169 | val path = if (storage) uri.path else ContentKits.getPath(this, uri)
170 | if (!File(path!!).exists()) {
171 | mainHandler.post {
172 | Toast.makeText(this, "原图已被删除,请选择其他图片", Toast.LENGTH_SHORT).show()
173 | }
174 | return false
175 | }
176 |
177 | var fis: FileInputStream? = null
178 | var fos: FileOutputStream? = null
179 | try {
180 | fis = if (storage) FileInputStream(File(path)) else {
181 | val pfd: ParcelFileDescriptor = contentResolver.openFileDescriptor(uri, "r")!!
182 | val fd = pfd.fileDescriptor
183 | FileInputStream(fd)
184 | }
185 | fos = FileOutputStream(dstFile)
186 | val readBytes = fis.readBytes()
187 | fos.write(readBytes)
188 | return true
189 | } catch (e: Exception) {
190 | e.printStackTrace()
191 | try {
192 | fis?.close()
193 | } catch (ex: Exception) {
194 | ex.printStackTrace()
195 | }
196 | try {
197 | fos?.close()
198 | } catch (ex: Exception) {
199 | ex.printStackTrace()
200 | }
201 | return false
202 | }
203 | }
204 |
205 | private fun getUriForFile(context: Context, file: File): Uri? {
206 | return if (Build.VERSION.SDK_INT >= 24) {
207 | //参数:authority 需要和清单文件中配置的保持完全一致:${applicationId}.provider
208 | FileProvider.getUriForFile(context, context.packageName + ".provider", file)
209 | } else {
210 | Uri.fromFile(file)
211 | }
212 | }
213 | }
--------------------------------------------------------------------------------
/app/src/main/java/org/liaohailong/cropimageview/PermissionUtil.kt:
--------------------------------------------------------------------------------
1 | package org.liaohailong.cropimageview
2 |
3 | import android.app.Activity
4 | import android.content.pm.PackageManager
5 | import androidx.core.app.ActivityCompat
6 |
7 | /**
8 | * Author: liaohailong
9 | * Date: 2019/3/18
10 | * Time: 8:08 PM
11 | * Description: 权限请求
12 | **/
13 | object PermissionUtil {
14 |
15 | fun requestIfNot(activity: Activity, permission: String, requestCode: Int): Boolean {
16 | return PermissionUtil.requestIfNot(activity, listOf(permission), requestCode)
17 | }
18 |
19 | fun requestIfNot(activity: Activity, permissions: List, requestCode: Int): Boolean {
20 | val deniedPermissions = mutableListOf()
21 | for (permission in permissions) {
22 | if (PackageManager.PERMISSION_DENIED == ActivityCompat.checkSelfPermission(activity, permission)) {
23 | deniedPermissions.add(permission)
24 | }
25 | }
26 | if (deniedPermissions.isEmpty()) return true
27 | ActivityCompat.requestPermissions(activity, deniedPermissions.toTypedArray(), requestCode)
28 | return false
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
17 |
18 |
23 |
24 |
28 |
33 |
34 |
35 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #6200EE
4 | #3700B3
5 | #03DAC5
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | CropImageView
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/provider_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/test/java/org/liaohailong/cropimageview/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package org.liaohailong.cropimageview
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext.kotlin_version = "1.4.0"
4 | repositories {
5 | google()
6 | jcenter()
7 | }
8 | dependencies {
9 | classpath "com.android.tools.build:gradle:4.0.1"
10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
11 |
12 | // NOTE: Do not place your application dependencies here; they belong
13 | // in the individual module build.gradle files
14 | }
15 | }
16 |
17 | allprojects {
18 | repositories {
19 | google()
20 | jcenter()
21 | }
22 | }
23 |
24 | task clean(type: Delete) {
25 | delete rootProject.buildDir
26 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 29 09:42:52 CST 2020
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-6.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
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 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/images/example01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/images/example01.gif
--------------------------------------------------------------------------------
/images/example02.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/images/example02.gif
--------------------------------------------------------------------------------
/images/example03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/images/example03.png
--------------------------------------------------------------------------------
/library/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 |
3 | android {
4 | compileSdkVersion 30
5 | buildToolsVersion "30.0.1"
6 |
7 | defaultConfig {
8 | minSdkVersion 21
9 | targetSdkVersion 30
10 | versionCode 1
11 | versionName "1.0"
12 |
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 |
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_1_8
19 | targetCompatibility JavaVersion.VERSION_1_8
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | }
29 |
30 | dependencies {
31 | implementation fileTree(dir: "libs", include: ["*.jar"])
32 | implementation 'androidx.appcompat:appcompat:1.2.0'
33 | testImplementation 'junit:junit:4.12'
34 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
35 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
36 |
37 | }
--------------------------------------------------------------------------------
/library/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/liaohailong123/CropImageView/1ff2496771606f7600b2987b0bdde7c4b63cd648/library/consumer-rules.pro
--------------------------------------------------------------------------------
/library/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/library/src/androidTest/java/org/liaohailong/library/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.library;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("org.liaohailong.library.test", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/library/src/main/java/org/liaohailong/library/CropImageActivity.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.library;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.graphics.Bitmap;
6 | import android.graphics.BitmapFactory;
7 | import android.graphics.Matrix;
8 | import android.net.Uri;
9 | import android.os.AsyncTask;
10 | import android.os.Bundle;
11 | import android.os.ParcelFileDescriptor;
12 | import android.text.TextUtils;
13 | import android.util.Log;
14 | import android.view.View;
15 | import android.widget.Toast;
16 |
17 | import androidx.annotation.NonNull;
18 | import androidx.annotation.Nullable;
19 | import androidx.appcompat.app.AppCompatActivity;
20 | import androidx.fragment.app.Fragment;
21 |
22 | import java.io.File;
23 | import java.io.FileDescriptor;
24 | import java.io.FileOutputStream;
25 |
26 | /**
27 | * Author: liaohailong
28 | * Time: 2020/9/8 09:06
29 | * Describe: 图片裁剪界面
30 | */
31 | public class CropImageActivity extends AppCompatActivity {
32 | private static final String TAG = "CropImageActivity";
33 | private static final String KEY_OPTIONS = "KEY_OPTIONS";
34 |
35 | public static void showForResult(@NonNull Activity activity, @NonNull CropOptions options, int requestCode) {
36 | Intent intent = new Intent();
37 | intent.setAction("org.liaohailong.view.crop");
38 | intent.addCategory("android.intent.category.DEFAULT");
39 | intent.putExtra(KEY_OPTIONS, options);
40 | activity.startActivityForResult(intent, requestCode);
41 | }
42 |
43 | public static void showForResult(@NonNull Fragment fragment, @NonNull CropOptions options, int requestCode) {
44 | Intent intent = new Intent();
45 | intent.setAction("org.liaohailong.view.crop");
46 | intent.addCategory("android.intent.category.DEFAULT");
47 | intent.putExtra(KEY_OPTIONS, options);
48 | fragment.startActivityForResult(intent, requestCode);
49 | }
50 |
51 |
52 | private CropPhotoView mCropView;
53 | private View mMaskView;
54 | private CropOptions mCropOptions;
55 | private String outputPath = "";
56 |
57 | private int maxBitmapWidth = 0;
58 | private int maxBitmapHeight = 0;
59 | private Bitmap srcBitmap = null;
60 |
61 | @Override
62 | protected void onCreate(@Nullable Bundle savedInstanceState) {
63 | super.onCreate(savedInstanceState);
64 | setContentView(R.layout.activity_crop_image);
65 |
66 | maxBitmapWidth = getResources().getDisplayMetrics().widthPixels;
67 | maxBitmapHeight = getResources().getDisplayMetrics().heightPixels;
68 |
69 | findViewById(R.id.tv_back).setOnClickListener(this::goBack);
70 | findViewById(R.id.tv_rotate).setOnClickListener(this::rotate);
71 | findViewById(R.id.tv_confirm).setOnClickListener(this::confirm);
72 | mCropView = findViewById(R.id.iv_crop);
73 | mMaskView = findViewById(R.id.fl_mask);
74 |
75 | // 遮罩拦截触点
76 | mMaskView.setOnTouchListener((v, event) -> true);
77 |
78 | // 获取原图片
79 | Intent intent = getIntent();
80 | mCropOptions = intent.getParcelableExtra(KEY_OPTIONS);
81 |
82 | // 检查输出路径
83 | if (mCropOptions != null) {
84 | Uri output = mCropOptions.getOutput();
85 | outputPath = output.getPath();
86 | }
87 |
88 | if (TextUtils.isEmpty(outputPath)) {
89 | if (isFinishing()) return;
90 | Toast.makeText(this, "图片保存路径为空", Toast.LENGTH_LONG).show();
91 | goBack(null);
92 | return;
93 | }
94 |
95 | mMaskView.setVisibility(View.VISIBLE);
96 | AsyncTask.SERIAL_EXECUTOR.execute(() -> {
97 | try {
98 | Uri source = mCropOptions.getSource();
99 | ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(source, "r");
100 | if (pfd == null) {
101 | mCropView.post(() -> {
102 | if (isFinishing()) return;
103 | Toast.makeText(this, "图片未找到,请重新选择", Toast.LENGTH_LONG).show();
104 | goBack(null);
105 | });
106 | return;
107 | }
108 | FileDescriptor fd = pfd.getFileDescriptor();
109 |
110 | // 图片裁剪取出,最大手机屏幕大小
111 | BitmapFactory.Options options = new BitmapFactory.Options();
112 | options.inJustDecodeBounds = true;
113 | BitmapFactory.decodeFileDescriptor(fd, null, options);
114 |
115 | // 判断图片最小尺寸 50 x 50
116 | if (options.outWidth * options.outHeight < 50 * 50) {
117 | mCropView.post(() -> {
118 | if (isFinishing()) return;
119 | Toast.makeText(this, "图片尺寸需大于50x50", Toast.LENGTH_LONG).show();
120 | goBack(null);
121 | });
122 | return;
123 | }
124 |
125 | options.inSampleSize = calculateInSampleSize(options, maxBitmapWidth, maxBitmapHeight);
126 | options.inJustDecodeBounds = false;
127 | srcBitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
128 | if (srcBitmap == null) {
129 | if (isFinishing()) return;
130 | Toast.makeText(this, "图片解析失败", Toast.LENGTH_LONG).show();
131 | goBack(null);
132 | return;
133 | }
134 |
135 | // 回调主线程
136 | mCropView.post(() -> {
137 | if (isFinishing()) return;
138 | mMaskView.setVisibility(View.GONE);
139 | float ratio = mCropOptions.getCropRatio();
140 | mCropView.setCropRatio(ratio);
141 | mCropView.setBitmap(srcBitmap);
142 | });
143 | } catch (Exception e) {
144 | mCropView.post(() -> {
145 | if (isFinishing()) return;
146 | Toast.makeText(this, "图片加载失败,请重新选择", Toast.LENGTH_LONG).show();
147 | goBack(null);
148 | });
149 | log("资源图片加载失败:" + e.toString());
150 | log(e);
151 | }
152 | });
153 | }
154 |
155 | public void goBack(View v) {
156 | setResult(Activity.RESULT_CANCELED);
157 | finish();
158 | }
159 |
160 | private int rotateIndex = 0;
161 |
162 | public void rotate(View v) {
163 | rotateIndex++;
164 | int index = rotateIndex % CropPhotoView.Degrees.values().length;
165 | mCropView.rotate(CropPhotoView.Degrees.values()[index]);
166 | }
167 |
168 | public void confirm(View v) {
169 | mMaskView.setVisibility(View.VISIBLE);
170 | mCropView.crop(this::saveBitmapToOutput);
171 | }
172 |
173 | private void saveBitmapToOutput(Bitmap bitmap) {
174 | AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
175 | // 位图缩放至配置尺寸
176 | int outputWidth = mCropOptions.getOutputWidth();
177 | int outputHeight = mCropOptions.getOutputHeight();
178 | // 最大不能超过屏幕尺寸,防止OOM
179 | if (maxBitmapWidth * maxBitmapHeight < outputWidth * outputHeight) {
180 | float scaleW = outputWidth * 1f / maxBitmapWidth;
181 | float scaleH = outputHeight * 1f / maxBitmapHeight;
182 | float scale = Math.max(scaleW, scaleH);
183 | outputWidth = (int) (outputWidth / scale);
184 | outputHeight = (int) (outputHeight / scale);
185 | }
186 |
187 | Bitmap scaleBitmap = scaleBitmap(bitmap, outputWidth, outputHeight);
188 |
189 | // 图片保存本地
190 | Bitmap.CompressFormat format = mCropOptions.getOutputFormat();
191 | FileOutputStream fos = null;
192 | try {
193 | fos = new FileOutputStream(new File(outputPath));
194 | if (scaleBitmap.compress(format, 100, fos)) {
195 | // 保存成功
196 | mCropView.post(() -> {
197 | log("图片裁剪成功 path = " + outputPath);
198 | if (isFinishing()) return;
199 | Intent intent = new Intent();
200 | intent.setData(Uri.parse(outputPath));
201 | setResult(Activity.RESULT_OK, intent);
202 | finish();
203 | });
204 | } else {
205 | // 保存失败
206 | mCropView.post(() -> {
207 | if (isFinishing()) return;
208 | Toast.makeText(this, "图片保存失败", Toast.LENGTH_LONG).show();
209 | goBack(null);
210 | });
211 | }
212 | } catch (Exception e) {
213 | log("图片裁剪失败:" + e.toString());
214 | log(e);
215 | } finally {
216 | try {
217 | if (fos != null) {
218 | fos.close();
219 | }
220 | } catch (Exception e) {
221 | e.printStackTrace();
222 | }
223 | }
224 | });
225 | }
226 |
227 |
228 | private int calculateInSampleSize(BitmapFactory.Options options,
229 | int reqWidth, int reqHeight) {
230 | // 源图片的高度和宽度
231 | final int height = options.outHeight;
232 | final int width = options.outWidth;
233 | int inSampleSize = 1;
234 | if (height > reqHeight || width > reqWidth) {
235 | // 计算出实际宽高和目标宽高的比率
236 | final int heightRatio = Math.round((float) height / (float) reqHeight);
237 | final int widthRatio = Math.round((float) width / (float) reqWidth);
238 | // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
239 | // 一定都会大于等于目标的宽和高。
240 | inSampleSize = Math.min(heightRatio, widthRatio);
241 | }
242 | return inSampleSize;
243 | }
244 |
245 | /**
246 | * 根据给定的宽和高进行拉伸
247 | *
248 | * @param origin 原图
249 | * @param newWidth 新图的宽
250 | * @param newHeight 新图的高
251 | * @return new Bitmap
252 | */
253 | private Bitmap scaleBitmap(Bitmap origin, int newWidth, int newHeight) {
254 | if (origin == null) {
255 | return null;
256 | }
257 | int height = origin.getHeight();
258 | int width = origin.getWidth();
259 | float scaleWidth = ((float) newWidth) / width;
260 | float scaleHeight = ((float) newHeight) / height;
261 | Matrix matrix = new Matrix();
262 | matrix.postScale(scaleWidth, scaleHeight);// 使用后乘
263 | return Bitmap.createBitmap(origin, 0, 0, width, height, matrix, false);
264 | }
265 |
266 | private void log(Object msg) {
267 | Log.i(TAG, msg.toString());
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/library/src/main/java/org/liaohailong/library/CropOptions.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.library;
2 |
3 | import android.graphics.Bitmap;
4 | import android.net.Uri;
5 | import android.os.Parcel;
6 | import android.os.Parcelable;
7 |
8 | import androidx.annotation.IntDef;
9 | import androidx.annotation.NonNull;
10 |
11 | import java.lang.annotation.Retention;
12 | import java.lang.annotation.RetentionPolicy;
13 |
14 | /**
15 | * Author: liaohailong
16 | * Time: 2020/9/8 09:12
17 | * Describe: 裁剪配置信息
18 | */
19 | public class CropOptions implements Parcelable {
20 | public static final int JPEG = 0;
21 | public static final int PNG = 1;
22 | public static final int WEBP = 2;
23 |
24 | @Retention(RetentionPolicy.SOURCE)
25 | @IntDef({JPEG, PNG, WEBP})
26 | @interface CompressFormat {
27 | }
28 |
29 | private Uri source;
30 | private Uri output;
31 | private int outputWidth;
32 | private int outputHeight;
33 | @CompressFormat
34 | private int outputFormat = JPEG;
35 |
36 | private CropOptions(Uri source, Uri output, int outputWidth, int outputHeight, int outputFormat) {
37 | this.source = source;
38 | this.output = output;
39 | this.outputWidth = outputWidth;
40 | this.outputHeight = outputHeight;
41 | this.outputFormat = outputFormat;
42 | }
43 |
44 | public Uri getSource() {
45 | return source;
46 | }
47 |
48 | public Uri getOutput() {
49 | return output;
50 | }
51 |
52 | public int getOutputWidth() {
53 | return outputWidth;
54 | }
55 |
56 | public int getOutputHeight() {
57 | return outputHeight;
58 | }
59 |
60 | public float getCropRatio() {
61 | return (outputWidth * 1f) / (outputHeight * 1f);
62 | }
63 |
64 | public Bitmap.CompressFormat getOutputFormat() {
65 | switch (outputFormat) {
66 | case JPEG:
67 | return Bitmap.CompressFormat.JPEG;
68 | case PNG:
69 | return Bitmap.CompressFormat.PNG;
70 | case WEBP:
71 | return Bitmap.CompressFormat.WEBP;
72 | }
73 | return Bitmap.CompressFormat.JPEG;
74 | }
75 |
76 | public static final class Factory {
77 |
78 | /**
79 | * 构建裁剪配置信息
80 | *
81 | * @param source 源文件
82 | * @param output 输出文件
83 | * @return 裁剪信息
84 | */
85 | public static CropOptions create(@NonNull Uri source, @NonNull Uri output) {
86 | return create(source, output, Bitmap.CompressFormat.JPEG);
87 | }
88 |
89 | /**
90 | * 构建裁剪配置信息
91 | *
92 | * @param source 源文件
93 | * @param output 输出文件
94 | * @param outputFormat 输出格式
95 | * @return 裁剪信息
96 | */
97 | public static CropOptions create(@NonNull Uri source,
98 | @NonNull Uri output,
99 | @NonNull Bitmap.CompressFormat outputFormat) {
100 | return create(source, output, -1, -1, outputFormat);
101 | }
102 |
103 | /**
104 | * 构建裁剪配置信息
105 | *
106 | * @param source 源文件
107 | * @param output 输出文件
108 | * @param outputWidth 输出宽度
109 | * @param outputHeight 输出高度
110 | * @param outputFormat 输出格式
111 | * @return 裁剪信息
112 | */
113 | public static CropOptions create(@NonNull Uri source,
114 | @NonNull Uri output,
115 | int outputWidth,
116 | int outputHeight,
117 | @NonNull Bitmap.CompressFormat outputFormat) {
118 | @CompressFormat int format = JPEG;
119 | switch (outputFormat) {
120 | case JPEG:
121 | format = JPEG;
122 | break;
123 | case PNG:
124 | format = PNG;
125 | break;
126 | case WEBP:
127 | format = WEBP;
128 | break;
129 | }
130 | return new CropOptions(source, output, outputWidth, outputHeight, format);
131 | }
132 | }
133 |
134 | protected CropOptions(Parcel in) {
135 | source = in.readParcelable(Uri.class.getClassLoader());
136 | output = in.readParcelable(Uri.class.getClassLoader());
137 | outputWidth = in.readInt();
138 | outputHeight = in.readInt();
139 | outputFormat = in.readInt();
140 | }
141 |
142 | @Override
143 | public void writeToParcel(Parcel dest, int flags) {
144 | dest.writeParcelable(source, flags);
145 | dest.writeParcelable(output, flags);
146 | dest.writeInt(outputWidth);
147 | dest.writeInt(outputHeight);
148 | dest.writeInt(outputFormat);
149 | }
150 |
151 | @Override
152 | public int describeContents() {
153 | return 0;
154 | }
155 |
156 | public static final Creator CREATOR = new Creator() {
157 | @Override
158 | public CropOptions createFromParcel(Parcel in) {
159 | return new CropOptions(in);
160 | }
161 |
162 | @Override
163 | public CropOptions[] newArray(int size) {
164 | return new CropOptions[size];
165 | }
166 | };
167 | }
168 |
--------------------------------------------------------------------------------
/library/src/main/java/org/liaohailong/library/CropPhotoView.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.library;
2 |
3 | import android.animation.Animator;
4 | import android.animation.AnimatorListenerAdapter;
5 | import android.animation.ValueAnimator;
6 | import android.annotation.SuppressLint;
7 | import android.content.Context;
8 | import android.graphics.Bitmap;
9 | import android.graphics.Canvas;
10 | import android.graphics.Color;
11 | import android.graphics.Matrix;
12 | import android.graphics.Paint;
13 | import android.graphics.Path;
14 | import android.graphics.PointF;
15 | import android.graphics.Rect;
16 | import android.graphics.RectF;
17 | import android.os.AsyncTask;
18 | import android.util.AttributeSet;
19 | import android.util.Log;
20 | import android.view.MotionEvent;
21 | import android.view.VelocityTracker;
22 | import android.view.View;
23 | import android.view.ViewConfiguration;
24 | import android.view.animation.AccelerateDecelerateInterpolator;
25 | import android.view.animation.AccelerateInterpolator;
26 | import android.widget.Scroller;
27 | import android.widget.Toast;
28 |
29 | import androidx.annotation.NonNull;
30 | import androidx.annotation.Nullable;
31 |
32 | import java.util.LinkedList;
33 |
34 | /**
35 | * Author: liaohailong
36 | * Time: 2020/9/9 16:37
37 | * Describe: 图片移动,裁剪框不动的图片裁剪组件
38 | */
39 | public class CropPhotoView extends View {
40 | private static final String TAG = "CropPhotoView";
41 |
42 | private void log(String msg) {
43 | Log.i(TAG, msg);
44 | }
45 |
46 | /**
47 | * 图片区域 - 坐标
48 | */
49 | private Rect bitmapRect = new Rect();
50 |
51 | /**
52 | * 图片可见区域 - 一份原视数据,便于矫正
53 | */
54 | private Rect originVisibleRect = new Rect();
55 |
56 | /**
57 | * 图片可见区域 - 坐标
58 | */
59 | private Rect visibleRect = new Rect();
60 |
61 | /**
62 | * 视图可见区域 - 坐标
63 | */
64 | private Rect viewRect = new Rect();
65 |
66 | /**
67 | * 裁剪区域 - 坐标
68 | */
69 | private Rect cropRect = new Rect();
70 |
71 | /**
72 | * 裁剪区域 - 反向区域
73 | */
74 | private Path cropPath = new Path();
75 |
76 | /**
77 | * 所有用到的画笔
78 | */
79 | private Paint visiblePaint, cropAreaPaint, cropBackgroundPaint;
80 |
81 | /**
82 | * 输入原图
83 | */
84 | private Bitmap src;
85 |
86 | /**
87 | * 裁剪区域比例
88 | */
89 | private float cropRatio = 1.0f;
90 |
91 | /**
92 | * 裁剪宽度与视图的比例
93 | */
94 | private static final float cropWidthPercent = 0.85f;
95 |
96 | /**
97 | * 裁剪高度与视图的比例
98 | */
99 | private static final float cropHeightPercent = 0.45f;
100 |
101 | /**
102 | * 图片最大显示宽度 - 缩放动画限制 - 高度等比例缩放,所以无需计算
103 | */
104 | private int maxVisibleWidth = 0;
105 | private int maxVisibleHeight = 0;
106 |
107 | /**
108 | * 惯性阈值
109 | */
110 | private int maxFlingVelocity, minFlingVelocity;
111 | private Scroller scroller;
112 | private boolean fling = false; // 是否位于惯性中
113 |
114 | /**
115 | * 当前图片旋转角度
116 | */
117 | private float currentDegrees = 0.0f;
118 | /**
119 | * 当前图片旋转角度 - enum
120 | */
121 | private Degrees currentDegreesEnum = Degrees.DEGREES_0;
122 |
123 | public CropPhotoView(Context context) {
124 | this(context, null);
125 | }
126 |
127 | public CropPhotoView(Context context, @Nullable AttributeSet attrs) {
128 | this(context, attrs, 0);
129 | }
130 |
131 | public CropPhotoView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
132 | super(context, attrs, defStyleAttr);
133 |
134 | // 初始化 - 位图显示区域画笔
135 | visiblePaint = new Paint();
136 | visiblePaint.setAntiAlias(true);
137 | visiblePaint.setFilterBitmap(true);
138 | visiblePaint.setStyle(Paint.Style.FILL_AND_STROKE);
139 |
140 | // 初始化 - 裁剪区域画笔
141 | cropAreaPaint = new Paint();
142 | cropAreaPaint.setAntiAlias(true);
143 | cropAreaPaint.setStyle(Paint.Style.FILL_AND_STROKE);
144 | cropAreaPaint.setColor(Color.parseColor("#CC000000"));
145 |
146 | // 初始化 - 裁剪区域画笔
147 | cropBackgroundPaint = new Paint();
148 | cropBackgroundPaint.setAntiAlias(true);
149 | cropBackgroundPaint.setStyle(Paint.Style.FILL_AND_STROKE);
150 | cropBackgroundPaint.setColor(Color.parseColor("#000000"));
151 |
152 | ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
153 | maxFlingVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
154 | minFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
155 | scroller = new Scroller(context);
156 | }
157 |
158 | /* ------------------------------------------------ 7.0以下需修复 ------------------------------------------------ */
159 | private LinkedList waitingQueue = new LinkedList<>();
160 |
161 | private static class HandlerAction {
162 | final Runnable action;
163 | final long delay;
164 |
165 | public HandlerAction(Runnable action, long delay) {
166 | this.action = action;
167 | this.delay = delay;
168 | }
169 |
170 | public boolean matches(Runnable otherAction) {
171 | return otherAction == null && action == null
172 | || action != null && action.equals(otherAction);
173 | }
174 | }
175 |
176 | @Override
177 | protected void onAttachedToWindow() {
178 | super.onAttachedToWindow();
179 | while (true) {
180 | HandlerAction action = waitingQueue.pollFirst();
181 | if (action != null) postDelayed(action.action, action.delay);
182 | else break;
183 | }
184 | }
185 |
186 | @Override
187 | public boolean post(Runnable action) {
188 | if (isAttachedToWindow()) {
189 | return super.post(action);
190 | } else {
191 | waitingQueue.addLast(new HandlerAction(action, 0));
192 | return true;
193 | }
194 | }
195 |
196 | @Override
197 | public boolean postDelayed(Runnable action, long delayMillis) {
198 | if (isAttachedToWindow()) {
199 | return super.postDelayed(action, delayMillis);
200 | } else {
201 | waitingQueue.addLast(new HandlerAction(action, delayMillis));
202 | return true;
203 | }
204 | }
205 |
206 | @Override
207 | public boolean removeCallbacks(Runnable action) {
208 | if (isAttachedToWindow()) {
209 | return super.removeCallbacks(action);
210 | } else {
211 | LinkedList removeList = new LinkedList<>();
212 | for (HandlerAction handlerAction : waitingQueue) {
213 | if (handlerAction.matches(action)) removeList.add(handlerAction);
214 | }
215 | boolean empty = removeList.isEmpty();
216 | for (HandlerAction removed : removeList) {
217 | waitingQueue.remove(removed);
218 | }
219 | return !empty;
220 | }
221 | }
222 |
223 | /* ------------------------------------------------ 初始化区域部分 ------------------------------------------------ */
224 | private Runnable prepareRunnable = new Runnable() {
225 | @Override
226 | public void run() {
227 | int viewWidth = viewRect.width();
228 | int viewHeight = viewRect.height();
229 | // 计算裁剪区域 - 整体居中显示
230 | int cropWidth;
231 | int cropHeight;
232 | if (cropRatio < 0.5f) {
233 | // 竖着的 - 长方形裁剪区域
234 | cropHeight = (int) (viewHeight * cropHeightPercent);
235 | cropWidth = (int) (cropHeight * cropRatio);
236 | } else {
237 | // 横着的 - 长方形裁剪区域 或者 正方形都用宽度限定裁剪尺寸
238 | cropWidth = (int) (viewWidth * cropWidthPercent);
239 | cropHeight = (int) (cropWidth / cropRatio);
240 | }
241 | int cropLeft = (viewWidth - cropWidth) / 2;
242 | int cropTop = (viewHeight - cropHeight) / 2;
243 | int cropRight = cropLeft + cropWidth;
244 | int cropBottom = cropTop + cropHeight;
245 |
246 | cropRect.set(cropLeft, cropTop, cropRight, cropBottom);
247 |
248 | // 计算反向裁剪绘制区域 - 黑色阴影
249 | cropPath.reset();
250 | cropPath.addRect(
251 | viewRect.left,
252 | viewRect.top,
253 | viewRect.right,
254 | viewRect.bottom,
255 | Path.Direction.CW);
256 | Path path = new Path();
257 | path.addRect(
258 | cropRect.left,
259 | cropRect.top,
260 | cropRect.right,
261 | cropRect.bottom,
262 | Path.Direction.CW);
263 | cropPath.op(path, Path.Op.DIFFERENCE);
264 |
265 |
266 | // 位图显示区域,以裁剪区域为参考基准 -> scaleType = centerCrop
267 | int srcWidth = src.getWidth();
268 | int srcHeight = src.getHeight();
269 |
270 | // 设置位图总区域
271 | bitmapRect.set(0, 0, srcWidth, srcHeight);
272 |
273 | float scaleW = srcWidth * 1f / cropWidth;
274 | float scaleH = srcHeight * 1f / cropHeight;
275 | float scale = Math.min(scaleW, scaleH);
276 | int resizeW = (int) (srcWidth / scale);
277 | int resizeH = (int) (srcHeight / scale);
278 |
279 | int visibleLeft = -(resizeW - cropWidth) / 2 + cropLeft;
280 | int visibleRight = visibleLeft + resizeW;
281 | int visibleTop = -(resizeH - cropHeight) / 2 + cropTop;
282 | int visibleBottom = visibleTop + resizeH;
283 |
284 | // 在视图中渲染的位置
285 | visibleRect.set(visibleLeft, visibleTop, visibleRight, visibleBottom);
286 | originVisibleRect.set(visibleRect);
287 |
288 | // 计算最大显示宽度 - 高度等比例缩放,无需计算
289 | maxVisibleWidth = (int) (viewWidth * 2 / cropWidthPercent);
290 | maxVisibleHeight = (int) (viewHeight * 2 / cropHeightPercent);
291 |
292 | // 都算好了,可以渲染了
293 | invalidate();
294 | }
295 | };
296 |
297 | /**
298 | * @param bitmap 设置裁剪原图
299 | */
300 | public void setBitmap(@NonNull Bitmap bitmap) {
301 | src = bitmap;
302 | post(prepareRunnable);
303 | }
304 |
305 | /**
306 | * @param ratio 设置裁剪区域宽高比
307 | */
308 | public void setCropRatio(float ratio) {
309 | cropRatio = ratio;
310 | }
311 |
312 | /**
313 | * 旋转角度
314 | *
315 | * @param degrees 需要旋转的角度 - 固定角度,每次从零度开始算
316 | * @return 是否生效,有时候正在旋转时不能操作
317 | */
318 | public boolean rotate(Degrees degrees) {
319 | abortFling(true); // 旋转前结束惯性动画
320 | abortAdjusting();// 旋转前结束矫正动画
321 | if (rotateAnim != null && rotateAnim.isRunning()) return false; // 正在旋转,恕不执行!
322 | _rotate(degrees);
323 | return true;
324 | }
325 |
326 | /**
327 | * @return 获取当前旋转位置
328 | */
329 | public Degrees getCurrentDegrees() {
330 | return currentDegreesEnum;
331 | }
332 |
333 | /**
334 | * 生成裁剪结果 - 子线程
335 | *
336 | * @param callback 裁剪图片回调 - 主线程
337 | */
338 | public void crop(@NonNull final OnImageCropCallback callback) {
339 | AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
340 | @Override
341 | public void run() {
342 |
343 | try {
344 | // 开始裁剪
345 | // 输出内容到bitmap上
346 | int measuredWidth = getMeasuredWidth();
347 | int measuredHeight = getMeasuredHeight();
348 | Bitmap layerBitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888);
349 | Canvas canvas = new Canvas(layerBitmap);
350 | canvas.clipRect(0, 0, layerBitmap.getWidth(), layerBitmap.getHeight());
351 | drawBitmap(canvas);
352 | canvas.setBitmap(null);
353 |
354 | int width = cropRect.width();
355 | int height = cropRect.height();
356 | final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
357 | canvas = new Canvas(bitmap);
358 | Rect src = new Rect();
359 | src.set(cropRect);
360 | Rect dst = new Rect();
361 | dst.set(0, 0, bitmap.getWidth(), bitmap.getHeight());
362 | Paint paint = new Paint();
363 | // 抗锯齿
364 | paint.setAntiAlias(true);
365 | paint.setFilterBitmap(true);
366 | canvas.drawBitmap(layerBitmap, src, dst, paint);
367 | canvas.setBitmap(null);
368 | layerBitmap.recycle();
369 |
370 | post(new Runnable() {
371 | @Override
372 | public void run() {
373 | callback.onImageCrop(bitmap);
374 | }
375 | });
376 | } catch (Exception e) {
377 | e.printStackTrace();
378 | post(new Runnable() {
379 | @Override
380 | public void run() {
381 | Toast.makeText(getContext(), "裁剪异常,请重试", Toast.LENGTH_SHORT).show();
382 | }
383 | });
384 | }
385 | }
386 | });
387 | }
388 |
389 |
390 | /* ------------------------------------------------ 绘制部分 ------------------------------------------------ */
391 | @Override
392 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
393 | super.onSizeChanged(w, h, oldw, oldh);
394 | viewRect.set(0, 0, w, h);
395 | }
396 |
397 | @Override
398 | protected void onDraw(Canvas canvas) {
399 | super.onDraw(canvas);
400 | drawCropBackground(canvas);
401 | drawBitmap(canvas);
402 | drawCropMask(canvas);
403 | }
404 |
405 | private void drawBitmap(Canvas canvas) {
406 | if (src == null) return;
407 | canvas.save();
408 | canvas.rotate(currentDegrees, cropRect.centerX(), cropRect.centerY());
409 | canvas.drawBitmap(src, bitmapRect, visibleRect, visiblePaint);
410 | canvas.restore();
411 | }
412 |
413 | private void drawCropBackground(Canvas canvas) {
414 | if (src == null) return;
415 | canvas.drawRect(cropRect, cropBackgroundPaint);
416 | }
417 |
418 | private void drawCropMask(Canvas canvas) {
419 | if (src == null) return;
420 | canvas.drawPath(cropPath, cropAreaPaint);
421 | }
422 |
423 | /* ------------------------------------------------ 手势操作部分 ------------------------------------------------ */
424 |
425 |
426 | /**
427 | * 旋转角度
428 | */
429 | public enum Degrees {
430 | DEGREES_0,
431 | DEGREES_90,
432 | DEGREES_180,
433 | DEGREES_270,
434 | DEGREES_360,
435 | }
436 |
437 | private enum Status {
438 | /**
439 | * 无状态 - 不可操作
440 | */
441 | IDLE,
442 | /**
443 | * 单指操作
444 | */
445 | SINGLE_POINT,
446 | /**
447 | * 两指操作
448 | */
449 | DOUBLE_POINT
450 | }
451 |
452 | private Status status = Status.IDLE;
453 | private final PointF lastTouch0 = new PointF();
454 | private final PointF lastTouch1 = new PointF();
455 | private VelocityTracker velocityTracker; // 惯性事件
456 |
457 | /**
458 | * @return 初始化velocityTracker
459 | */
460 | private VelocityTracker getVelocityTracker() {
461 | if (velocityTracker == null) {
462 | velocityTracker = VelocityTracker.obtain();
463 | }
464 | return velocityTracker;
465 | }
466 |
467 | private void releaseVelocityTracker() {
468 | if (velocityTracker != null) {
469 | velocityTracker.clear();
470 | velocityTracker.recycle();
471 | velocityTracker = null;
472 | }
473 | }
474 |
475 | @SuppressLint("ClickableViewAccessibility")
476 | @Override
477 | public boolean onTouchEvent(MotionEvent event) {
478 | // 跟踪惯性事件
479 | getVelocityTracker().addMovement(event);
480 |
481 | switch (event.getAction() & MotionEvent.ACTION_MASK) {
482 | case MotionEvent.ACTION_DOWN:
483 | // 停止惯性滑动
484 | abortFling(false);
485 | // 停止矫正位置
486 | abortAdjusting();
487 | // 单指按下
488 | lastTouch0.set(event.getX(), event.getY());
489 | lastTouch1.set(-1f, -1f);
490 | status = Status.SINGLE_POINT;
491 | break;
492 | case MotionEvent.ACTION_POINTER_DOWN:
493 | // 多指按下
494 | if (status != Status.IDLE) // 两指放开之后本次操作无效,需要用户松开所有手指,再次两指操作
495 | if (event.getPointerCount() >= 2) {
496 | float x0 = event.getX(0);
497 | float y0 = event.getY(0);
498 |
499 | float x1 = event.getX(1);
500 | float y1 = event.getY(1);
501 |
502 | lastTouch0.set(x0, y0);
503 | lastTouch1.set(x1, y1);
504 |
505 | status = Status.DOUBLE_POINT;
506 | }
507 | break;
508 | case MotionEvent.ACTION_MOVE:
509 | switch (status) {
510 | case IDLE:
511 | // do nothing...
512 | break;
513 | // 手指移动 - 单指
514 | case SINGLE_POINT: {
515 | float x = event.getX();
516 | float y = event.getY();
517 | float dx = x - lastTouch0.x;
518 | float dy = y - lastTouch0.y;
519 | translateVisibleRect((int) dx, (int) dy, false);
520 |
521 | // 记得记录本次记录,否则下次不会动了
522 | lastTouch0.set(x, y);
523 | }
524 | break;
525 | // 手指移动 - 两指
526 | case DOUBLE_POINT: {
527 | float x0 = event.getX(0);
528 | float y0 = event.getY(0);
529 |
530 | float x1 = event.getX(1);
531 | float y1 = event.getY(1);
532 |
533 | // 下面是上次的两指位置
534 | float _x0 = lastTouch0.x;
535 | float _y0 = lastTouch0.y;
536 |
537 | float _x1 = lastTouch1.x;
538 | float _y1 = lastTouch1.y;
539 |
540 | // 两指的位置可能: 1,左边的手指先按下 2,右边的手指先按下 - 这里使用两指中心点换算
541 | float cx = x0 < x1 ? ((x1 - x0) / 2f + x0) : ((x0 - x1) / 2f + x1);
542 | float cy = y0 < y1 ? ((y1 - y0) / 2f + y0) : ((y0 - y1) / 2f + y1);
543 |
544 | float _cx = _x0 < _x1 ? ((_x1 - _x0) / 2f + _x0) : ((_x0 - _x1) / 2f + _x1);
545 | float _cy = _y0 < _y1 ? ((_y1 - _y0) / 2f + _y0) : ((_y0 - _y1) / 2f + _y1);
546 |
547 | // 计算两指平移量
548 | float dx = cx - _cx;
549 | float dy = cy - _cy;
550 |
551 | // 两指间距 - 肯定得是正数
552 | float lx = x0 < x1 ? (x1 - x0) : (x0 - x1);
553 | float ly = y0 < y1 ? (y1 - y0) : (y0 - y1);
554 | float length = (float) Math.sqrt(Math.pow(lx, 2) + Math.pow(ly, 2)); // 勾股定理求两点距离
555 |
556 | // 上次两指间距 - 肯定得是正数
557 | float _lx = _x0 < _x1 ? (_x1 - _x0) : (_x0 - _x1);
558 | float _ly = _y0 < _y1 ? (_y1 - _y0) : (_y0 - _y1);
559 | float _length = (float) Math.sqrt(Math.pow(_lx, 2) + Math.pow(_ly, 2));
560 |
561 | // 缩放值
562 | float scale = length / _length;
563 |
564 | // 先缩放 后平移
565 | scaleAndTranslateVisibleRect(scale, dx, dy);
566 |
567 | // 记得记录本次记录,否则下次不会动了
568 | lastTouch0.set(x0, y0);
569 | lastTouch1.set(x1, y1);
570 | }
571 | break;
572 | }
573 | break;
574 | case MotionEvent.ACTION_POINTER_UP:
575 | // 手指松开 - 可能还有手指在屏幕上
576 | switch (status) {
577 | case IDLE:
578 | case SINGLE_POINT:
579 | status = Status.IDLE;
580 | break;
581 | case DOUBLE_POINT:
582 | // 之前是两指状态,然后放开一根手指,本次操作结束
583 | float x = event.getX();
584 | float y = event.getY();
585 | lastTouch0.set(x, y);
586 | lastTouch1.set(-1f, -1f);
587 | status = Status.IDLE;
588 | break;
589 | }
590 | break;
591 | case MotionEvent.ACTION_UP:
592 | // 尺寸偏小
593 | if (tooSmall()) {
594 | adjustPosition();// 开始矫正位置
595 | }
596 | // 处理惯性
597 | else if (!computeVelocity()) {
598 | adjustPosition(); // 开始矫正位置
599 | }
600 | // 释放惯性实践
601 | releaseVelocityTracker();
602 | break;
603 | }
604 | return true;
605 | }
606 |
607 | /**
608 | * 处理惯性
609 | */
610 | private boolean computeVelocity() {
611 | VelocityTracker vt = getVelocityTracker();
612 | vt.computeCurrentVelocity(1000, maxFlingVelocity);
613 | float xVelocity = vt.getXVelocity();
614 | float yVelocity = vt.getYVelocity();
615 |
616 | if (Math.abs(xVelocity) > Math.abs(yVelocity)) {
617 | if (xVelocity > 0) {
618 | // 惯性往左
619 | xVelocity = canScrollRight() ? xVelocity : 0;
620 | } else {
621 | // 惯性往右
622 | xVelocity = canScrollLeft() ? xVelocity : 0;
623 | }
624 | } else {
625 | if (yVelocity > 0) {
626 | // 惯性往下
627 | yVelocity = canScrollBottom() ? yVelocity : 0;
628 | } else {
629 | // 惯性往上
630 | yVelocity = canScrollTop() ? yVelocity : 0;
631 | }
632 | }
633 |
634 | if (Math.abs(xVelocity) > minFlingVelocity && Math.abs(yVelocity) > minFlingVelocity) {
635 | int startX = (int) lastTouch0.x;
636 | int startY = (int) lastTouch0.y;
637 | int velocityX = (int) xVelocity;
638 | int velocityY = (int) yVelocity;
639 | int minX = Integer.MIN_VALUE;
640 | int maxX = Integer.MAX_VALUE;
641 | int minY = Integer.MIN_VALUE;
642 | int maxY = Integer.MAX_VALUE;
643 | scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
644 | fling = true;
645 | invalidate();
646 | return true;
647 | }
648 | return false;
649 | }
650 |
651 | @Override
652 | public void computeScroll() {
653 | super.computeScroll();
654 | if (scroller.computeScrollOffset()) {
655 | // 模拟单指滑动
656 | int x = scroller.getCurrX();
657 | int y = scroller.getCurrY();
658 |
659 | float dx = x - lastTouch0.x;
660 | float dy = y - lastTouch0.y;
661 | translateVisibleRect((int) dx, (int) dy, true);
662 |
663 | // 记得记录本次记录,否则下次不会动了
664 | lastTouch0.set(x, y);
665 | } else abortFling(true);
666 | }
667 |
668 | /**
669 | * 中止惯性滑动
670 | *
671 | * @param adjust 是否需要矫正位置
672 | */
673 | private void abortFling(boolean adjust) {
674 | if (fling) {
675 | fling = false;
676 | scroller.abortAnimation();
677 | if (adjust) adjustPosition();
678 | }
679 | }
680 |
681 | private final Rect tempRect = new Rect();
682 | private final Matrix tempMatrix = new Matrix();
683 | private final RectF tempRectF = new RectF();
684 | private final Rect transformRect = new Rect();
685 | private final Rect restoreRect = new Rect();
686 | /* -- 动画相关 --*/
687 | private final Rect adjustRect = new Rect();
688 | private int adjustX = 0, adjustY = 0;
689 | private float adjustScaleFrom = 1.0f, adjustScaleTo = 1.0f;
690 | private ValueAnimator adjustAnim;
691 | private final Rect rotateCropRect = new Rect();
692 | private float degreesFrom = 0.0f, degreesTo = 0.0f;
693 | private ValueAnimator rotateAnim;
694 |
695 | /**
696 | * 更新可见图片的位置
697 | *
698 | * @param dx x偏移量
699 | * @param dy y偏移量
700 | * @param strict 严格模式 如果为true则还原边界值,不做刷新
701 | * @return true表示未到边界值还能移动 false表示已经到了边界
702 | */
703 | private void translateVisibleRect(int dx, int dy, boolean strict) {
704 | Rect transformVisibleRect = getTransformVisibleRect();
705 | tempRect.set(transformVisibleRect);
706 |
707 | tempRect.left += dx;
708 | tempRect.right += dx;
709 | tempRect.top += dy;
710 | tempRect.bottom += dy;
711 |
712 | // 图片往左移动 - 判断右边界
713 | if (dx < 0) {
714 | if (tempRect.right <= cropRect.right) {
715 | if (strict) {
716 | tempRect.right = cropRect.right;
717 | tempRect.left = tempRect.right - transformVisibleRect.width();
718 | }
719 | }
720 | }
721 | // 图片往右移动 - 判断左边界
722 | else {
723 | if (tempRect.left >= cropRect.left) {
724 | if (strict) {
725 | tempRect.left = cropRect.left;
726 | tempRect.right = tempRect.left + transformVisibleRect.width();
727 | }
728 | }
729 | }
730 |
731 | // 图片往上移动 - 判断下边界
732 | if (dy < 0) {
733 | if (tempRect.bottom <= cropRect.bottom) {
734 | if (strict) {
735 | tempRect.bottom = cropRect.bottom;
736 | tempRect.top = tempRect.bottom - transformVisibleRect.height();
737 | }
738 | }
739 | }
740 | // 图片往下移动 - 判断上边界
741 | else {
742 | if (tempRect.top >= cropRect.top) {
743 | if (strict) {
744 | tempRect.top = cropRect.top;
745 | tempRect.bottom = tempRect.top + transformVisibleRect.height();
746 | }
747 | }
748 | }
749 |
750 | Rect restoreVisibleRect = restoreVisibleRect(tempRect);
751 | visibleRect.set(restoreVisibleRect);
752 |
753 | // 坐标改变,记得重绘视图
754 | invalidate();
755 | }
756 |
757 | /**
758 | * 缩放 + 平移
759 | *
760 | * 在这需要限制最大尺寸!
761 | *
762 | * @param scale 缩放比例
763 | */
764 | private void scaleAndTranslateVisibleRect(float scale, float dx, float dy) {
765 | // 限制最大尺寸
766 | float maxScale = calculateMaxScale();
767 | scale = Math.min(maxScale, scale);
768 |
769 | Rect transformVisibleRect = getTransformVisibleRect();
770 | tempRect.set(transformVisibleRect);
771 | // 缩放中心始终为裁剪区域的中心
772 | int cx = cropRect.centerX();
773 | int cy = cropRect.centerY();
774 |
775 | tempMatrix.reset();
776 | tempMatrix.setScale(scale, scale, cx, cy);
777 | tempMatrix.postTranslate(dx, dy);
778 | tempRectF.set(tempRect);
779 | tempMatrix.mapRect(tempRectF);
780 | tempRect.set(
781 | (int) Math.ceil(tempRectF.left),
782 | (int) Math.ceil(tempRectF.top),
783 | (int) Math.ceil(tempRectF.right),
784 | (int) Math.ceil(tempRectF.bottom));
785 |
786 | Rect restoreVisibleRect = restoreVisibleRect(tempRect);
787 | visibleRect.set(restoreVisibleRect);
788 |
789 | // 坐标改变记得重绘视图
790 | invalidate();
791 | }
792 |
793 | /**
794 | * @return 最大缩放比例
795 | */
796 | private float calculateMaxScale() {
797 | Rect transformVisibleRect = getTransformVisibleRect();
798 |
799 | float scaleW = maxVisibleWidth * 1f / transformVisibleRect.width();
800 | float scaleH = maxVisibleHeight * 1f / transformVisibleRect.height();
801 | return Math.max(scaleW, scaleH);
802 | }
803 |
804 | /**
805 | * @return true表示裁剪区域在可见范围内 false表示没有
806 | */
807 | private boolean cropInVisible() {
808 | Rect transformVisibleRect = getTransformVisibleRect();
809 | return transformVisibleRect.contains(cropRect);
810 | }
811 |
812 | /**
813 | * @return 可以往右滑
814 | */
815 | private boolean canScrollRight() {
816 | Rect transformVisibleRect = getTransformVisibleRect();
817 | return transformVisibleRect.left < cropRect.left;
818 | }
819 |
820 | /**
821 | * @return 可以往左滑
822 | */
823 | private boolean canScrollLeft() {
824 | Rect transformVisibleRect = getTransformVisibleRect();
825 | return cropRect.right < transformVisibleRect.right;
826 | }
827 |
828 | /**
829 | * @return 可以往上滑
830 | */
831 | private boolean canScrollTop() {
832 | Rect transformVisibleRect = getTransformVisibleRect();
833 | return cropRect.bottom < transformVisibleRect.bottom;
834 | }
835 |
836 | /**
837 | * @return 可以往下滑
838 | */
839 | private boolean canScrollBottom() {
840 | Rect transformVisibleRect = getTransformVisibleRect();
841 | return transformVisibleRect.top < cropRect.top;
842 | }
843 |
844 | /**
845 | * 视图可见尺寸比裁剪尺寸还小
846 | *
847 | * @return true表示尺寸过小了
848 | */
849 | private boolean tooSmall() {
850 | Rect transformVisibleRect = getTransformVisibleRect();
851 | return transformVisibleRect.width() < cropRect.width()
852 | || transformVisibleRect.height() < cropRect.height();
853 | }
854 |
855 | /**
856 | * @return 经过矩阵旋转,跟屏幕显示区域一致的visibleRect对象
857 | */
858 | private Rect getTransformVisibleRect() {
859 | tempMatrix.reset();
860 | transformRect.set(visibleRect);
861 | tempMatrix.setRotate(currentDegrees, cropRect.centerX(), cropRect.centerY());
862 | tempRectF.set(transformRect);
863 | tempMatrix.mapRect(tempRectF);
864 | transformRect.set(
865 | (int) Math.ceil(tempRectF.left),
866 | (int) Math.ceil(tempRectF.top),
867 | (int) Math.ceil(tempRectF.right),
868 | (int) Math.ceil(tempRectF.bottom));
869 | return transformRect;
870 | }
871 |
872 | /**
873 | * @param transformRect 跟屏幕显示区域一致的visibleRect
874 | * @return 未经过旋转的visibleRect
875 | */
876 | private Rect restoreVisibleRect(Rect transformRect) {
877 | tempMatrix.reset();
878 | restoreRect.set(transformRect);
879 | tempMatrix.setRotate(-currentDegrees, cropRect.centerX(), cropRect.centerY());
880 | tempRectF.set(restoreRect);
881 | tempMatrix.mapRect(tempRectF);
882 | restoreRect.set(
883 | (int) Math.ceil(tempRectF.left),
884 | (int) Math.ceil(tempRectF.top),
885 | (int) Math.ceil(tempRectF.right),
886 | (int) Math.ceil(tempRectF.bottom));
887 | return restoreRect;
888 | }
889 |
890 | /**
891 | * 矫正位置,处于裁剪区域有效范围内
892 | *
893 | * 先全部按照屏幕空间计算,然后将平移值进行旋转换算
894 | *
895 | * ps:尺寸过小的矫正在这里进行,尺寸最大边界的控制在{@link CropPhotoView#scaleAndTranslateVisibleRect(float, float, float)}中
896 | */
897 | private void adjustPosition() {
898 | // fixme:角度为90或270度时,计算有误!
899 | // if (cropInVisible()) return;
900 |
901 | // 先预测一下,合理范围,无需矫正
902 | adjustScaleFrom = 1.0f;
903 | adjustScaleTo = 1.0f;
904 |
905 | Rect transformVisibleRect = getTransformVisibleRect();
906 | // 矫正高度
907 | if (transformVisibleRect.height() < cropRect.height()) {
908 | // 尺寸过小了,得放大
909 | adjustScaleTo = cropRect.height() * 1f / transformVisibleRect.height();
910 | }
911 | // 矫正宽度
912 | if (transformVisibleRect.width() < cropRect.width()) {
913 | // 尺寸过小了,得放大
914 | float _adjustScaleTo = cropRect.width() * 1f / transformVisibleRect.width();
915 | // 取一个最大的
916 | adjustScaleTo = Math.max(adjustScaleTo, _adjustScaleTo);
917 | }
918 |
919 | // 矫正缩放 - 矫正旋转
920 | tempRectF.set(transformVisibleRect);
921 | tempMatrix.reset();
922 | tempMatrix.setScale(adjustScaleTo, adjustScaleTo, cropRect.centerX(), cropRect.centerY());
923 | tempMatrix.mapRect(tempRectF);
924 | tempRect.set(
925 | (int) Math.ceil(tempRectF.left),
926 | (int) Math.ceil(tempRectF.top),
927 | (int) Math.ceil(tempRectF.right),
928 | (int) Math.ceil(tempRectF.bottom));
929 |
930 | // 矫正偏移值还原
931 | adjustX = 0;
932 | adjustY = 0;
933 |
934 | // 左边界需要矫正
935 | if (tempRect.left > cropRect.left) {
936 | adjustX = -(tempRect.left - cropRect.left);
937 | }
938 | // 右边界需要矫正
939 | if (tempRect.right < cropRect.right) {
940 | adjustX = (cropRect.right - tempRect.right);
941 | }
942 | // 上边界需要矫正
943 | if (tempRect.top > cropRect.top) {
944 | adjustY = -(tempRect.top - cropRect.top);
945 | }
946 | // 下边界需要矫正
947 | if (tempRect.bottom < cropRect.bottom) {
948 | adjustY = (cropRect.bottom - tempRect.bottom);
949 | }
950 |
951 | // 矫正值计算完毕,保存动画基准区域
952 | adjustRect.set(transformVisibleRect);
953 |
954 | // 动画矫正位置
955 | adjustAnim = ValueAnimator.ofFloat(0.0f, 1.0f);
956 | adjustAnim.setDuration(200);
957 | adjustAnim.setInterpolator(new AccelerateDecelerateInterpolator());
958 | adjustAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
959 | @Override
960 | public void onAnimationUpdate(ValueAnimator animation) {
961 | float progress = (float) animation.getAnimatedValue();
962 | onAdjusting(progress);
963 | }
964 | });
965 | adjustAnim.addListener(new AnimatorListenerAdapter() {
966 | @Override
967 | public void onAnimationEnd(Animator animation) {
968 | super.onAnimationEnd(animation);
969 | onAdjusting(1.0f);
970 | }
971 | });
972 | adjustAnim.start();
973 | }
974 |
975 | /**
976 | * 矫正中...
977 | *
978 | * @param progress 矫正进度 [0.0f~1.0f]
979 | */
980 | private void onAdjusting(float progress) {
981 | float scale = adjustScaleFrom + progress * (adjustScaleTo - adjustScaleFrom);
982 | float transX = progress * adjustX;
983 | float transY = progress * adjustY;
984 | // 拿基准区域做举证变换
985 | tempRectF.set(adjustRect);
986 | // 矩阵变换顺序 - 先缩放 再平移
987 | tempMatrix.reset();
988 | tempMatrix.setScale(scale, scale, cropRect.centerX(), cropRect.centerY());
989 | tempMatrix.postTranslate(transX, transY);
990 | tempMatrix.mapRect(tempRectF);
991 | // 变换好的值,直接给可见区域,因为adjustRect基准区域未变,所以这里可以直接赋值
992 | tempRect.set(
993 | (int) Math.ceil(tempRectF.left),
994 | (int) Math.ceil(tempRectF.top),
995 | (int) Math.ceil(tempRectF.right),
996 | (int) Math.ceil(tempRectF.bottom));
997 |
998 | Rect restoreVisibleRect = restoreVisibleRect(tempRect);
999 | visibleRect.set(restoreVisibleRect);
1000 |
1001 | invalidate();
1002 | }
1003 |
1004 | /**
1005 | * 中止矫正
1006 | */
1007 | private void abortAdjusting() {
1008 | if (adjustAnim != null) {
1009 | adjustAnim.cancel();
1010 | adjustAnim.removeAllListeners();
1011 | adjustAnim.removeAllUpdateListeners();
1012 | adjustAnim = null;
1013 | }
1014 | }
1015 |
1016 | private void _rotate(Degrees degrees) {
1017 | // 完成老动画
1018 | if (rotateAnim != null && rotateAnim.isRunning()) {
1019 | rotateAnim.cancel();
1020 | rotateAnim.removeAllUpdateListeners();
1021 | rotateAnim.removeAllListeners();
1022 | }
1023 |
1024 | float postDegrees = 0.0f;
1025 | switch (degrees) {
1026 | case DEGREES_0:
1027 | postDegrees = 0.0f;
1028 | break;
1029 | case DEGREES_90:
1030 | postDegrees = 90.0f;
1031 | break;
1032 | case DEGREES_180:
1033 | postDegrees = 180.0f;
1034 | break;
1035 | case DEGREES_270:
1036 | postDegrees = 270.0f;
1037 | break;
1038 | case DEGREES_360:
1039 | postDegrees = 360.0f;
1040 | break;
1041 | }
1042 | // 计算旋转范围
1043 | if (currentDegrees == 360 && postDegrees == 0f) currentDegrees = 0f; // 此情况不用旋转
1044 | degreesFrom = currentDegrees;
1045 | degreesTo = postDegrees;
1046 | currentDegreesEnum = degrees;
1047 |
1048 | rotateAnim = ValueAnimator.ofFloat(degreesFrom, degreesTo);
1049 | rotateAnim.setDuration(200);
1050 | rotateAnim.setInterpolator(new AccelerateInterpolator());
1051 | rotateAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1052 | @Override
1053 | public void onAnimationUpdate(ValueAnimator animation) {
1054 | // 更新当前角度,draw里面有用
1055 | currentDegrees = (float) animation.getAnimatedValue();
1056 | // 记得重绘视图
1057 | invalidate();
1058 | }
1059 | });
1060 | rotateAnim.addListener(new AnimatorListenerAdapter() {
1061 | @Override
1062 | public void onAnimationEnd(Animator animation) {
1063 | super.onAnimationEnd(animation);
1064 | // 裁剪区域不是正方形(cropRatio != 1.0)的旋转,需要再矫正位置
1065 | adjustPosition();
1066 | }
1067 | });
1068 | rotateAnim.start();
1069 | }
1070 |
1071 | @Override
1072 | protected void onDetachedFromWindow() {
1073 | super.onDetachedFromWindow();
1074 | releaseVelocityTracker();
1075 | }
1076 |
1077 |
1078 | /**
1079 | * 图片裁剪完成回调
1080 | */
1081 | public interface OnImageCropCallback {
1082 | void onImageCrop(Bitmap bitmap);
1083 | }
1084 | }
1085 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/selector_green_item_click_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/selector_item_click_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/shape_round_rect_green.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/view_progress_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/activity_crop_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
18 |
19 |
30 |
31 |
35 |
36 |
49 |
50 |
54 |
55 |
69 |
70 |
71 |
77 |
78 |
85 |
86 |
91 |
92 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/library/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #00000000
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
--------------------------------------------------------------------------------
/library/src/test/java/org/liaohailong/library/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package org.liaohailong.library;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':library'
2 | include ':app'
3 | rootProject.name = "CropImageView"
--------------------------------------------------------------------------------