From 59046caf0d32e4bb2e93a52bfb42d5e39ee4301b Mon Sep 17 00:00:00 2001 From: eggmanQQQ2 <3671373519@qq.com> Date: Mon, 7 Jul 2025 10:49:39 +0800 Subject: [PATCH] feat : libs --- libs/lib_core/.gitignore | 1 + libs/lib_core/build.gradle | 21 + libs/lib_core/consumer-rules.pro | 0 libs/lib_core/proguard-rules.pro | 21 + libs/lib_core/src/main/AndroidManifest.xml | 4 + .../com/example/lib_core/LifecycleCleared.kt | 62 + .../component/SuperBottomSheetDialog.kt | 54 + libs/lib_crop/.gitignore | 3 + libs/lib_crop/build.gradle | 31 + libs/lib_crop/src/main/AndroidManifest.xml | 1 + .../main/java/com/soundcloud/crop/Crop.java | 266 ++++ .../soundcloud/crop/CropImageActivity.java | 441 ++++++ .../com/soundcloud/crop/CropImageView.java | 195 +++ .../java/com/soundcloud/crop/CropUtil.java | 227 +++ .../com/soundcloud/crop/HighlightView.java | 395 +++++ .../soundcloud/crop/ImageViewTouchBase.java | 400 +++++ .../main/java/com/soundcloud/crop/Log.java | 15 + .../soundcloud/crop/MonitoredActivity.java | 86 ++ .../com/soundcloud/crop/RotateBitmap.java | 96 ++ .../java/com/soundcloud/crop/UriUtil.java | 48 + .../res/drawable-hdpi/crop__divider.9.png | Bin 0 -> 112 bytes .../res/drawable-hdpi/crop__ic_cancel.webp | Bin 0 -> 240 bytes .../main/res/drawable-hdpi/crop__ic_done.webp | Bin 0 -> 230 bytes .../res/drawable-mdpi/crop__divider.9.png | Bin 0 -> 112 bytes .../res/drawable-mdpi/crop__ic_cancel.webp | Bin 0 -> 196 bytes .../main/res/drawable-mdpi/crop__ic_done.webp | Bin 0 -> 204 bytes .../crop__selectable_background.xml | 6 + .../res/drawable-xhdpi/crop__divider.9.png | Bin 0 -> 119 bytes .../res/drawable-xhdpi/crop__ic_cancel.webp | Bin 0 -> 270 bytes .../res/drawable-xhdpi/crop__ic_done.webp | Bin 0 -> 304 bytes .../main/res/drawable-xhdpi/crop__tile.webp | Bin 0 -> 88 bytes .../drawable/crop__selectable_background.xml | 20 + .../src/main/res/drawable/crop__texture.xml | 5 + .../main/res/layout/crop__activity_crop.xml | 19 + .../res/layout/crop__layout_done_cancel.xml | 16 + .../src/main/res/values-ar/strings.xml | 8 + .../src/main/res/values-v21/colors.xml | 5 + .../src/main/res/values-zh-rTW/strings.xml | 8 + libs/lib_crop/src/main/res/values/attrs.xml | 16 + libs/lib_crop/src/main/res/values/colors.xml | 8 + libs/lib_crop/src/main/res/values/dimens.xml | 5 + libs/lib_crop/src/main/res/values/strings.xml | 10 + libs/lib_crop/src/main/res/values/styles.xml | 44 + libs/lib_encipher/.gitignore | 1 + libs/lib_encipher/build.gradle | 15 + libs/lib_encipher/consumer-rules.pro | 0 .../libs/arm64-v8a/libencipher.so | Bin 0 -> 131600 bytes .../libs/armeabi-v7a/libencipher.so | Bin 0 -> 136232 bytes libs/lib_encipher/libs/x86/libencipher.so | Bin 0 -> 130580 bytes libs/lib_encipher/libs/x86_64/libencipher.so | Bin 0 -> 122336 bytes libs/lib_encipher/proguard-rules.pro | 21 + .../lib_encipher/src/main/AndroidManifest.xml | 4 + .../java/com/secure/encipher/EncipherLib.kt | 58 + libs/lib_standard.gradle | 40 + libs/lib_utils/.gitignore | 1 + libs/lib_utils/build.gradle | 10 + libs/lib_utils/consumer-rules.pro | 0 libs/lib_utils/proguard-rules.pro | 21 + libs/lib_utils/src/main/AndroidManifest.xml | 4 + .../java/com/example/lib_utils/AppUtils.java | 403 +++++ .../com/example/lib_utils/FileUtils2.java | 1294 +++++++++++++++++ .../java/com/example/lib_utils/ICleared.kt | 14 + .../java/com/example/lib_utils/PathUtils.kt | 521 +++++++ .../java/com/example/lib_utils/ServiceTime.kt | 25 + .../lib_utils/ShapeViewOutlineProvider.kt | 42 + .../lib_utils/SolveEditTextScrollClash.kt | 39 + .../com/example/lib_utils/StringUtils2.kt | 68 + .../com/example/lib_utils/TelephonyUtils.kt | 148 ++ .../java/com/example/lib_utils/UiUtils.kt | 79 + .../com/example/lib_utils/ktx/ContextKtx.kt | 70 + .../com/example/lib_utils/ktx/EditTextKtx.kt | 105 ++ .../example/lib_utils/ktx/ImageToAlbumKtx.kt | 250 ++++ .../com/example/lib_utils/ktx/ResourcesKtx.kt | 216 +++ .../java/com/example/lib_utils/ktx/UiKtx.kt | 51 + .../java/com/example/lib_utils/ktx/ViewKtx.kt | 192 +++ .../lib_utils/log/AndroidLogPrinter.kt | 13 + .../java/com/example/lib_utils/log/ILog.kt | 61 + .../com/example/lib_utils/log/LogPrinter.kt | 14 + .../java/com/example/lib_utils/log/LogUtil.kt | 106 ++ .../lib_utils/spannable/IconTextSpan.java | 156 ++ .../spannable/RoundBackgroundColorSpan.kt | 29 + .../spannable/SpannableTextBuilder.kt | 517 +++++++ .../lib_utils/spannable/VerticalImageSpan.kt | 66 + 83 files changed, 7191 insertions(+) create mode 100644 libs/lib_core/.gitignore create mode 100644 libs/lib_core/build.gradle create mode 100644 libs/lib_core/consumer-rules.pro create mode 100644 libs/lib_core/proguard-rules.pro create mode 100644 libs/lib_core/src/main/AndroidManifest.xml create mode 100644 libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt create mode 100644 libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt create mode 100644 libs/lib_crop/.gitignore create mode 100644 libs/lib_crop/build.gradle create mode 100644 libs/lib_crop/src/main/AndroidManifest.xml create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java create mode 100644 libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java create mode 100644 libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png create mode 100644 libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp create mode 100644 libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp create mode 100644 libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png create mode 100644 libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp create mode 100644 libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp create mode 100644 libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml create mode 100644 libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png create mode 100644 libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp create mode 100644 libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp create mode 100644 libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp create mode 100644 libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml create mode 100644 libs/lib_crop/src/main/res/drawable/crop__texture.xml create mode 100644 libs/lib_crop/src/main/res/layout/crop__activity_crop.xml create mode 100644 libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml create mode 100644 libs/lib_crop/src/main/res/values-ar/strings.xml create mode 100644 libs/lib_crop/src/main/res/values-v21/colors.xml create mode 100644 libs/lib_crop/src/main/res/values-zh-rTW/strings.xml create mode 100644 libs/lib_crop/src/main/res/values/attrs.xml create mode 100644 libs/lib_crop/src/main/res/values/colors.xml create mode 100644 libs/lib_crop/src/main/res/values/dimens.xml create mode 100644 libs/lib_crop/src/main/res/values/strings.xml create mode 100644 libs/lib_crop/src/main/res/values/styles.xml create mode 100644 libs/lib_encipher/.gitignore create mode 100644 libs/lib_encipher/build.gradle create mode 100644 libs/lib_encipher/consumer-rules.pro create mode 100755 libs/lib_encipher/libs/arm64-v8a/libencipher.so create mode 100755 libs/lib_encipher/libs/armeabi-v7a/libencipher.so create mode 100755 libs/lib_encipher/libs/x86/libencipher.so create mode 100755 libs/lib_encipher/libs/x86_64/libencipher.so create mode 100644 libs/lib_encipher/proguard-rules.pro create mode 100644 libs/lib_encipher/src/main/AndroidManifest.xml create mode 100644 libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt create mode 100644 libs/lib_standard.gradle create mode 100644 libs/lib_utils/.gitignore create mode 100644 libs/lib_utils/build.gradle create mode 100644 libs/lib_utils/consumer-rules.pro create mode 100644 libs/lib_utils/proguard-rules.pro create mode 100644 libs/lib_utils/src/main/AndroidManifest.xml create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt create mode 100644 libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt diff --git a/libs/lib_core/.gitignore b/libs/lib_core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lib_core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/lib_core/build.gradle b/libs/lib_core/build.gradle new file mode 100644 index 0000000..f7b67d6 --- /dev/null +++ b/libs/lib_core/build.gradle @@ -0,0 +1,21 @@ +apply from : "../lib_standard.gradle" + +android { + namespace 'com.example.lib_core' +} + +dependencies { + // 工具集 + api project(path: ":libs:lib_utils") + + api 'androidx.constraintlayout:constraintlayout:2.1.4' + api 'com.google.android.material:material:1.6.1' + api 'androidx.recyclerview:recyclerview:1.3.0' + api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2' + api 'androidx.lifecycle:lifecycle-extensions:2.2.0' + + +// api "com.alibaba:fastjson:1.2.41" + api 'com.alibaba.fastjson2:fastjson2:2.0.57.android5' // 检查最新版本 + +} \ No newline at end of file diff --git a/libs/lib_core/consumer-rules.pro b/libs/lib_core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_core/proguard-rules.pro b/libs/lib_core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/libs/lib_core/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/libs/lib_core/src/main/AndroidManifest.xml b/libs/lib_core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/libs/lib_core/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt b/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt new file mode 100644 index 0000000..c9a5391 --- /dev/null +++ b/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt @@ -0,0 +1,62 @@ +import androidx.lifecycle.* + +/** + * Created by Max on 2023/10/24 15:11 + * Desc:跟随目标生命周期销毁 + **/ +interface LifecycleCleared : LifecycleEventObserver { + + /** + * 是否启用 + */ + fun isEnabledLifecycleClear(): Boolean { + return true + } + + /** + * 获取监听的目标生命周期 + */ + abstract fun getTargetLifecycle(): Lifecycle? + + /** + * 目标生命周期已销毁:执行清除资源操作 + */ + abstract fun onTargetCleared() + + /** + * 获取要执行清理的事件 + */ + fun getClearEvent(): Lifecycle.Event? { + return Lifecycle.Event.ON_DESTROY + } + + /** + * 绑定生命周期 + */ + fun bindLifecycleClear() { + if (!isEnabledLifecycleClear()) { + return + } + getTargetLifecycle()?.addObserver(this) + } + + /** + * 取消绑定生命周期(如果实现类是自己主动销毁的,需要主动调下本方法) + */ + fun unBindLifecycleClear() { + if (!isEnabledLifecycleClear()) { + return + } + getTargetLifecycle()?.removeObserver(this) + } + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (!isEnabledLifecycleClear()) { + return + } + if (getClearEvent() == event) { + unBindLifecycleClear() + onTargetCleared() + } + } +} \ No newline at end of file diff --git a/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt b/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt new file mode 100644 index 0000000..14d3247 --- /dev/null +++ b/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt @@ -0,0 +1,54 @@ +package com.example.lib_core.component + +import LifecycleCleared +import android.content.Context +import android.content.DialogInterface +import androidx.lifecycle.Lifecycle +import com.example.lib_utils.ktx.asLifecycle +import com.google.android.material.bottomsheet.BottomSheetDialog + + +/** + * Created by Max on 2023/10/24 15:11 + * Desc:BottomSheetDialog + */ +open class SuperBottomSheetDialog : BottomSheetDialog, LifecycleCleared { + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, theme: Int) : super(context, theme) { + init() + } + + constructor( + context: Context, + cancelable: Boolean, + cancelListener: DialogInterface.OnCancelListener? + ) : super(context, cancelable, cancelListener) { + init() + } + + protected open fun init() { + + } + + override fun getTargetLifecycle(): Lifecycle? { + return context.asLifecycle() + } + + override fun onTargetCleared() { + dismiss() + } + + override fun show() { + super.show() + bindLifecycleClear() + } + + override fun dismiss() { + super.dismiss() + unBindLifecycleClear() + } +} \ No newline at end of file diff --git a/libs/lib_crop/.gitignore b/libs/lib_crop/.gitignore new file mode 100644 index 0000000..36af493 --- /dev/null +++ b/libs/lib_crop/.gitignore @@ -0,0 +1,3 @@ +/build +*.iml +*.DS_Store \ No newline at end of file diff --git a/libs/lib_crop/build.gradle b/libs/lib_crop/build.gradle new file mode 100644 index 0000000..fe71656 --- /dev/null +++ b/libs/lib_crop/build.gradle @@ -0,0 +1,31 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion COMPILE_SDK_VERSION.toInteger() + + defaultConfig { + minSdkVersion MIN_SDK_VERSION.toInteger() + targetSdkVersion TARGET_SDK_VERSION.toInteger() + } + + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildToolsVersion = '30.0.3' +} + +dependencies { + api 'androidx.annotation:annotation:1.6.0' + api 'androidx.legacy:legacy-support-v4:1.0.0' + implementation "androidx.core:core-ktx:1.9.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" +} +repositories { + mavenCentral() +} + diff --git a/libs/lib_crop/src/main/AndroidManifest.xml b/libs/lib_crop/src/main/AndroidManifest.xml new file mode 100644 index 0000000..abc38ad --- /dev/null +++ b/libs/lib_crop/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java new file mode 100644 index 0000000..f3fcc2b --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java @@ -0,0 +1,266 @@ +package com.soundcloud.crop; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Fragment; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.widget.Toast; + +/** + * Builder for crop Intents and utils for handling result + */ +public class Crop { + + public static final int REQUEST_CROP = 6709; + public static final int REQUEST_PICK = 9162; + public static final int RESULT_ERROR = 404; + + interface Extra { + String ASPECT_X = "aspect_x"; + String ASPECT_Y = "aspect_y"; + String MAX_X = "max_x"; + String MAX_Y = "max_y"; + String AS_PNG = "as_png"; + String ERROR = "error"; + } + + private Intent cropIntent; + + /** + * Create a crop Intent builder with source and destination image Uris + * + * @param source Uri for image to crop + * @param destination Uri for saving the cropped image + */ + public static Crop of(Uri source, Uri destination) { + return new Crop(source, destination); + } + + private Crop(Uri source, Uri destination) { + cropIntent = new Intent(); + cropIntent.setData(source); + cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination); + } + + /** + * Set fixed aspect ratio for crop area + * + * @param x Aspect X + * @param y Aspect Y + */ + public Crop withAspect(int x, int y) { + cropIntent.putExtra(Extra.ASPECT_X, x); + cropIntent.putExtra(Extra.ASPECT_Y, y); + return this; + } + + /** + * Crop area with fixed 1:1 aspect ratio + */ + public Crop asSquare() { + cropIntent.putExtra(Extra.ASPECT_X, 1); + cropIntent.putExtra(Extra.ASPECT_Y, 1); + return this; + } + + /** + * Set maximum crop size + * + * @param width Max width + * @param height Max height + */ + public Crop withMaxSize(int width, int height) { + cropIntent.putExtra(Extra.MAX_X, width); + cropIntent.putExtra(Extra.MAX_Y, height); + return this; + } + + /** + * Set whether to save the result as a PNG or not. Helpful to preserve alpha. + * @param asPng whether to save the result as a PNG or not + */ + public Crop asPng(boolean asPng) { + cropIntent.putExtra(Extra.AS_PNG, asPng); + return this; + } + + /** + * Send the crop Intent from an Activity + * + * @param activity Activity to receive result + */ + public void start(Activity activity) { + start(activity, REQUEST_CROP); + } + + /** + * Send the crop Intent from an Activity with a custom request code + * + * @param activity Activity to receive result + * @param requestCode requestCode for result + */ + public void start(Activity activity, int requestCode) { + activity.startActivityForResult(getIntent(activity), requestCode); + } + + /** + * Send the crop Intent from a Fragment + * + * @param context Context + * @param fragment Fragment to receive result + */ + public void start(Context context, Fragment fragment) { + start(context, fragment, REQUEST_CROP); + } + + /** + * Send the crop Intent from a support library Fragment + * + * @param context Context + * @param fragment Fragment to receive result + */ + public void start(Context context, androidx.fragment.app.Fragment fragment) { + start(context, fragment, REQUEST_CROP); + } + + /** + * Send the crop Intent with a custom request code + * + * @param context Context + * @param fragment Fragment to receive result + * @param requestCode requestCode for result + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void start(Context context, Fragment fragment, int requestCode) { + fragment.startActivityForResult(getIntent(context), requestCode); + } + + /** + * Send the crop Intent with a custom request code + * + * @param context Context + * @param fragment Fragment to receive result + * @param requestCode requestCode for result + */ + public void start(Context context, androidx.fragment.app.Fragment fragment, int requestCode) { + fragment.startActivityForResult(getIntent(context), requestCode); + } + + /** + * Get Intent to start crop Activity + * + * @param context Context + * @return Intent for CropImageActivity + */ + public Intent getIntent(Context context) { + cropIntent.setClass(context, CropImageActivity.class); + return cropIntent; + } + + /** + * Retrieve URI for cropped image, as set in the Intent builder + * + * @param result Output Image URI + */ + public static Uri getOutput(Intent result) { + return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT); + } + + /** + * Retrieve error that caused crop to fail + * + * @param result Result Intent + * @return Throwable handled in CropImageActivity + */ + public static Throwable getError(Intent result) { + return (Throwable) result.getSerializableExtra(Extra.ERROR); + } + + /** + * Pick image from an Activity + * + * @param activity Activity to receive result + */ + public static void pickImage(Activity activity) { + pickImage(activity, REQUEST_PICK); + } + + /** + * Pick image from a Fragment + * + * @param context Context + * @param fragment Fragment to receive result + */ + public static void pickImage(Context context, Fragment fragment) { + pickImage(context, fragment, REQUEST_PICK); + } + + /** + * Pick image from a support library Fragment + * + * @param context Context + * @param fragment Fragment to receive result + */ + public static void pickImage(Context context, androidx.fragment.app.Fragment fragment) { + pickImage(context, fragment, REQUEST_PICK); + } + + /** + * Pick image from an Activity with a custom request code + * + * @param activity Activity to receive result + * @param requestCode requestCode for result + */ + public static void pickImage(Activity activity, int requestCode) { + try { + activity.startActivityForResult(getImagePicker(), requestCode); + } catch (ActivityNotFoundException e) { + showImagePickerError(activity); + } + } + + /** + * Pick image from a Fragment with a custom request code + * + * @param context Context + * @param fragment Fragment to receive result + * @param requestCode requestCode for result + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static void pickImage(Context context, Fragment fragment, int requestCode) { + try { + fragment.startActivityForResult(getImagePicker(), requestCode); + } catch (ActivityNotFoundException e) { + showImagePickerError(context); + } + } + + /** + * Pick image from a support library Fragment with a custom request code + * + * @param context Context + * @param fragment Fragment to receive result + * @param requestCode requestCode for result + */ + public static void pickImage(Context context, androidx.fragment.app.Fragment fragment, int requestCode) { + try { + fragment.startActivityForResult(getImagePicker(), requestCode); + } catch (ActivityNotFoundException e) { + showImagePickerError(context); + } + } + + private static Intent getImagePicker() { + return new Intent(Intent.ACTION_GET_CONTENT).setType("image/*"); + } + + private static void showImagePickerError(Context context) { + Toast.makeText(context.getApplicationContext(), R.string.crop__pick_error, Toast.LENGTH_SHORT).show(); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java new file mode 100644 index 0000000..782cfe5 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java @@ -0,0 +1,441 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.annotation.TargetApi; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.net.Uri; +import android.opengl.GLES10; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.provider.MediaStore; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.CountDownLatch; + +/* + * Modified from original in AOSP. + */ +public class CropImageActivity extends MonitoredActivity { + + private static final int SIZE_DEFAULT = 2048; + private static final int SIZE_LIMIT = 4096; + + private final Handler handler = new Handler(); + + private int aspectX; + private int aspectY; + + // Output image + private int maxX; + private int maxY; + private int exifRotation; + private boolean saveAsPng; + + private Uri sourceUri; + private Uri saveUri; + + private boolean isSaving; + + private int sampleSize; + private RotateBitmap rotateBitmap; + private CropImageView imageView; + private HighlightView cropView; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + setupWindowFlags(); + setupViews(); + + loadInput(); + if (rotateBitmap == null) { + finish(); + return; + } + startCrop(); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void setupWindowFlags() { + requestWindowFeature(Window.FEATURE_NO_TITLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } + } + + private void setupViews() { + setContentView(R.layout.crop__activity_crop); + + imageView = (CropImageView) findViewById(R.id.crop_image); + imageView.context = this; + imageView.setRecycler(new ImageViewTouchBase.Recycler() { + @Override + public void recycle(Bitmap b) { + b.recycle(); + System.gc(); + } + }); + + findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + onSaveClicked(); + } + }); + } + + private void loadInput() { + Intent intent = getIntent(); + Bundle extras = intent.getExtras(); + + if (extras != null) { + aspectX = extras.getInt(Crop.Extra.ASPECT_X); + aspectY = extras.getInt(Crop.Extra.ASPECT_Y); + maxX = extras.getInt(Crop.Extra.MAX_X); + maxY = extras.getInt(Crop.Extra.MAX_Y); + saveAsPng = extras.getBoolean(Crop.Extra.AS_PNG, false); + saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT); + } + + sourceUri = intent.getData(); + if (sourceUri != null) { + exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri)); + + InputStream is = null; + try { + sampleSize = calculateBitmapSampleSize(sourceUri); + is = getContentResolver().openInputStream(sourceUri); + BitmapFactory.Options option = new BitmapFactory.Options(); + option.inSampleSize = sampleSize; + rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation); + } catch (IOException e) { + Log.e("Error reading image: " + e.getMessage(), e); + setResultException(e); + } catch (OutOfMemoryError e) { + Log.e("OOM reading image: " + e.getMessage(), e); + setResultException(e); + } finally { + CropUtil.closeSilently(is); + } + } + } + + private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException { + InputStream is = null; + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + try { + is = getContentResolver().openInputStream(bitmapUri); + BitmapFactory.decodeStream(is, null, options); // Just get image size + } finally { + CropUtil.closeSilently(is); + } + + int maxSize = getMaxImageSize(); + int sampleSize = 1; + while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) { + sampleSize = sampleSize << 1; + } + return sampleSize; + } + + private int getMaxImageSize() { + int textureLimit = getMaxTextureSize(); + if (textureLimit == 0) { + return SIZE_DEFAULT; + } else { + return Math.min(textureLimit, SIZE_LIMIT); + } + } + + private int getMaxTextureSize() { + // The OpenGL texture size is the maximum size that can be drawn in an ImageView + int[] maxSize = new int[1]; + GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0); + return maxSize[0]; + } + + private void startCrop() { + if (isFinishing()) { + return; + } + imageView.setImageRotateBitmapResetBase(rotateBitmap, true); + CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait), + new Runnable() { + public void run() { + final CountDownLatch latch = new CountDownLatch(1); + handler.post(new Runnable() { + public void run() { + if (imageView.getScale() == 1F) { + imageView.center(); + } + latch.countDown(); + } + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + new Cropper().crop(); + } + }, handler + ); + } + + private class Cropper { + + private void makeDefault() { + if (rotateBitmap == null) { + return; + } + + HighlightView hv = new HighlightView(imageView); + final int width = rotateBitmap.getWidth(); + final int height = rotateBitmap.getHeight(); + + Rect imageRect = new Rect(0, 0, width, height); + + // Make the default size about 4/5 of the width or height + int cropWidth = Math.min(width, height) * 4 / 5; + @SuppressWarnings("SuspiciousNameCombination") + int cropHeight = cropWidth; + + if (aspectX != 0 && aspectY != 0) { + if (aspectX > aspectY) { + cropHeight = cropWidth * aspectY / aspectX; + } else { + cropWidth = cropHeight * aspectX / aspectY; + } + } + + int x = (width - cropWidth) / 2; + int y = (height - cropHeight) / 2; + + RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight); + hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0); + imageView.add(hv); + } + + public void crop() { + handler.post(new Runnable() { + public void run() { + makeDefault(); + imageView.invalidate(); + if (imageView.highlightViews.size() == 1) { + cropView = imageView.highlightViews.get(0); + cropView.setFocus(true); + } + } + }); + } + } + + private void onSaveClicked() { + if (cropView == null || isSaving) { + return; + } + isSaving = true; + + Bitmap croppedImage; + Rect r = cropView.getScaledCropRect(sampleSize); + int width = r.width(); + int height = r.height(); + + int outWidth = width; + int outHeight = height; + if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) { + float ratio = (float) width / (float) height; + if ((float) maxX / (float) maxY > ratio) { + outHeight = maxY; + outWidth = (int) ((float) maxY * ratio + .5f); + } else { + outWidth = maxX; + outHeight = (int) ((float) maxX / ratio + .5f); + } + } + + try { + croppedImage = decodeRegionCrop(r, outWidth, outHeight); + } catch (IllegalArgumentException e) { + setResultException(e); + finish(); + return; + } + + if (croppedImage != null) { + imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true); + imageView.center(); + imageView.highlightViews.clear(); + } + saveImage(croppedImage); + } + + private void saveImage(Bitmap croppedImage) { + if (croppedImage != null) { + final Bitmap b = croppedImage; + CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving), + new Runnable() { + public void run() { + saveOutput(b); + } + }, handler + ); + } else { + finish(); + } + } + + private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) { + // Release memory now + clearImageView(); + + InputStream is = null; + Bitmap croppedImage = null; + try { + is = getContentResolver().openInputStream(sourceUri); + BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false); + final int width = decoder.getWidth(); + final int height = decoder.getHeight(); + + if (exifRotation != 0) { + // Adjust crop area to account for image rotation + Matrix matrix = new Matrix(); + matrix.setRotate(-exifRotation); + + RectF adjusted = new RectF(); + matrix.mapRect(adjusted, new RectF(rect)); + + // Adjust to account for origin at 0,0 + adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0); + rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom); + } + + try { + croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options()); + if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) { + Matrix matrix = new Matrix(); + matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height()); + croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true); + } + } catch (IllegalArgumentException e) { + // Rethrow with some extra information + throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image (" + + width + "," + height + "," + exifRotation + ")", e); + } + + } catch (IOException e) { + Log.e("Error cropping image: " + e.getMessage(), e); + setResultException(e); + } catch (OutOfMemoryError e) { + Log.e("OOM cropping image: " + e.getMessage(), e); + setResultException(e); + } finally { + CropUtil.closeSilently(is); + } + return croppedImage; + } + + private void clearImageView() { + imageView.clear(); + if (rotateBitmap != null) { + rotateBitmap.recycle(); + } + System.gc(); + } + + private void saveOutput(Bitmap croppedImage) { + if (saveUri != null) { + OutputStream outputStream = null; + try { + outputStream = getContentResolver().openOutputStream(saveUri); + if (outputStream != null) { + croppedImage.compress(saveAsPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, + 90, // note: quality is ignored when using PNG + outputStream); + } + } catch (IOException e) { + setResultException(e); + Log.e("Cannot open file: " + saveUri, e); + } finally { + CropUtil.closeSilently(outputStream); + } + + CropUtil.copyExifRotation( + CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri), + CropUtil.getFromMediaUri(this, getContentResolver(), saveUri) + ); + + setResultUri(saveUri); + } + + final Bitmap b = croppedImage; + handler.post(new Runnable() { + public void run() { + imageView.clear(); + b.recycle(); + } + }); + + finish(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (rotateBitmap != null) { + rotateBitmap.recycle(); + } + } + + @Override + public boolean onSearchRequested() { + return false; + } + + public boolean isSaving() { + return isSaving; + } + + private void setResultUri(Uri uri) { + setResult(RESULT_OK, new Intent().putExtra(MediaStore.EXTRA_OUTPUT, uri)); + } + + private void setResultException(Throwable throwable) { + setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable)); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java new file mode 100644 index 0000000..4ad26c7 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java @@ -0,0 +1,195 @@ +package com.soundcloud.crop; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import androidx.annotation.NonNull; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import java.util.ArrayList; + +public class CropImageView extends ImageViewTouchBase { + + ArrayList highlightViews = new ArrayList(); + HighlightView motionHighlightView; + Context context; + + private float lastX; + private float lastY; + private int motionEdge; + private int validPointerId; + + public CropImageView(Context context) { + super(context); + } + + public CropImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CropImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + if (bitmapDisplayed.getBitmap() != null) { + for (HighlightView hv : highlightViews) { + + hv.matrix.set(getUnrotatedMatrix()); + hv.invalidate(); + if (hv.hasFocus()) { + centerBasedOnHighlightView(hv); + } + } + } + } + + @Override + protected void zoomTo(float scale, float centerX, float centerY) { + super.zoomTo(scale, centerX, centerY); + for (HighlightView hv : highlightViews) { + hv.matrix.set(getUnrotatedMatrix()); + hv.invalidate(); + } + } + + @Override + protected void zoomIn() { + super.zoomIn(); + for (HighlightView hv : highlightViews) { + hv.matrix.set(getUnrotatedMatrix()); + hv.invalidate(); + } + } + + @Override + protected void zoomOut() { + super.zoomOut(); + for (HighlightView hv : highlightViews) { + hv.matrix.set(getUnrotatedMatrix()); + hv.invalidate(); + } + } + + @Override + protected void postTranslate(float deltaX, float deltaY) { + super.postTranslate(deltaX, deltaY); + for (HighlightView hv : highlightViews) { + hv.matrix.postTranslate(deltaX, deltaY); + hv.invalidate(); + } + } + + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + CropImageActivity cropImageActivity = (CropImageActivity) context; + if (cropImageActivity.isSaving()) { + return false; + } + + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + for (HighlightView hv : highlightViews) { + int edge = hv.getHit(event.getX(), event.getY()); + if (edge != HighlightView.GROW_NONE) { + motionEdge = edge; + motionHighlightView = hv; + lastX = event.getX(); + lastY = event.getY(); + // Prevent multiple touches from interfering with crop area re-sizing + validPointerId = event.getPointerId(event.getActionIndex()); + motionHighlightView.setMode((edge == HighlightView.MOVE) + ? HighlightView.ModifyMode.Move + : HighlightView.ModifyMode.Grow); + break; + } + } + break; + case MotionEvent.ACTION_UP: + if (motionHighlightView != null) { + centerBasedOnHighlightView(motionHighlightView); + motionHighlightView.setMode(HighlightView.ModifyMode.None); + } + motionHighlightView = null; + center(); + break; + case MotionEvent.ACTION_MOVE: + if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) { + motionHighlightView.handleMotion(motionEdge, event.getX() + - lastX, event.getY() - lastY); + lastX = event.getX(); + lastY = event.getY(); + } + + // If we're not zoomed then there's no point in even allowing the user to move the image around. + // This call to center puts it back to the normalized location. + if (getScale() == 1F) { + center(); + } + break; + } + + return true; + } + + // Pan the displayed image to make sure the cropping rectangle is visible. + private void ensureVisible(HighlightView hv) { + Rect r = hv.drawRect; + + int panDeltaX1 = Math.max(0, getLeft() - r.left); + int panDeltaX2 = Math.min(0, getRight() - r.right); + + int panDeltaY1 = Math.max(0, getTop() - r.top); + int panDeltaY2 = Math.min(0, getBottom() - r.bottom); + + int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2; + int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2; + + if (panDeltaX != 0 || panDeltaY != 0) { + panBy(panDeltaX, panDeltaY); + } + } + + // If the cropping rectangle's size changed significantly, change the + // view's center and scale according to the cropping rectangle. + private void centerBasedOnHighlightView(HighlightView hv) { + Rect drawRect = hv.drawRect; + + float width = drawRect.width(); + float height = drawRect.height(); + + float thisWidth = getWidth(); + float thisHeight = getHeight(); + + float z1 = thisWidth / width * .6F; + float z2 = thisHeight / height * .6F; + + float zoom = Math.min(z1, z2); + zoom = zoom * this.getScale(); + zoom = Math.max(1F, zoom); + + if ((Math.abs(zoom - getScale()) / zoom) > .1) { + float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() }; + getUnrotatedMatrix().mapPoints(coordinates); + zoomTo(zoom, coordinates[0], coordinates[1], 300F); + } + + ensureVisible(hv); + } + + @Override + protected void onDraw(@NonNull Canvas canvas) { + super.onDraw(canvas); + for (HighlightView highlightView : highlightViews) { + highlightView.draw(canvas); + } + } + + public void add(HighlightView hv) { + highlightViews.add(hv); + invalidate(); + } +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java new file mode 100644 index 0000000..2f945e3 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Handler; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; +import androidx.annotation.Nullable; +import android.text.TextUtils; + +import java.io.Closeable; +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/* + * Modified from original in AOSP. + */ +class CropUtil { + + private static final String SCHEME_FILE = "file"; + private static final String SCHEME_CONTENT = "content"; + + public static void closeSilently(@Nullable Closeable c) { + if (c == null) return; + try { + c.close(); + } catch (Throwable t) { + // Do nothing + } + } + + public static int getExifRotation(File imageFile) { + if (imageFile == null) return 0; + try { + ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath()); + // We only recognize a subset of orientation tag values + switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) { + case ExifInterface.ORIENTATION_ROTATE_90: + return 90; + case ExifInterface.ORIENTATION_ROTATE_180: + return 180; + case ExifInterface.ORIENTATION_ROTATE_270: + return 270; + default: + return ExifInterface.ORIENTATION_UNDEFINED; + } + } catch (IOException e) { + Log.e("Error getting Exif data", e); + return 0; + } + } + + public static boolean copyExifRotation(File sourceFile, File destFile) { + if (sourceFile == null || destFile == null) return false; + try { + ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath()); + ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath()); + exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION)); + exifDest.saveAttributes(); + return true; + } catch (IOException e) { + Log.e("Error copying Exif data", e); + return false; + } + } + + @Nullable + public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) { + if (uri == null) return null; + + if (SCHEME_FILE.equals(uri.getScheme())) { + return new File(uri.getPath()); + } else if (SCHEME_CONTENT.equals(uri.getScheme())) { + final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME }; + Cursor cursor = null; + try { + cursor = resolver.query(uri, filePathColumn, null, null, null); + if (cursor != null && cursor.moveToFirst()) { + final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ? + cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) : + cursor.getColumnIndex(MediaStore.MediaColumns.DATA); + // Picasa images on API 13+ + if (columnIndex != -1) { + String filePath = cursor.getString(columnIndex); + if (!TextUtils.isEmpty(filePath)) { + return new File(filePath); + } + } else if (TextUtils.equals(uri.getAuthority(), UriUtil.getFileProviderName(context))) { + //这里修复拍照自定义的uri获取路径失败的问题 + String path = UriUtil.parseOwnUri(context, uri); + if (!TextUtils.isEmpty(path)) { + File file = new File(path); + if (file.exists()) { + return file; + } + } + } + } + } catch (IllegalArgumentException e) { + // Google Drive images + return getFromMediaUriPfd(context, resolver, uri); + } catch (SecurityException ignored) { + // Nothing we can do + } finally { + if (cursor != null) cursor.close(); + } + } + return null; + } + + private static String getTempFilename(Context context) throws IOException { + File outputDir = context.getCacheDir(); + File outputFile = File.createTempFile("image", "tmp", outputDir); + return outputFile.getAbsolutePath(); + } + + @Nullable + private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) { + if (uri == null) return null; + + FileInputStream input = null; + FileOutputStream output = null; + try { + ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r"); + FileDescriptor fd = pfd.getFileDescriptor(); + input = new FileInputStream(fd); + + String tempFilename = getTempFilename(context); + output = new FileOutputStream(tempFilename); + + int read; + byte[] bytes = new byte[4096]; + while ((read = input.read(bytes)) != -1) { + output.write(bytes, 0, read); + } + return new File(tempFilename); + } catch (IOException ignored) { + // Nothing we can do + } finally { + closeSilently(input); + closeSilently(output); + } + return null; + } + + public static void startBackgroundJob(MonitoredActivity activity, + String title, String message, Runnable job, Handler handler) { + // Make the progress dialog uncancelable, so that we can guarantee + // the thread will be done before the activity getting destroyed + ProgressDialog dialog = ProgressDialog.show( + activity, title, message, true, false); + new Thread(new BackgroundJob(activity, job, dialog, handler)).start(); + } + + private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable { + + private final MonitoredActivity activity; + private final ProgressDialog dialog; + private final Runnable job; + private final Handler handler; + private final Runnable cleanupRunner = new Runnable() { + public void run() { + activity.removeLifeCycleListener(BackgroundJob.this); + if (dialog.getWindow() != null) dialog.dismiss(); + } + }; + + public BackgroundJob(MonitoredActivity activity, Runnable job, + ProgressDialog dialog, Handler handler) { + this.activity = activity; + this.dialog = dialog; + this.job = job; + this.activity.addLifeCycleListener(this); + this.handler = handler; + } + + public void run() { + try { + job.run(); + } finally { + handler.post(cleanupRunner); + } + } + + @Override + public void onActivityDestroyed(MonitoredActivity activity) { + // We get here only when the onDestroyed being called before + // the cleanupRunner. So, run it now and remove it from the queue + cleanupRunner.run(); + handler.removeCallbacks(cleanupRunner); + } + + @Override + public void onActivityStopped(MonitoredActivity activity) { + dialog.hide(); + } + + @Override + public void onActivityStarted(MonitoredActivity activity) { + dialog.show(); + } + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java new file mode 100644 index 0000000..ec0b764 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Build; +import android.util.TypedValue; +import android.view.View; + +/* + * Modified from version in AOSP. + * + * This class is used to display a highlighted cropping rectangle + * overlayed on the image. There are two coordinate spaces in use. One is + * image, another is screen. computeLayout() uses matrix to map from image + * space to screen space. + */ +class HighlightView { + + public static final int GROW_NONE = (1 << 0); + public static final int GROW_LEFT_EDGE = (1 << 1); + public static final int GROW_RIGHT_EDGE = (1 << 2); + public static final int GROW_TOP_EDGE = (1 << 3); + public static final int GROW_BOTTOM_EDGE = (1 << 4); + public static final int MOVE = (1 << 5); + + private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5; + private static final float HANDLE_RADIUS_DP = 12f; + private static final float OUTLINE_DP = 2f; + + enum ModifyMode { None, Move, Grow } + enum HandleMode { Changing, Always, Never } + + RectF cropRect; // Image space + Rect drawRect; // Screen space + Matrix matrix; + private RectF imageRect; // Image space + + private final Paint outsidePaint = new Paint(); + private final Paint outlinePaint = new Paint(); + private final Paint handlePaint = new Paint(); + + private View viewContext; // View displaying image + private boolean showThirds; + private boolean showCircle; + private int highlightColor; + + private ModifyMode modifyMode = ModifyMode.None; + private HandleMode handleMode = HandleMode.Changing; + private boolean maintainAspectRatio; + private float initialAspectRatio; + private float handleRadius; + private float outlineWidth; + private boolean isFocused; + + public HighlightView(View context) { + viewContext = context; + initStyles(context.getContext()); + } + + private void initStyles(Context context) { + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true); + TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView); + try { + showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false); + showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false); + highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor, + DEFAULT_HIGHLIGHT_COLOR); + handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)]; + } finally { + attributes.recycle(); + } + } + + public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) { + matrix = new Matrix(m); + + this.cropRect = cropRect; + this.imageRect = new RectF(imageRect); + this.maintainAspectRatio = maintainAspectRatio; + + initialAspectRatio = this.cropRect.width() / this.cropRect.height(); + drawRect = computeLayout(); + + outsidePaint.setARGB(125, 50, 50, 50); + outlinePaint.setStyle(Paint.Style.STROKE); + outlinePaint.setAntiAlias(true); + outlineWidth = dpToPx(OUTLINE_DP); + + handlePaint.setColor(highlightColor); + handlePaint.setStyle(Paint.Style.FILL); + handlePaint.setAntiAlias(true); + handleRadius = dpToPx(HANDLE_RADIUS_DP); + + modifyMode = ModifyMode.None; + } + + private float dpToPx(float dp) { + return dp * viewContext.getResources().getDisplayMetrics().density; + } + + protected void draw(Canvas canvas) { + canvas.save(); + Path path = new Path(); + outlinePaint.setStrokeWidth(outlineWidth); + if (!hasFocus()) { + outlinePaint.setColor(Color.BLACK); + canvas.drawRect(drawRect, outlinePaint); + } else { + Rect viewDrawingRect = new Rect(); + viewContext.getDrawingRect(viewDrawingRect); + + path.addRect(new RectF(drawRect), Path.Direction.CW); + outlinePaint.setColor(highlightColor); + + if (isClipPathSupported(canvas)) { + canvas.clipPath(path, Region.Op.DIFFERENCE); + canvas.drawRect(viewDrawingRect, outsidePaint); + } else { + drawOutsideFallback(canvas); + } + + canvas.restore(); + canvas.drawPath(path, outlinePaint); + + if (showThirds) { + drawThirds(canvas); + } + + if (showCircle) { + drawCircle(canvas); + } + + if (handleMode == HandleMode.Always || + (handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) { + drawHandles(canvas); + } + } + } + + /* + * Fall back to naive method for darkening outside crop area + */ + private void drawOutsideFallback(Canvas canvas) { + canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint); + canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint); + canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint); + canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint); + } + + /* + * Clip path is broken, unreliable or not supported on: + * - JellyBean MR1 + * - ICS & ICS MR1 with hardware acceleration turned on + */ + @SuppressLint("NewApi") + private boolean isClipPathSupported(Canvas canvas) { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) { + return false; + } else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + || Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + return true; + } else { + return !canvas.isHardwareAccelerated(); + } + } + + private void drawHandles(Canvas canvas) { + int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2); + int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2); + + canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint); + canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint); + canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint); + canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint); + } + + private void drawThirds(Canvas canvas) { + outlinePaint.setStrokeWidth(1); + float xThird = (drawRect.right - drawRect.left) / 3; + float yThird = (drawRect.bottom - drawRect.top) / 3; + + canvas.drawLine(drawRect.left + xThird, drawRect.top, + drawRect.left + xThird, drawRect.bottom, outlinePaint); + canvas.drawLine(drawRect.left + xThird * 2, drawRect.top, + drawRect.left + xThird * 2, drawRect.bottom, outlinePaint); + canvas.drawLine(drawRect.left, drawRect.top + yThird, + drawRect.right, drawRect.top + yThird, outlinePaint); + canvas.drawLine(drawRect.left, drawRect.top + yThird * 2, + drawRect.right, drawRect.top + yThird * 2, outlinePaint); + } + + private void drawCircle(Canvas canvas) { + outlinePaint.setStrokeWidth(1); + canvas.drawOval(new RectF(drawRect), outlinePaint); + } + + public void setMode(ModifyMode mode) { + if (mode != modifyMode) { + modifyMode = mode; + viewContext.invalidate(); + } + } + + // Determines which edges are hit by touching at (x, y) + public int getHit(float x, float y) { + Rect r = computeLayout(); + final float hysteresis = 20F; + int retval = GROW_NONE; + + // verticalCheck makes sure the position is between the top and + // the bottom edge (with some tolerance). Similar for horizCheck. + boolean verticalCheck = (y >= r.top - hysteresis) + && (y < r.bottom + hysteresis); + boolean horizCheck = (x >= r.left - hysteresis) + && (x < r.right + hysteresis); + + // Check whether the position is near some edge(s) + if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) { + retval |= GROW_LEFT_EDGE; + } + if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) { + retval |= GROW_RIGHT_EDGE; + } + if ((Math.abs(r.top - y) < hysteresis) && horizCheck) { + retval |= GROW_TOP_EDGE; + } + if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) { + retval |= GROW_BOTTOM_EDGE; + } + + // Not near any edge but inside the rectangle: move + if (retval == GROW_NONE && r.contains((int) x, (int) y)) { + retval = MOVE; + } + return retval; + } + + // Handles motion (dx, dy) in screen space. + // The "edge" parameter specifies which edges the user is dragging. + void handleMotion(int edge, float dx, float dy) { + Rect r = computeLayout(); + if (edge == MOVE) { + // Convert to image space before sending to moveBy() + moveBy(dx * (cropRect.width() / r.width()), + dy * (cropRect.height() / r.height())); + } else { + if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) { + dx = 0; + } + + if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) { + dy = 0; + } + + // Convert to image space before sending to growBy() + float xDelta = dx * (cropRect.width() / r.width()); + float yDelta = dy * (cropRect.height() / r.height()); + growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta, + (((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta); + } + } + + // Grows the cropping rectangle by (dx, dy) in image space + void moveBy(float dx, float dy) { + Rect invalRect = new Rect(drawRect); + + cropRect.offset(dx, dy); + + // Put the cropping rectangle inside image rectangle + cropRect.offset( + Math.max(0, imageRect.left - cropRect.left), + Math.max(0, imageRect.top - cropRect.top)); + + cropRect.offset( + Math.min(0, imageRect.right - cropRect.right), + Math.min(0, imageRect.bottom - cropRect.bottom)); + + drawRect = computeLayout(); + invalRect.union(drawRect); + invalRect.inset(-(int) handleRadius, -(int) handleRadius); + viewContext.invalidate(invalRect); + } + + // Grows the cropping rectangle by (dx, dy) in image space. + void growBy(float dx, float dy) { + if (maintainAspectRatio) { + if (dx != 0) { + dy = dx / initialAspectRatio; + } else if (dy != 0) { + dx = dy * initialAspectRatio; + } + } + + // Don't let the cropping rectangle grow too fast. + // Grow at most half of the difference between the image rectangle and + // the cropping rectangle. + RectF r = new RectF(cropRect); + if (dx > 0F && r.width() + 2 * dx > imageRect.width()) { + dx = (imageRect.width() - r.width()) / 2F; + if (maintainAspectRatio) { + dy = dx / initialAspectRatio; + } + } + if (dy > 0F && r.height() + 2 * dy > imageRect.height()) { + dy = (imageRect.height() - r.height()) / 2F; + if (maintainAspectRatio) { + dx = dy * initialAspectRatio; + } + } + + r.inset(-dx, -dy); + + // Don't let the cropping rectangle shrink too fast + final float widthCap = 25F; + if (r.width() < widthCap) { + r.inset(-(widthCap - r.width()) / 2F, 0F); + } + float heightCap = maintainAspectRatio + ? (widthCap / initialAspectRatio) + : widthCap; + if (r.height() < heightCap) { + r.inset(0F, -(heightCap - r.height()) / 2F); + } + + // Put the cropping rectangle inside the image rectangle + if (r.left < imageRect.left) { + r.offset(imageRect.left - r.left, 0F); + } else if (r.right > imageRect.right) { + r.offset(-(r.right - imageRect.right), 0F); + } + if (r.top < imageRect.top) { + r.offset(0F, imageRect.top - r.top); + } else if (r.bottom > imageRect.bottom) { + r.offset(0F, -(r.bottom - imageRect.bottom)); + } + + cropRect.set(r); + drawRect = computeLayout(); + viewContext.invalidate(); + } + + // Returns the cropping rectangle in image space with specified scale + public Rect getScaledCropRect(float scale) { + return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale), + (int) (cropRect.right * scale), (int) (cropRect.bottom * scale)); + } + + // Maps the cropping rectangle from image space to screen space + private Rect computeLayout() { + RectF r = new RectF(cropRect.left, cropRect.top, + cropRect.right, cropRect.bottom); + matrix.mapRect(r); + return new Rect(Math.round(r.left), Math.round(r.top), + Math.round(r.right), Math.round(r.bottom)); + } + + public void invalidate() { + drawRect = computeLayout(); + } + + public boolean hasFocus() { + return isFocused; + } + + public void setFocus(boolean isFocused) { + this.isFocused = isFocused; + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java new file mode 100644 index 0000000..64dc841 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.widget.ImageView; + +/* + * Modified from original in AOSP. + */ +abstract class ImageViewTouchBase extends ImageView { + + private static final float SCALE_RATE = 1.25F; + + // This is the base transformation which is used to show the image + // initially. The current computation for this shows the image in + // it's entirety, letterboxing as needed. One could choose to + // show the image as cropped instead. + // + // This matrix is recomputed when we go from the thumbnail image to + // the full size image. + protected Matrix baseMatrix = new Matrix(); + + // This is the supplementary transformation which reflects what + // the user has done in terms of zooming and panning. + // + // This matrix remains the same when we go from the thumbnail image + // to the full size image. + protected Matrix suppMatrix = new Matrix(); + + // This is the final matrix which is computed as the concatentation + // of the base matrix and the supplementary matrix. + private final Matrix displayMatrix = new Matrix(); + + // Temporary buffer used for getting the values out of a matrix. + private final float[] matrixValues = new float[9]; + + // The current bitmap being displayed. + protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0); + + int thisWidth = -1; + int thisHeight = -1; + + float maxZoom; + + private Runnable onLayoutRunnable; + + protected Handler handler = new Handler(); + + // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished + // its use of that Bitmap + public interface Recycler { + public void recycle(Bitmap b); + } + + private Recycler recycler; + + public ImageViewTouchBase(Context context) { + super(context); + init(); + } + + public ImageViewTouchBase(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(); + } + + public void setRecycler(Recycler recycler) { + this.recycler = recycler; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + thisWidth = right - left; + thisHeight = bottom - top; + Runnable r = onLayoutRunnable; + if (r != null) { + onLayoutRunnable = null; + r.run(); + } + if (bitmapDisplayed.getBitmap() != null) { + getProperBaseMatrix(bitmapDisplayed, baseMatrix, true); + setImageMatrix(getImageViewMatrix()); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { + event.startTracking(); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) { + if (getScale() > 1.0f) { + // If we're zoomed in, pressing Back jumps out to show the + // entire image, otherwise Back returns the user to the gallery + zoomTo(1.0f); + return true; + } + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void setImageBitmap(Bitmap bitmap) { + setImageBitmap(bitmap, 0); + } + + private void setImageBitmap(Bitmap bitmap, int rotation) { + super.setImageBitmap(bitmap); + Drawable d = getDrawable(); + if (d != null) { + d.setDither(true); + } + + Bitmap old = bitmapDisplayed.getBitmap(); + bitmapDisplayed.setBitmap(bitmap); + bitmapDisplayed.setRotation(rotation); + + if (old != null && old != bitmap && recycler != null) { + recycler.recycle(old); + } + } + + public void clear() { + setImageBitmapResetBase(null, true); + } + + + // This function changes bitmap, reset base matrix according to the size + // of the bitmap, and optionally reset the supplementary matrix + public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) { + setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp); + } + + public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) { + final int viewWidth = getWidth(); + + if (viewWidth <= 0) { + onLayoutRunnable = new Runnable() { + public void run() { + setImageRotateBitmapResetBase(bitmap, resetSupp); + } + }; + return; + } + + if (bitmap.getBitmap() != null) { + getProperBaseMatrix(bitmap, baseMatrix, true); + setImageBitmap(bitmap.getBitmap(), bitmap.getRotation()); + } else { + baseMatrix.reset(); + setImageBitmap(null); + } + + if (resetSupp) { + suppMatrix.reset(); + } + setImageMatrix(getImageViewMatrix()); + maxZoom = calculateMaxZoom(); + } + + // Center as much as possible in one or both axis. Centering is defined as follows: + // * If the image is scaled down below the view's dimensions then center it. + // * If the image is scaled larger than the view and is translated out of view then translate it back into view. + protected void center() { + final Bitmap bitmap = bitmapDisplayed.getBitmap(); + if (bitmap == null) { + return; + } + Matrix m = getImageViewMatrix(); + + RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight()); + m.mapRect(rect); + + float height = rect.height(); + float width = rect.width(); + + float deltaX = 0, deltaY = 0; + + deltaY = centerVertical(rect, height, deltaY); + deltaX = centerHorizontal(rect, width, deltaX); + + postTranslate(deltaX, deltaY); + setImageMatrix(getImageViewMatrix()); + } + + private float centerVertical(RectF rect, float height, float deltaY) { + int viewHeight = getHeight(); + if (height < viewHeight) { + deltaY = (viewHeight - height) / 2 - rect.top; + } else if (rect.top > 0) { + deltaY = -rect.top; + } else if (rect.bottom < viewHeight) { + deltaY = getHeight() - rect.bottom; + } + return deltaY; + } + + private float centerHorizontal(RectF rect, float width, float deltaX) { + int viewWidth = getWidth(); + if (width < viewWidth) { + deltaX = (viewWidth - width) / 2 - rect.left; + } else if (rect.left > 0) { + deltaX = -rect.left; + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right; + } + return deltaX; + } + + private void init() { + setScaleType(ImageView.ScaleType.MATRIX); + } + + protected float getValue(Matrix matrix, int whichValue) { + matrix.getValues(matrixValues); + return matrixValues[whichValue]; + } + + // Get the scale factor out of the matrix. + protected float getScale(Matrix matrix) { + return getValue(matrix, Matrix.MSCALE_X); + } + + protected float getScale() { + return getScale(suppMatrix); + } + + // Setup the base matrix so that the image is centered and scaled properly. + private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) { + float viewWidth = getWidth(); + float viewHeight = getHeight(); + + float w = bitmap.getWidth(); + float h = bitmap.getHeight(); + matrix.reset(); + + // We limit up-scaling to 3x otherwise the result may look bad if it's a small icon + float widthScale = Math.min(viewWidth / w, 3.0f); + float heightScale = Math.min(viewHeight / h, 3.0f); + float scale = Math.min(widthScale, heightScale); + + if (includeRotation) { + matrix.postConcat(bitmap.getRotateMatrix()); + } + matrix.postScale(scale, scale); + matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F); + } + + // Combine the base matrix and the supp matrix to make the final matrix + protected Matrix getImageViewMatrix() { + // The final matrix is computed as the concatentation of the base matrix + // and the supplementary matrix + displayMatrix.set(baseMatrix); + displayMatrix.postConcat(suppMatrix); + return displayMatrix; + } + + public Matrix getUnrotatedMatrix(){ + Matrix unrotated = new Matrix(); + getProperBaseMatrix(bitmapDisplayed, unrotated, false); + unrotated.postConcat(suppMatrix); + return unrotated; + } + + protected float calculateMaxZoom() { + if (bitmapDisplayed.getBitmap() == null) { + return 1F; + } + + float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth; + float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight; + return Math.max(fw, fh) * 4; // 400% + } + + protected void zoomTo(float scale, float centerX, float centerY) { + if (scale > maxZoom) { + scale = maxZoom; + } + + float oldScale = getScale(); + float deltaScale = scale / oldScale; + + suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY); + setImageMatrix(getImageViewMatrix()); + center(); + } + + protected void zoomTo(final float scale, final float centerX, + final float centerY, final float durationMs) { + final float incrementPerMs = (scale - getScale()) / durationMs; + final float oldScale = getScale(); + final long startTime = System.currentTimeMillis(); + + handler.post(new Runnable() { + public void run() { + long now = System.currentTimeMillis(); + float currentMs = Math.min(durationMs, now - startTime); + float target = oldScale + (incrementPerMs * currentMs); + zoomTo(target, centerX, centerY); + + if (currentMs < durationMs) { + handler.post(this); + } + } + }); + } + + protected void zoomTo(float scale) { + float cx = getWidth() / 2F; + float cy = getHeight() / 2F; + zoomTo(scale, cx, cy); + } + + protected void zoomIn() { + zoomIn(SCALE_RATE); + } + + protected void zoomOut() { + zoomOut(SCALE_RATE); + } + + protected void zoomIn(float rate) { + if (getScale() >= maxZoom) { + return; // Don't let the user zoom into the molecular level + } + if (bitmapDisplayed.getBitmap() == null) { + return; + } + + float cx = getWidth() / 2F; + float cy = getHeight() / 2F; + + suppMatrix.postScale(rate, rate, cx, cy); + setImageMatrix(getImageViewMatrix()); + } + + protected void zoomOut(float rate) { + if (bitmapDisplayed.getBitmap() == null) { + return; + } + + float cx = getWidth() / 2F; + float cy = getHeight() / 2F; + + // Zoom out to at most 1x + Matrix tmp = new Matrix(suppMatrix); + tmp.postScale(1F / rate, 1F / rate, cx, cy); + + if (getScale(tmp) < 1F) { + suppMatrix.setScale(1F, 1F, cx, cy); + } else { + suppMatrix.postScale(1F / rate, 1F / rate, cx, cy); + } + setImageMatrix(getImageViewMatrix()); + center(); + } + + protected void postTranslate(float dx, float dy) { + suppMatrix.postTranslate(dx, dy); + } + + protected void panBy(float dx, float dy) { + postTranslate(dx, dy); + setImageMatrix(getImageViewMatrix()); + } +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java new file mode 100644 index 0000000..0522a62 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java @@ -0,0 +1,15 @@ +package com.soundcloud.crop; + +class Log { + + private static final String TAG = "android-crop"; + + public static void e(String msg) { + android.util.Log.e(TAG, msg); + } + + public static void e(String msg, Throwable e) { + android.util.Log.e(TAG, msg, e); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java new file mode 100644 index 0000000..91e6e72 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.app.Activity; +import android.os.Bundle; + +import java.util.ArrayList; + +/* + * Modified from original in AOSP. + */ +abstract class MonitoredActivity extends Activity { + + private final ArrayList listeners = new ArrayList(); + + public static interface LifeCycleListener { + public void onActivityCreated(MonitoredActivity activity); + public void onActivityDestroyed(MonitoredActivity activity); + public void onActivityStarted(MonitoredActivity activity); + public void onActivityStopped(MonitoredActivity activity); + } + + public static class LifeCycleAdapter implements LifeCycleListener { + public void onActivityCreated(MonitoredActivity activity) {} + public void onActivityDestroyed(MonitoredActivity activity) {} + public void onActivityStarted(MonitoredActivity activity) {} + public void onActivityStopped(MonitoredActivity activity) {} + } + + public void addLifeCycleListener(LifeCycleListener listener) { + if (listeners.contains(listener)) return; + listeners.add(listener); + } + + public void removeLifeCycleListener(LifeCycleListener listener) { + listeners.remove(listener); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + for (LifeCycleListener listener : listeners) { + listener.onActivityCreated(this); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + for (LifeCycleListener listener : listeners) { + listener.onActivityDestroyed(this); + } + } + + @Override + protected void onStart() { + super.onStart(); + for (LifeCycleListener listener : listeners) { + listener.onActivityStarted(this); + } + } + + @Override + protected void onStop() { + super.onStop(); + for (LifeCycleListener listener : listeners) { + listener.onActivityStopped(this); + } + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java new file mode 100644 index 0000000..1b1f9a2 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.soundcloud.crop; + +import android.graphics.Bitmap; +import android.graphics.Matrix; + +/* + * Modified from original in AOSP. + */ +class RotateBitmap { + + private Bitmap bitmap; + private int rotation; + + public RotateBitmap(Bitmap bitmap, int rotation) { + this.bitmap = bitmap; + this.rotation = rotation % 360; + } + + public void setRotation(int rotation) { + this.rotation = rotation; + } + + public int getRotation() { + return rotation; + } + + public Bitmap getBitmap() { + return bitmap; + } + + public void setBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + } + + public Matrix getRotateMatrix() { + // By default this is an identity matrix + Matrix matrix = new Matrix(); + if (bitmap != null && rotation != 0) { + // We want to do the rotation at origin, but since the bounding + // rectangle will be changed after rotation, so the delta values + // are based on old & new width/height respectively. + int cx = bitmap.getWidth() / 2; + int cy = bitmap.getHeight() / 2; + matrix.preTranslate(-cx, -cy); + matrix.postRotate(rotation); + matrix.postTranslate(getWidth() / 2, getHeight() / 2); + } + return matrix; + } + + public boolean isOrientationChanged() { + return (rotation / 90) % 2 != 0; + } + + public int getHeight() { + if (bitmap == null) return 0; + if (isOrientationChanged()) { + return bitmap.getWidth(); + } else { + return bitmap.getHeight(); + } + } + + public int getWidth() { + if (bitmap == null) return 0; + if (isOrientationChanged()) { + return bitmap.getHeight(); + } else { + return bitmap.getWidth(); + } + } + + public void recycle() { + if (bitmap != null) { + bitmap.recycle(); + bitmap = null; + } + } +} + diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java new file mode 100644 index 0000000..271202f --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java @@ -0,0 +1,48 @@ +package com.soundcloud.crop; + +import android.content.Context; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.text.TextUtils; +import android.util.Log; + +import java.io.File; + +public class UriUtil { + + public final static String getFileProviderName(Context context){ + return context.getPackageName()+".fileprovider"; + } + + /** + * 将TakePhoto 提供的Uri 解析出文件绝对路径 + * + * @param uri + * @return + */ + public static String parseOwnUri(Context context, Uri uri) { + if (uri == null) return null; + String path; + if (TextUtils.equals(uri.getAuthority(), getFileProviderName(context))) { + String target_text_camera_photos = "camera_photos/"; + if (uri.getPath() != null && uri.getPath().contains(target_text_camera_photos)) { + path = new File(uri.getPath().replace(target_text_camera_photos, "")) + .getAbsolutePath(); + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + path = new File(Environment.getExternalStorageDirectory(), + uri.getPath()) + .getAbsolutePath(); + } else { + path = uri.getPath(); + } + } + + } else { + path = uri.getPath(); + } + return path; + } + +} diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..373e5d8c4061ee136aca3f75b831547b4767f634 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1|-8Yw(bW~!k#XUAsp9}6B-)+^LX%RmN2q0 zcy4A9FVZ~13zTN?boFyt=akT7nrLcZW?^EGY+#aXWNL11kYZt&W(E{BHBU@7Pcl_d H(B%RE{4yBW literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp new file mode 100644 index 0000000000000000000000000000000000000000..b929a2b81e87fbb3fcb5ddeffd2296291a4d085a GIT binary patch literal 240 zcmVi?ge4Q|(IZV~-g;I@s@<(Tmi?oC@fS4`^} zK`px$XhyNhAYo9&pd*8OVsH=X_nhJA&(7dkBk=zj{y)b5C;9*I qi#7)}VKp!|I5aS0Ff?N_H8(dfWH>QqHZ@^2H8){nH)1s)AT0_rp=U+_ literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp new file mode 100644 index 0000000000000000000000000000000000000000..5719d1f9b537bc1dae31f2302cb223621d86cd09 GIT binary patch literal 230 zcmVHnbKf`7wr!h49NYXcFU{htDeYjK|{$)T@%zM6%!5wV2&3z#g3 zg_tEWT)<>OOoVKa31k6P_%RWkMe*IO1!q}*KI>&8Qea=1tiF05xGXFg7?e gFk>(@V=^^2H!x&4F=jS3VKp^3VPiL9H6S1@3hCituK)l5 literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..373e5d8c4061ee136aca3f75b831547b4767f634 GIT binary patch literal 112 zcmeAS@N?(olHy`uVBq!ia0vp^%plCc1|-8Yw(bW~!k#XUAsp9}6B-)+^LX%RmN2q0 zcy4A9FVZ~13zTN?boFyt=akT7nrLcZW?^EGY+#aXWNL11kYZt&W(E{BHBU@7Pcl_d H(B%RE{4yBW literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp new file mode 100644 index 0000000000000000000000000000000000000000..54dd1ebea5a3f6831eaa771d3ba3a2fedae19542 GIT binary patch literal 196 zcmV;#06YIuNk&GP00012MM6+kP&iDC0000lAHW9?XD~A}GcyVrNos@N8yyGuApl=s zAQ%@P1;&GdAz)ftdy}84k;uqQfMx_nXj6bAHh4y01x8@5za=yu(SHSQ+b9_y!!O{E z(lwl^;PhKcn5*QprzM=vMd>^S!>0nUv0|teSkywjQq;n7SWv0 ycM88__?;vE;fDe>VKp!|I5aS0Ff?N_H8(dfWH>QqHZ@^2H8){nH)1s)AT0{IBS)tI literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp new file mode 100644 index 0000000000000000000000000000000000000000..6be409de7ba2135a8ed0a3181859032b7f9b0d07 GIT binary patch literal 204 zcmV;-05ktmNk&GX00012MM6+kP&iDJ0000lAHW9?H85%;$tx%0qawGFnkid>8=uIzi56@GT+3Iy2{0{BBUTH*nx1CN zCaZgzv9Dx^5TUV_@z=++Z5ak$dYHBw#HS$Y9@lFPQu46oDnNbx2kYk>mfzjWo? zY^odN=5p07GPG}VLghX2_X_|uVKp!|I5aS0Ff?N_H8(dfWH>QqHZ@^2H8){nH)1s) GAT0_J_fw<* literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml new file mode 100644 index 0000000..2b25609 --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png new file mode 100644 index 0000000000000000000000000000000000000000..b3da354c27b8a270ec21a5bd4021a416d4d8a734 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^EFjFm1|(O0oL2{=q&!_5LpZJ{Cp0wt58!$3;rOwB z4O4-x!vO{c$tv!+&aYQD0#z}1y85}Sb4qA3O*AzyvoJA8HZVyxGBr0hNU<(GBw+XP4TO{dv%CNRoOVYl&1dyv!@fEh@4ak2hB~v6L zHIS$RrVf}&AZmfA#y|D=CxKrQNM}DJFqwZzU|N@pfGW>%S;r19gA@J{gAeg}F~}=l z7Y233*QG&S^L=5^SA1Xcea(*x{J6r8OZ>Rj6elbP05xGXFg7?eFk>(@V=^^2H!x&4 UF=jS3VKp^3VPiL9H6S1@3SrG^M*si- literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp new file mode 100644 index 0000000000000000000000000000000000000000..b6230d26359479cda8fba7d78466f1f8419db8e9 GIT binary patch literal 304 zcmV-00nh$YNk&El0RRA3MM6+kP&iEZ0000lKfn(V1)DaKBrPZN2e*6wRdl)}!fkBZ zs)D<_vgr7K}y_)Zd8+s!rt_VkGJh~&c zoJJMP={86xF#M!HF+&G6VKp!|I5aS0Ff?N_H8(dfWH>QqHZ@^2H8){nH)1s)AT0{m C%z*^} literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp new file mode 100644 index 0000000000000000000000000000000000000000..806077571bb27891bac7a69cb38c17e86d0d16bb GIT binary patch literal 88 zcmWIYbaT^TU|I3pW pGBKDYni`l{m>47*m?RsSnwuM>SQw_60Yy#C6O+x8OcfM#xd35p85{rr literal 0 HcmV?d00001 diff --git a/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml new file mode 100644 index 0000000..83c4a7d --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/drawable/crop__texture.xml b/libs/lib_crop/src/main/res/drawable/crop__texture.xml new file mode 100644 index 0000000..23ec469 --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable/crop__texture.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml new file mode 100644 index 0000000..c133df0 --- /dev/null +++ b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml new file mode 100644 index 0000000..fe07063 --- /dev/null +++ b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/libs/lib_crop/src/main/res/values-ar/strings.xml b/libs/lib_crop/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..b10c50a --- /dev/null +++ b/libs/lib_crop/src/main/res/values-ar/strings.xml @@ -0,0 +1,8 @@ + + جارٍ حفظ الصورة... + يرجى الانتظار... + لا يوجد مصدر صور متاح + + تم + إلغاء + diff --git a/libs/lib_crop/src/main/res/values-v21/colors.xml b/libs/lib_crop/src/main/res/values-v21/colors.xml new file mode 100644 index 0000000..55a5350 --- /dev/null +++ b/libs/lib_crop/src/main/res/values-v21/colors.xml @@ -0,0 +1,5 @@ + + + #aaaaaa + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..0eb32d2 --- /dev/null +++ b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + 正在儲存相片… + 請稍候… + 沒有可用的圖片來源 + + 完成 + 取消 + diff --git a/libs/lib_crop/src/main/res/values/attrs.xml b/libs/lib_crop/src/main/res/values/attrs.xml new file mode 100644 index 0000000..86fed35 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/attrs.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/colors.xml b/libs/lib_crop/src/main/res/values/colors.xml new file mode 100644 index 0000000..455f448 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #f3f3f3 + #666666 + #1a000000 + #77000000 + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/dimens.xml b/libs/lib_crop/src/main/res/values/dimens.xml new file mode 100644 index 0000000..9051a35 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 56dp + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/strings.xml b/libs/lib_crop/src/main/res/values/strings.xml new file mode 100644 index 0000000..7543813 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Saving photo… + Please wait… + No image source available + + Done + Cancel + + diff --git a/libs/lib_crop/src/main/res/values/styles.xml b/libs/lib_crop/src/main/res/values/styles.xml new file mode 100644 index 0000000..2ec6171 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/styles.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_encipher/.gitignore b/libs/lib_encipher/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lib_encipher/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/lib_encipher/build.gradle b/libs/lib_encipher/build.gradle new file mode 100644 index 0000000..59196ae --- /dev/null +++ b/libs/lib_encipher/build.gradle @@ -0,0 +1,15 @@ +apply from : "../lib_standard.gradle" + +android { + namespace 'com.example.lib_encipher' + + sourceSets{ + main{ + jniLibs.srcDirs = ['libs'] + } + } +} + +dependencies { + api "androidx.core:core-ktx:1.9.0" +} \ No newline at end of file diff --git a/libs/lib_encipher/consumer-rules.pro b/libs/lib_encipher/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_encipher/libs/arm64-v8a/libencipher.so b/libs/lib_encipher/libs/arm64-v8a/libencipher.so new file mode 100755 index 0000000000000000000000000000000000000000..017da6653e29b146f624bf127443795bb6c3e5da GIT binary patch literal 131600 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