├── .idea ├── .name ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── vcs.xml ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── propicker ├── .gitignore ├── consumer-rules.pro ├── src │ └── main │ │ ├── res │ │ ├── drawable │ │ │ ├── zoom_seekbar_line_fill.xml │ │ │ ├── circle_green_background.xml │ │ │ ├── circle_transparent.xml │ │ │ ├── ic_baseline_flash_on_24.xml │ │ │ ├── circle_black_background_white_2dp_border.xml │ │ │ ├── ic_baseline_close_24.xml │ │ │ ├── ic_baseline_flash_off_24.xml │ │ │ ├── ic_baseline_flash_auto_24.xml │ │ │ ├── ic_baseline_check_24.xml │ │ │ ├── ic_baseline_white_close_24.xml │ │ │ ├── ic_baseline_photo_camera_24.xml │ │ │ └── ic_baseline_flip_camera_android_24.xml │ │ ├── values │ │ │ ├── styles.xml │ │ │ └── colors.xml │ │ └── layout │ │ │ ├── activity_pro_image_picker.xml │ │ │ ├── fragment_image_provider.xml │ │ │ ├── dialog_progress.xml │ │ │ ├── dialog_image_picker_chooser.xml │ │ │ └── camera_controller_ui.xml │ │ ├── java │ │ └── com │ │ │ └── shaon2016 │ │ │ └── propicker │ │ │ ├── pro_image_picker │ │ │ ├── model │ │ │ │ ├── ImageProvider.kt │ │ │ │ └── Picker.kt │ │ │ ├── ProviderHelper.kt │ │ │ ├── ui │ │ │ │ ├── ProPickerActivity.kt │ │ │ │ └── ImageProviderFragment.kt │ │ │ └── ProPicker.kt │ │ │ └── util │ │ │ ├── DateUtil.kt │ │ │ ├── D.kt │ │ │ ├── FileUriUtils.kt │ │ │ ├── FileUtil.kt │ │ │ └── TouchImageView.kt │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle ├── propicker.jks ├── settings.gradle ├── screenshot ├── image1.jpeg └── image2.jpeg ├── app ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── styles.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 │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── layout │ │ │ │ ├── activity_main.xml │ │ │ │ └── activity_main2.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── shaon2016 │ │ │ └── propickersample │ │ │ ├── JavaMainActivityExample.java │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── shaon2016 │ │ │ └── propickersample │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── shaon2016 │ │ └── propickersample │ │ └── ExampleInstrumentedTest.kt ├── release │ └── output-metadata.json ├── proguard-rules.pro ├── build.gradle └── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .gitignore ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /.idea/.name: -------------------------------------------------------------------------------- 1 | ProPickerSample -------------------------------------------------------------------------------- /propicker/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /propicker/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /propicker.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/propicker.jks -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':propicker' 2 | include ':app' 3 | rootProject.name = "ProPickerSample" -------------------------------------------------------------------------------- /screenshot/image1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/screenshot/image1.jpeg -------------------------------------------------------------------------------- /screenshot/image2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/screenshot/image2.jpeg -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ProPickerSample 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shaon2016/ProPicker/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Feb 13 22:22:13 BDT 2021 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-7.2-bin.zip 7 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/zoom_seekbar_line_fill.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/model/ImageProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/21 5 | */ 6 | 7 | package com.shaon2016.propicker.pro_image_picker.model 8 | 9 | internal enum class ImageProvider { 10 | GALLERY, 11 | CAMERA, 12 | BOTH 13 | } -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/model/Picker.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propicker.pro_image_picker.model 2 | 3 | import android.net.Uri 4 | import android.os.Parcelable 5 | import kotlinx.android.parcel.Parcelize 6 | import java.io.File 7 | 8 | @Parcelize 9 | data class Picker(val name: String, val uri: Uri, val file: File) : Parcelable -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/circle_green_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/circle_transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_flash_on_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /propicker/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/circle_black_background_white_2dp_border.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_flash_off_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_flash_auto_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/test/java/com/shaon2016/propickersample/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propickersample 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 | } -------------------------------------------------------------------------------- /propicker/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | #673AB7 10 | #512DA8 11 | #009688 12 | 13 | #89000000 14 | #008000 15 | 16 | -------------------------------------------------------------------------------- /app/release/output-metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "artifactType": { 4 | "type": "APK", 5 | "kind": "Directory" 6 | }, 7 | "applicationId": "com.shaon2016.propickersample", 8 | "variantName": "release", 9 | "elements": [ 10 | { 11 | "type": "SINGLE", 12 | "filters": [], 13 | "attributes": [], 14 | "versionCode": 1, 15 | "versionName": "1.0", 16 | "outputFile": "app-release.apk" 17 | } 18 | ], 19 | "elementType": "File" 20 | } -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_check_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/util/DateUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/22 5 | */ 6 | 7 | package com.shaon2016.propicker.util 8 | 9 | 10 | import java.lang.Exception 11 | import java.text.SimpleDateFormat 12 | import java.util.* 13 | 14 | fun Date.formattedDate(toFormat: String = "yyyy-mm-dd"): String { 15 | return try { 16 | SimpleDateFormat(toFormat, Locale.ENGLISH).format(this) 17 | } catch (e: Exception) { 18 | e.printStackTrace() 19 | "" 20 | } 21 | } -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_white_close_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /propicker/src/main/res/layout/activity_pro_image_picker.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/util/D.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propicker.util 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.view.View 6 | import com.shaon2016.propicker.R 7 | import kotlinx.android.synthetic.main.dialog_progress.view.* 8 | 9 | object D { 10 | fun showProgressDialog( 11 | context: Context, 12 | msg: String, 13 | isCancelable: Boolean = false 14 | ): Dialog { 15 | 16 | val v = View.inflate(context, R.layout.dialog_progress, null) 17 | 18 | val d = Dialog(context) 19 | d.setContentView(v) 20 | d.setCancelable(isCancelable) 21 | 22 | v.tvMsg.text = msg 23 | 24 | return d 25 | } 26 | } -------------------------------------------------------------------------------- /propicker/src/main/res/layout/fragment_image_provider.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 12 | 16 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_photo_camera_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /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/com/shaon2016/propickersample/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propickersample 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("com.shaon2016.propickersample", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /propicker/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 -------------------------------------------------------------------------------- /propicker/src/main/res/drawable/ic_baseline_flip_camera_android_24.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /propicker/src/main/res/layout/dialog_progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | 19 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /propicker/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 18 | 23 | 24 | -------------------------------------------------------------------------------- /propicker/src/main/res/layout/dialog_image_picker_chooser.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 16 | 17 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/src/main/java/com/shaon2016/propickersample/JavaMainActivityExample.java: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propickersample; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | 5 | import android.os.Bundle; 6 | import android.view.View; 7 | import android.widget.ImageView; 8 | 9 | import com.shaon2016.propicker.pro_image_picker.ProPicker; 10 | 11 | public class JavaMainActivityExample extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_main2); 17 | 18 | findViewById(R.id.btnChooser).setOnClickListener(v -> { 19 | ProPicker.with(this).start((integer, intent) -> { 20 | 21 | ImageView iv = findViewById(R.id.iv); 22 | iv.setImageURI(ProPicker.getPickerData(intent).getUri()); 23 | return null; 24 | }); 25 | }); 26 | 27 | /*ProPicker.with(this) 28 | .compressImage() 29 | .cameraOnly() 30 | .start((integer, intent) -> { 31 | 32 | ImageView iv = findViewById(R.id.iv); 33 | iv.setImageURI(ProPicker.getPickerData(intent).getUri()); 34 | return null; 35 | });*/ 36 | } 37 | 38 | 39 | } -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 32 8 | 9 | defaultConfig { 10 | applicationId "com.shaon2016.propickersample" 11 | minSdkVersion 21 12 | targetSdkVersion 32 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(dir: "libs", include: ["*.jar"]) 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | implementation 'androidx.appcompat:appcompat:1.4.1' 36 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 37 | 38 | implementation project(':propicker') 39 | 40 | implementation 'com.github.bumptech.glide:glide:4.12.0' 41 | kapt 'com.github.bumptech.glide:compiler:4.11.0' 42 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | # Android Studio 3 in .gitignore file. 48 | .idea/caches 49 | .idea/modules.xml 50 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 51 | .idea/navEditor.xml 52 | 53 | # Keystore files 54 | # Uncomment the following lines if you do not want to check your keystore files in. 55 | #*.jks 56 | #*.keystore 57 | 58 | # External native build folder generated in Android Studio 2.2 and later 59 | .externalNativeBuild 60 | .cxx/ 61 | 62 | # Google Services (e.g. APIs or Firebase) 63 | # google-services.json 64 | 65 | # Freeline 66 | freeline.py 67 | freeline/ 68 | freeline_project_description.json 69 | 70 | # fastlane 71 | fastlane/report.xml 72 | fastlane/Preview.html 73 | fastlane/screenshots 74 | fastlane/test_output 75 | fastlane/readme.md 76 | 77 | # Version control 78 | vcs.xml 79 | 80 | # lint 81 | lint/intermediates/ 82 | lint/generated/ 83 | lint/outputs/ 84 | lint/tmp/ 85 | # lint/reports/ 86 | 87 | # Android Profiling 88 | *.hprof -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof -------------------------------------------------------------------------------- /propicker/src/main/res/layout/camera_controller_ui.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 24 | 25 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /propicker/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | android { 7 | compileSdkVersion 32 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 19 12 | targetSdkVersion 32 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | kotlinOptions { 26 | jvmTarget = "1.8" 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation fileTree(dir: "libs", include: ["*.jar"]) 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.appcompat:appcompat:1.4.1' 39 | 40 | // Inline activity result 41 | implementation 'com.github.florent37:inline-activity-result-kotlin:1.0.4' 42 | // For lifecyclescope 43 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 44 | 45 | implementation 'androidx.activity:activity-ktx:1.6.0-alpha01' 46 | implementation 'androidx.fragment:fragment-ktx:1.5.0-alpha04' 47 | // implementation 'androidx.legacy:legacy-support-v4:1.0.0' 48 | 49 | // CameraX Library 50 | def camerax_version = "1.1.0-beta02" 51 | // CameraX core library using camera2 implementation 52 | implementation "androidx.camera:camera-camera2:$camerax_version" 53 | // CameraX Lifecycle Library 54 | implementation "androidx.camera:camera-lifecycle:$camerax_version" 55 | // CameraX View class 56 | implementation "androidx.camera:camera-view:$camerax_version" 57 | 58 | // Ucrop 59 | implementation 'com.github.yalantis:ucrop:2.2.6' 60 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 29 | 30 | 39 | 40 | 49 | 50 | 56 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main2.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 19 | 20 | 29 | 30 | 39 | 40 | 49 | 50 | 57 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/java/com/shaon2016/propickersample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propickersample 2 | 3 | import android.net.Uri 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.widget.ImageView 8 | import android.widget.TextView 9 | import com.bumptech.glide.Glide 10 | import com.shaon2016.propicker.pro_image_picker.ProPicker 11 | 12 | class MainActivity : AppCompatActivity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_main) 17 | 18 | 19 | findViewById(R.id.btnChooser).setOnClickListener { 20 | ProPicker.with(this) 21 | .start { resultCode, data -> 22 | if (resultCode == RESULT_OK && data != null) { 23 | val l = ProPicker.getPickerData(data) 24 | 25 | findViewById(R.id.iv).setImageURI(l?.uri) 26 | } 27 | } 28 | } 29 | 30 | findViewById(R.id.btnGallery).setOnClickListener { 31 | ProPicker.with(this) 32 | .galleryOnly() 33 | .compressImage() 34 | .start { resultCode, data -> 35 | if (resultCode == RESULT_OK && data != null) { 36 | val list = ProPicker.getSelectedPickerDatas(data) 37 | if (list.size > 0) { 38 | Glide.with(this) 39 | .load(list[0].file) 40 | .into(findViewById(R.id.iv)) 41 | } 42 | 43 | } 44 | } 45 | } 46 | 47 | findViewById(R.id.btnShowCameraOnlyWithCrop).setOnClickListener { 48 | ProPicker.with(this) 49 | .cameraOnly() 50 | .crop() 51 | .start { resultCode, data -> 52 | if (resultCode == RESULT_OK && data != null) { 53 | val picker = ProPicker.getPickerData(data) 54 | 55 | findViewById(R.id.iv).setImageURI(picker?.uri) 56 | 57 | } 58 | } 59 | } 60 | findViewById(R.id.btnShowCameraOnlyCompress).setOnClickListener { 61 | ProPicker.with(this) 62 | .cameraOnly() 63 | .compressImage() 64 | .start { resultCode, data -> 65 | if (resultCode == RESULT_OK && data != null) { 66 | 67 | findViewById(R.id.iv).setImageURI(ProPicker.getPickerData(data)?.uri) 68 | 69 | } 70 | } 71 | } 72 | 73 | } 74 | 75 | 76 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProPicker 2 | 3 | A simple library to select images from the gallery and camera. 4 | 5 | There are many libraries out there. May be some serves your purposes but not satisfactory. This library is different from the others. 6 | 7 | Why should you use it? 8 | 9 | * CameraX library to capture images. 10 | * It also uses UCrop library to crop images. 11 | * It uses best compression to compress your image without loosing image's quality. 12 | 13 | 14 | Step 1. Add the JitPack repository to your build file 15 | 16 | ``` 17 | allprojects { 18 | repositories { 19 | maven { url "https://jitpack.io" } 20 | } 21 | } 22 | ``` 23 | 24 | Step 2. Add the dependency 25 | 26 | ``` 27 | dependencies { 28 | implementation 'com.github.shaon2016:ProPicker:1.0.5' 29 | } 30 | 31 | ``` 32 | 33 | # To working with this library you have to do the below work....... 34 | 35 | 36 | Add this in your build.gradle app module 37 | 38 | ``` 39 | android { 40 | 41 | //......... 42 | 43 | compileOptions { 44 | sourceCompatibility JavaVersion.VERSION_1_8 45 | targetCompatibility JavaVersion.VERSION_1_8 46 | } 47 | 48 | } 49 | ``` 50 | 51 | # Screenshot 52 | 53 | 54 | ![](screenshot/image1.jpeg) ![](screenshot/image2.jpeg) 55 | 56 | ## Start Pro image picker activity 57 | 58 | The simplest way to start 59 | 60 | ``` 61 | ProPicker.with(this) 62 | .start { resultCode, data -> 63 | if (resultCode == RESULT_OK && data != null) { 64 | val list = ProPicker.getSelectedPickerDatas(data) 65 | 66 | if (list.size > 0) { 67 | iv.setImageURI(list[0].uri) 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | What you can do with ImagePicker 74 | 75 | Camera 76 | 77 | ``` 78 | ProPicker.with(this) 79 | .cameraOnly() 80 | .crop() 81 | .start { resultCode, data -> 82 | if (resultCode == RESULT_OK && data != null) { 83 | val picker = ProPicker.getPickerData(data) 84 | 85 | iv.setImageURI(picker?.uri) 86 | 87 | } 88 | } 89 | ``` 90 | 91 | Gallery 92 | 93 | ``` 94 | ProPicker.with(this) 95 | .galleryOnly() 96 | .start { resultCode, data -> 97 | if (resultCode == RESULT_OK && data != null) { 98 | val list = ProPicker.getSelectedPickerDatas(data) 99 | if (list.size > 0) { 100 | Glide.with(this) 101 | .load(list[0].file) 102 | .into(iv) 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | # There are some example added for Java. Check it out at JavaMainActivityExample 109 | 110 | 111 | ##### Function that offers this library 112 | 113 | ## For Camera 114 | 115 | 1. cameraOnly() -> To open the CameraX only 116 | 3. crop() -> Only works with camera 117 | 3. compressImage -> compresing image work for both gallery and camera 118 | 119 | 120 | ## Gallery related function 121 | 4. galleryOnly() -> To open the gallery view only 122 | 5. singleSelection -> Pick single file 123 | 6. multiSelection -> Pick multi file and get the result as ArrayList 124 | 7. maxResultSize -> Max Width and Height of final image 125 | 8. compressImage -> compresing image work for both gallery and camera 126 | 9. compressVideo -> (Under Development) 127 | 10. onlyImage -> Select image from gallery 128 | 11. onlyVideo -> Select video from gallery 129 | 130 | ## Receiver the result 131 | 132 | 12. ProPicker.getPickerDataAsByteArray(this, intent) -> Returns all the data as ByteArray 133 | 13. ProPicker.getSelectedPickerDatas(intent: Intent) -> Get all the data 134 | 14. ProPicker.getPickerData(intent: Intent) -> Get single data 135 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 20 | 22 | 23 | 24 | 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 | 137 |
138 |
-------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/ProviderHelper.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propicker.pro_image_picker 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.appcompat.app.AppCompatActivity 7 | import com.shaon2016.propicker.pro_image_picker.model.Picker 8 | import com.shaon2016.propicker.util.FileUriUtils 9 | import com.shaon2016.propicker.util.FileUtil 10 | import com.yalantis.ucrop.UCrop 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.withContext 13 | import java.io.File 14 | 15 | class ProviderHelper(private val activity: AppCompatActivity) { 16 | 17 | /** 18 | * How many image user can pick 19 | * */ 20 | private val isMultiSelection: Boolean 21 | private val isCropEnabled: Boolean 22 | private val isToCompress: Boolean 23 | 24 | // Ucrop & compress 25 | private val mMaxWidth: Int 26 | private val mMaxHeight: Int 27 | private val mCropAspectX: Float 28 | private val mCropAspectY: Float 29 | private val mGalleryMimeTypes: Array 30 | 31 | init { 32 | val bundle = activity.intent.extras!! 33 | 34 | isMultiSelection = bundle.getBoolean(ProPicker.EXTRA_MULTI_SELECTION, false) 35 | 36 | // Cropping 37 | isCropEnabled = bundle.getBoolean(ProPicker.EXTRA_CROP, false) 38 | isToCompress = bundle.getBoolean(ProPicker.EXTRA_IS_TO_COMPRESS, false) 39 | 40 | // Get Max Width/Height parameter from Intent 41 | mMaxWidth = bundle.getInt(ProPicker.EXTRA_MAX_WIDTH, 0) 42 | mMaxHeight = bundle.getInt(ProPicker.EXTRA_MAX_HEIGHT, 0) 43 | 44 | // Get Crop Aspect Ratio parameter from Intent 45 | mCropAspectX = bundle.getFloat(ProPicker.EXTRA_CROP_X, 0f) 46 | mCropAspectY = bundle.getFloat(ProPicker.EXTRA_CROP_Y, 0f) 47 | 48 | mGalleryMimeTypes = bundle.getStringArray(ProPicker.EXTRA_MIME_TYPES) as Array 49 | 50 | 51 | } 52 | 53 | fun isToCompress() = isToCompress 54 | 55 | fun getGalleryMimeTypes() = mGalleryMimeTypes 56 | 57 | fun getMultiSelection() = isMultiSelection 58 | 59 | fun setResultAndFinish(images: ArrayList?) { 60 | val i = Intent().apply { 61 | putParcelableArrayListExtra(ProPicker.EXTRA_SELECTED_IMAGES, images) 62 | } 63 | activity.setResult(Activity.RESULT_OK, i) 64 | activity.finish() 65 | } 66 | 67 | private suspend fun prepareImage(uri: Uri) = withContext(Dispatchers.IO) { 68 | return@withContext if (isToCompress) { 69 | 70 | val file = FileUtil.compressImage( 71 | activity.baseContext, 72 | uri, 73 | mMaxWidth.toFloat(), 74 | mMaxHeight.toFloat() 75 | ) 76 | 77 | val name = file.name 78 | Picker(name, Uri.fromFile(file), file) 79 | } else { 80 | val file = File(FileUriUtils.getRealPath(activity.baseContext, uri) ?: "") 81 | val name = file.name 82 | Picker(name, uri, file) 83 | } 84 | } 85 | 86 | suspend fun performGalleryOperationForSingleSelection(uri: Uri): ArrayList { 87 | val image = prepareImage(uri) 88 | val images = ArrayList() 89 | images.add(image) 90 | // setResultAndFinish(images) 91 | return images 92 | } 93 | 94 | suspend fun performGalleryOperationForMultipleSelection(uris: List): ArrayList { 95 | val images = ArrayList() 96 | 97 | uris.forEach { uri -> 98 | val image = prepareImage(uri) 99 | images.add(image) 100 | } 101 | //setResultAndFinish(images) 102 | return images 103 | } 104 | 105 | suspend fun performCameraOperation(savedUri: Uri) { 106 | when { 107 | isCropEnabled -> { 108 | val croppedFile = FileUtil.getImageOutputDirectory(activity.baseContext) 109 | startCrop(savedUri, Uri.fromFile(croppedFile)) 110 | } 111 | else -> { 112 | val image = prepareImage(savedUri) 113 | val images = ArrayList() 114 | images.add(image) 115 | 116 | // if compress is true then delete the saved image 117 | if (isToCompress) delete(savedUri) 118 | 119 | setResultAndFinish(images) 120 | } 121 | } 122 | } 123 | 124 | private fun startCrop(sourceUri: Uri, croppedUri: Uri) { 125 | val uCrop = UCrop.of(sourceUri, croppedUri) 126 | 127 | if (mCropAspectX > 0 && mCropAspectY > 0) { 128 | uCrop.withAspectRatio(mCropAspectX, mCropAspectY) 129 | } 130 | 131 | if (mMaxWidth > 0 && mMaxHeight > 0) { 132 | uCrop.withMaxResultSize(mMaxWidth, mMaxHeight) 133 | } 134 | 135 | uCrop.start(activity, UCrop.REQUEST_CROP) 136 | } 137 | 138 | private suspend fun delete(uri: Uri) { 139 | FileUtil.delete(File(uri.path)) 140 | } 141 | 142 | suspend fun handleUCropResult( 143 | requestCode: Int, 144 | resultCode: Int, 145 | data: Intent?, 146 | captureImageUri: Uri? 147 | ) { 148 | 149 | if (resultCode == Activity.RESULT_OK && requestCode == UCrop.REQUEST_CROP && data != null) { 150 | // Deleting Captured image 151 | captureImageUri?.let { 152 | delete(it) 153 | } 154 | // Getting the cropped image 155 | val resultUri = UCrop.getOutput(data) 156 | 157 | val image = prepareImage(resultUri!!) 158 | val images = ArrayList() 159 | images.add(image) 160 | setResultAndFinish(images) 161 | 162 | } else if (resultCode == UCrop.RESULT_ERROR) { 163 | val cropError = UCrop.getError(data!!) 164 | setResultAndFinish(null) 165 | } 166 | } 167 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/ui/ProPickerActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/22 5 | */ 6 | 7 | package com.shaon2016.propicker.pro_image_picker.ui 8 | 9 | 10 | import android.Manifest 11 | import android.content.Intent 12 | import android.content.pm.PackageManager 13 | import android.net.Uri 14 | import android.os.Build 15 | import android.os.Bundle 16 | import android.provider.Settings 17 | import androidx.activity.result.contract.ActivityResultContracts 18 | import androidx.appcompat.app.AlertDialog 19 | import androidx.appcompat.app.AppCompatActivity 20 | import androidx.core.app.ActivityCompat 21 | import androidx.core.content.ContextCompat 22 | import androidx.fragment.app.Fragment 23 | import androidx.lifecycle.lifecycleScope 24 | import com.shaon2016.propicker.R 25 | import com.shaon2016.propicker.pro_image_picker.ProPicker 26 | import com.shaon2016.propicker.pro_image_picker.ProviderHelper 27 | import com.shaon2016.propicker.pro_image_picker.model.ImageProvider 28 | import com.shaon2016.propicker.util.D 29 | import kotlinx.coroutines.launch 30 | 31 | /** The request code for requesting [Manifest.permission.READ_EXTERNAL_STORAGE] permission. */ 32 | private const val PERMISSIONS_REQUEST = 0x1045 33 | 34 | internal class ProPickerActivity : AppCompatActivity() { 35 | private val providerHelper by lazy { ProviderHelper(this) } 36 | private lateinit var imageProvider: ImageProvider 37 | 38 | override fun onCreate(savedInstanceState: Bundle?) { 39 | super.onCreate(savedInstanceState) 40 | setContentView(R.layout.activity_pro_image_picker) 41 | 42 | imageProvider = 43 | intent?.extras?.getSerializable(ProPicker.EXTRA_IMAGE_PROVIDER) as ImageProvider 44 | 45 | loadProvider(imageProvider) 46 | 47 | } 48 | 49 | private fun loadProvider(provider: ImageProvider) { 50 | when (provider) { 51 | ImageProvider.GALLERY -> { 52 | prepareGallery() 53 | } 54 | ImageProvider.CAMERA -> { 55 | if (havePermission()) { 56 | replaceFragment(ImageProviderFragment.newInstance()) 57 | } else { 58 | requestPermissions() 59 | } 60 | } 61 | else -> { 62 | finish() 63 | } 64 | } 65 | } 66 | 67 | private fun prepareGallery() { 68 | val d = if (providerHelper.isToCompress()) 69 | D.showProgressDialog(this, "Compressing....", false) 70 | else D.showProgressDialog(this, "Processing....", false) 71 | 72 | if (!providerHelper.getMultiSelection()) { 73 | // Single choice 74 | registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> 75 | if (uri == null) finish() 76 | uri?.let { 77 | lifecycleScope.launch { 78 | d.show() 79 | val images = providerHelper.performGalleryOperationForSingleSelection(uri) 80 | d.dismiss() 81 | providerHelper.setResultAndFinish(images) 82 | } 83 | } 84 | 85 | }.launch(providerHelper.getGalleryMimeTypes()) 86 | } else { 87 | // Multiple choice 88 | registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> 89 | if (uris == null) finish() 90 | uris?.let { 91 | lifecycleScope.launch { 92 | d.show() 93 | val images = providerHelper.performGalleryOperationForMultipleSelection(uris) 94 | d.dismiss() 95 | providerHelper.setResultAndFinish(images) 96 | } 97 | } 98 | }.launch(providerHelper.getGalleryMimeTypes()) 99 | } 100 | 101 | } 102 | 103 | private fun replaceFragment(fragment: Fragment) { 104 | supportFragmentManager.beginTransaction() 105 | .replace(R.id.container, fragment) 106 | .commit() 107 | } 108 | 109 | // Permission Sections 110 | private fun havePermission() = 111 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 112 | (ContextCompat.checkSelfPermission( 113 | this, 114 | Manifest.permission.READ_EXTERNAL_STORAGE 115 | ) == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission( 116 | this, Manifest.permission.WRITE_EXTERNAL_STORAGE 117 | ) == PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission( 118 | this, Manifest.permission.CAMERA 119 | ) == PackageManager.PERMISSION_GRANTED) 120 | } else { 121 | (ContextCompat.checkSelfPermission( 122 | this, Manifest.permission.CAMERA 123 | ) == PackageManager.PERMISSION_GRANTED) 124 | } 125 | 126 | /** 127 | * Convenience method to request [Manifest.permission.READ_EXTERNAL_STORAGE] permission. 128 | */ 129 | private fun requestPermissions() { 130 | if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { 131 | if (!havePermission()) { 132 | val permissions = arrayOf( 133 | Manifest.permission.READ_EXTERNAL_STORAGE, 134 | Manifest.permission.WRITE_EXTERNAL_STORAGE, 135 | Manifest.permission.CAMERA 136 | ) 137 | ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_REQUEST) 138 | } 139 | } else { 140 | if (!havePermission()) { 141 | val permissions = Manifest.permission.CAMERA 142 | 143 | ActivityCompat.requestPermissions(this, arrayOf(permissions), PERMISSIONS_REQUEST) 144 | } 145 | } 146 | } 147 | 148 | override fun onRequestPermissionsResult( 149 | requestCode: Int, 150 | permissions: Array, 151 | grantResults: IntArray 152 | ) { 153 | 154 | super.onRequestPermissionsResult(requestCode, permissions, grantResults) 155 | 156 | when (requestCode) { 157 | PERMISSIONS_REQUEST -> { 158 | // If request is cancelled, the result arrays are empty. 159 | if (grantResults.isNotEmpty() && havePermission()) { 160 | loadProvider(imageProvider) 161 | } else { 162 | // If we weren't granted the permission, check to see if we should show 163 | // rationale for the permission. 164 | showDialogToAcceptPermissions() 165 | } 166 | return 167 | } 168 | } 169 | 170 | } 171 | 172 | private val startSettingsForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 173 | if (havePermission()) { 174 | replaceFragment(ImageProviderFragment.newInstance()) 175 | } else finish() 176 | } 177 | 178 | private fun goToSettings() { 179 | val intent = 180 | Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.parse("package:$packageName")) 181 | startSettingsForResult.launch(intent) 182 | } 183 | 184 | private fun showDialogToAcceptPermissions() { 185 | showPermissionRationalDialog("You need to allow access to view and capture image") 186 | } 187 | 188 | private fun showPermissionRationalDialog(msg: String) { 189 | AlertDialog.Builder(this) 190 | .setMessage(msg) 191 | .setPositiveButton( 192 | "OK" 193 | ) { dialog, which -> 194 | goToSettings() 195 | } 196 | .setNegativeButton("Cancel") { dialog, which -> 197 | onBackPressed() 198 | } 199 | .create() 200 | .show() 201 | } 202 | 203 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 204 | super.onActivityResult(requestCode, resultCode, data) 205 | 206 | for (fragment in supportFragmentManager.fragments) { 207 | fragment.onActivityResult(requestCode, resultCode, data) 208 | } 209 | } 210 | } -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/util/FileUriUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/22 5 | */ 6 | 7 | /** 8 | * This file was taken from 9 | * https://gist.github.com/HBiSoft/15899990b8cd0723c3a894c1636550a8 10 | * 11 | * Later on it was modified from the below resource: 12 | * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java 13 | * https://raw.githubusercontent.com/iPaulPro/aFileChooser/master/aFileChooser/src/com/ipaulpro/afilechooser/utils/FileUtils.java 14 | */ 15 | 16 | package com.shaon2016.propicker.util 17 | 18 | 19 | import android.content.ContentUris 20 | import android.content.Context 21 | import android.database.Cursor 22 | import android.net.Uri 23 | import android.os.Build 24 | import android.os.Environment 25 | import android.provider.DocumentsContract 26 | import android.provider.MediaStore 27 | import java.io.File 28 | import java.io.FileOutputStream 29 | import java.io.IOException 30 | import java.io.InputStream 31 | import java.io.OutputStream 32 | 33 | object FileUriUtils { 34 | 35 | suspend fun getRealPath(context: Context, uri: Uri): String? { 36 | var path = getPathFromLocalUri(context, uri) 37 | if (path == null) { 38 | path = getPathFromRemoteUri(context, uri) 39 | } 40 | return path 41 | } 42 | 43 | private suspend fun getPathFromLocalUri(context: Context, uri: Uri): String? { 44 | 45 | val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT 46 | 47 | // DocumentProvider 48 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 49 | // ExternalStorageProvider 50 | if (isExternalStorageDocument(uri)) { 51 | val docId = DocumentsContract.getDocumentId(uri) 52 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 53 | val type = split[0] 54 | 55 | // This is for checking Main Memory 56 | return if ("primary".equals(type, ignoreCase = true)) { 57 | if (split.size > 1) { 58 | Environment.getExternalStorageDirectory().toString() + "/" + split[1] 59 | } else { 60 | Environment.getExternalStorageDirectory().toString() + "/" 61 | } 62 | // This is for checking SD Card 63 | } else { 64 | val path = "storage" + "/" + docId.replace(":", "/") 65 | if (File(path).exists()) { 66 | path 67 | } else { 68 | "/storage/sdcard/" + split[1] 69 | } 70 | } 71 | } else if (isDownloadsDocument(uri)) { 72 | val fileName = getFilePath(context, uri) 73 | if (fileName != null) { 74 | return Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName 75 | } 76 | 77 | val id = DocumentsContract.getDocumentId(uri) 78 | val contentUri = ContentUris.withAppendedId( 79 | Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) 80 | ) 81 | return getDataColumn(context, contentUri, null, null) 82 | } else if (isMediaDocument(uri)) { 83 | val docId = DocumentsContract.getDocumentId(uri) 84 | val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() 85 | val type = split[0] 86 | 87 | var contentUri: Uri? = null 88 | if ("image" == type) { 89 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 90 | } else if ("video" == type) { 91 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI 92 | } else if ("audio" == type) { 93 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 94 | } 95 | 96 | val selection = "_id=?" 97 | val selectionArgs = arrayOf(split[1]) 98 | 99 | return getDataColumn(context, contentUri, selection, selectionArgs) 100 | } // MediaProvider 101 | // DownloadsProvider 102 | } else if ("content".equals(uri.scheme!!, ignoreCase = true)) { 103 | 104 | // Return the remote address 105 | return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn(context, uri, null, null) 106 | } else if ("file".equals(uri.scheme!!, ignoreCase = true)) { 107 | return uri.path 108 | } // File 109 | // MediaStore (and general) 110 | 111 | return null 112 | } 113 | 114 | private fun getDataColumn( 115 | context: Context, 116 | uri: Uri?, 117 | selection: String?, 118 | selectionArgs: Array? 119 | ): String? { 120 | 121 | var cursor: Cursor? = null 122 | val column = "_data" 123 | val projection = arrayOf(column) 124 | 125 | try { 126 | cursor = context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) 127 | if (cursor != null && cursor.moveToFirst()) { 128 | val index = cursor.getColumnIndexOrThrow(column) 129 | return cursor.getString(index) 130 | } 131 | } catch (ex: Exception) { 132 | } finally { 133 | cursor?.close() 134 | } 135 | return null 136 | } 137 | 138 | private fun getFilePath(context: Context, uri: Uri): String? { 139 | 140 | var cursor: Cursor? = null 141 | val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME) 142 | 143 | try { 144 | cursor = context.contentResolver.query(uri, projection, null, null, null) 145 | if (cursor != null && cursor.moveToFirst()) { 146 | val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) 147 | return cursor.getString(index) 148 | } 149 | } finally { 150 | cursor?.close() 151 | } 152 | return null 153 | } 154 | 155 | private suspend fun getPathFromRemoteUri(context: Context, uri: Uri): String? { 156 | // The code below is why Java now has try-with-resources and the Files utility. 157 | var file: File? = null 158 | var inputStream: InputStream? = null 159 | var outputStream: OutputStream? = null 160 | var success = false 161 | try { 162 | inputStream = context.contentResolver.openInputStream(uri) 163 | file = FileUtil.getImageOutputDirectory(context) 164 | if (file == null) return null 165 | outputStream = FileOutputStream(file) 166 | if (inputStream != null) { 167 | inputStream.copyTo(outputStream, bufferSize = 4 * 1024) 168 | success = true 169 | } 170 | } catch (ignored: IOException) { 171 | } finally { 172 | try { 173 | inputStream?.close() 174 | } catch (ignored: IOException) { 175 | } 176 | 177 | try { 178 | outputStream?.close() 179 | } catch (ignored: IOException) { 180 | // If closing the output stream fails, we cannot be sure that the 181 | // target file was written in full. Flushing the stream merely moves 182 | // the bytes into the OS, not necessarily to the file. 183 | success = false 184 | } 185 | } 186 | return if (success) file!!.path else null 187 | } 188 | 189 | 190 | /** 191 | * @param uri The Uri to check. 192 | * @return Whether the Uri authority is ExternalStorageProvider. 193 | */ 194 | private fun isExternalStorageDocument(uri: Uri): Boolean { 195 | return "com.android.externalstorage.documents" == uri.authority 196 | } 197 | 198 | /** 199 | * @param uri The Uri to check. 200 | * @return Whether the Uri authority is DownloadsProvider. 201 | */ 202 | private fun isDownloadsDocument(uri: Uri): Boolean { 203 | return "com.android.providers.downloads.documents" == uri.authority 204 | } 205 | 206 | /** 207 | * @param uri The Uri to check. 208 | * @return Whether the Uri authority is MediaProvider. 209 | */ 210 | private fun isMediaDocument(uri: Uri): Boolean { 211 | return "com.android.providers.media.documents" == uri.authority 212 | } 213 | 214 | /** 215 | * @param uri The Uri to check. 216 | * @return Whether the Uri authority is Google Photos. 217 | */ 218 | private fun isGooglePhotosUri(uri: Uri): Boolean { 219 | return "com.google.android.apps.photos.content" == uri.authority 220 | } 221 | } -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/util/FileUtil.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/22 5 | */ 6 | 7 | package com.shaon2016.propicker.util 8 | 9 | 10 | import android.content.Context 11 | import android.graphics.* 12 | import android.media.ExifInterface 13 | import android.net.Uri 14 | import android.os.Environment 15 | import android.webkit.MimeTypeMap 16 | import java.io.* 17 | import java.text.SimpleDateFormat 18 | import java.util.* 19 | import kotlin.jvm.Throws 20 | 21 | 22 | /** 23 | * File Utility Methods 24 | * 25 | * @author Dhaval Patel 26 | * @version 1.0 27 | * @since 04 January 2019 28 | */ 29 | object FileUtil { 30 | private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" 31 | 32 | suspend fun fileFromContentUri(context: Context, contentUri: Uri): File { 33 | // Preparing Temp file name 34 | val fileExtension = getFileExtension(context, contentUri) 35 | val fileName = "temp_file" + if (fileExtension != null) ".$fileExtension" else "" 36 | 37 | // Creating Temp file 38 | val tempFile = File(context.cacheDir, fileName) 39 | tempFile.createNewFile() 40 | 41 | try { 42 | val oStream = FileOutputStream(tempFile) 43 | val inputStream = context.contentResolver.openInputStream(contentUri) 44 | 45 | inputStream?.let { 46 | copy(inputStream, oStream) 47 | } 48 | 49 | oStream.flush() 50 | } catch (e: Exception) { 51 | e.printStackTrace() 52 | } 53 | 54 | return tempFile 55 | } 56 | 57 | suspend fun getFileExtension(context: Context, uri: Uri): String? { 58 | val fileType: String? = context.contentResolver.getType(uri) 59 | return MimeTypeMap.getSingleton().getExtensionFromMimeType(fileType) 60 | } 61 | 62 | suspend fun getFileExtension(fileName: String): String? { 63 | return fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length) 64 | } 65 | 66 | @Throws(IOException::class) 67 | suspend fun copy(source: InputStream, target: OutputStream) { 68 | val buf = ByteArray(8192) 69 | var length: Int 70 | while (source.read(buf).also { length = it } > 0) { 71 | target.write(buf, 0, length) 72 | } 73 | } 74 | 75 | // Method to save an bitmap to a file 76 | suspend fun bitmapToFile(context: Context, bitmap: Bitmap): File { 77 | val file = getImageOutputDirectory(context) 78 | 79 | try { 80 | // Compress the bitmap and save in png format 81 | val stream: OutputStream = FileOutputStream(file) 82 | bitmap.compress(Bitmap.CompressFormat.PNG, 90, stream) 83 | stream.flush() 84 | stream.close() 85 | } catch (e: IOException) { 86 | e.printStackTrace() 87 | } 88 | 89 | // Return the saved bitmap uri 90 | return file 91 | } 92 | 93 | /** 94 | * It creates a image file with png extension 95 | * */ 96 | fun getImageOutputDirectory(context: Context): File { 97 | 98 | val mediaDir = 99 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { 100 | context.getExternalFilesDirs(Environment.DIRECTORY_DCIM).firstOrNull()?.let { 101 | File(it, "images").apply { mkdirs() } 102 | } 103 | } else { 104 | null 105 | } 106 | return if (mediaDir != null && mediaDir.exists()) 107 | File( 108 | mediaDir, 109 | SimpleDateFormat( 110 | FILENAME_FORMAT, Locale.US 111 | ).format(System.currentTimeMillis()) + ".png" 112 | ) 113 | else File( 114 | context.filesDir, 115 | SimpleDateFormat( 116 | FILENAME_FORMAT, Locale.US 117 | ).format(System.currentTimeMillis()) + ".png" 118 | ) 119 | } 120 | 121 | 122 | suspend fun delete(file: File) { 123 | if (file.exists()) file.delete() 124 | } 125 | 126 | 127 | /** 128 | * It compresses the image maintaining the ratio 129 | * */ 130 | suspend fun compressImage( 131 | context: Context, 132 | uri: Uri, 133 | maxWidth: Float, 134 | maxHeight: Float 135 | ): File { 136 | 137 | val filePath: String = FileUriUtils.getRealPath(context, uri)!! 138 | var scaledBitmap: Bitmap? = null 139 | val options = BitmapFactory.Options() 140 | 141 | // by setting this field as true, the actual bitmap pixels are not loaded in the memory. Just the bounds are loaded. If 142 | // you try the use the bitmap here, you will get null. 143 | options.inJustDecodeBounds = true 144 | var bmp = BitmapFactory.decodeFile(filePath, options) 145 | var actualHeight = options.outHeight 146 | var actualWidth = options.outWidth 147 | 148 | var imgRatio = actualWidth / actualHeight.toFloat() 149 | val maxRatio = maxWidth / maxHeight 150 | 151 | // width and height values are set maintaining the aspect ratio of the image 152 | if (actualHeight > maxHeight || actualWidth > maxWidth) { 153 | when { 154 | imgRatio < maxRatio -> { 155 | imgRatio = maxHeight / actualHeight 156 | actualWidth = (imgRatio * actualWidth).toInt() 157 | actualHeight = maxHeight.toInt() 158 | } 159 | imgRatio > maxRatio -> { 160 | imgRatio = maxWidth / actualWidth 161 | actualHeight = (imgRatio * actualHeight).toInt() 162 | actualWidth = maxWidth.toInt() 163 | } 164 | else -> { 165 | actualHeight = maxHeight.toInt() 166 | actualWidth = maxWidth.toInt() 167 | } 168 | } 169 | } 170 | 171 | // setting inSampleSize value allows to load a scaled down version of the original image 172 | options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight) 173 | 174 | // inJustDecodeBounds set to false to load the actual bitmap 175 | options.inJustDecodeBounds = false 176 | 177 | options.inTempStorage = ByteArray(16 * 1024) 178 | try { 179 | // load the bitmap from its path 180 | bmp = BitmapFactory.decodeFile(filePath, options) 181 | } catch (exception: OutOfMemoryError) { 182 | exception.printStackTrace() 183 | } 184 | try { 185 | scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.ARGB_8888) 186 | } catch (exception: OutOfMemoryError) { 187 | exception.printStackTrace() 188 | } 189 | val ratioX = actualWidth / options.outWidth.toFloat() 190 | val ratioY = actualHeight / options.outHeight.toFloat() 191 | val middleX = actualWidth / 2.0f 192 | val middleY = actualHeight / 2.0f 193 | val scaleMatrix = Matrix() 194 | scaleMatrix.setScale(ratioX, ratioY, middleX, middleY) 195 | val canvas = Canvas(scaledBitmap!!) 196 | canvas.setMatrix(scaleMatrix) 197 | canvas.drawBitmap( 198 | bmp, 199 | middleX - bmp.width / 2, 200 | middleY - bmp.height / 2, 201 | Paint(Paint.FILTER_BITMAP_FLAG) 202 | ) 203 | 204 | // check the rotation of the image and display it properly 205 | try { 206 | scaledBitmap = checkRotation(filePath, scaledBitmap, context) 207 | } catch (e: IOException) { 208 | e.printStackTrace() 209 | } 210 | var out: FileOutputStream? = null 211 | val file = getImageOutputDirectory(context) 212 | try { 213 | out = FileOutputStream(file) 214 | 215 | // write the compressed bitmap at the destination specified by filename. 216 | scaledBitmap!!.compress(Bitmap.CompressFormat.PNG, 75, out) 217 | } catch (e: FileNotFoundException) { 218 | e.printStackTrace() 219 | } 220 | return file 221 | } 222 | 223 | /*After getting image check the orientation of the image*/ 224 | @Throws(IOException::class) 225 | private fun checkRotation( 226 | photoPath: String?, 227 | bitmap: Bitmap?, 228 | context: Context 229 | ): Bitmap? { 230 | val ei = ExifInterface(photoPath!!) 231 | val orientation = 232 | ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) 233 | 234 | val pref = context.getSharedPreferences("propicker", Context.MODE_PRIVATE) 235 | val frontCameraVertical = pref.getBoolean("front_camera_vertical", false) 236 | 237 | when (orientation) { 238 | ExifInterface.ORIENTATION_ROTATE_90 -> return rotateImage(bitmap, 90f) 239 | ExifInterface.ORIENTATION_ROTATE_180 -> return rotateImage(bitmap, 180f) 240 | ExifInterface.ORIENTATION_ROTATE_270 -> return rotateImage(bitmap, 270f) 241 | ExifInterface.ORIENTATION_TRANSVERSE -> return if (frontCameraVertical) bitmap?.flip( 242 | horizontal = false, 243 | vertical = true 244 | ) else bitmap?.flip(true, false) 245 | } 246 | return bitmap 247 | } 248 | 249 | /** 250 | * This extension fuction is used to flip the image for front end camera 251 | * */ 252 | private fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap { 253 | val matrix = Matrix() 254 | matrix.preScale((if (horizontal) -1 else 1).toFloat(), (if (vertical) -1 else 1).toFloat()) 255 | 256 | if (vertical) 257 | matrix.postRotate(270f) 258 | else matrix.postRotate(90f) 259 | 260 | return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) 261 | } 262 | 263 | private fun rotateImage(source: Bitmap?, angle: Float): Bitmap { 264 | val matrix = Matrix() 265 | matrix.postRotate(angle) 266 | return Bitmap.createBitmap(source!!, 0, 0, source.width, source.height, matrix, true) 267 | } 268 | 269 | 270 | private fun calculateInSampleSize( 271 | options: BitmapFactory.Options, 272 | reqWidth: Int, 273 | reqHeight: Int 274 | ): Int { 275 | // Raw height and width of image 276 | val height = options.outHeight 277 | val width = options.outWidth 278 | var inSampleSize = 1 279 | if (height > reqHeight || width > reqWidth) { 280 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both 281 | // height and width lower or equal to the requested height and width. 282 | while (height / inSampleSize > reqHeight || width / inSampleSize > reqWidth) { 283 | inSampleSize *= 2 284 | } 285 | } 286 | return inSampleSize 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/ProPicker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/21 5 | */ 6 | 7 | package com.shaon2016.propicker.pro_image_picker 8 | 9 | 10 | import android.app.Activity 11 | import android.app.Dialog 12 | import android.content.Context 13 | import android.content.Intent 14 | import android.net.Uri 15 | import android.os.Bundle 16 | import android.view.View 17 | import android.widget.TextView 18 | import androidx.appcompat.app.AppCompatActivity 19 | import androidx.fragment.app.Fragment 20 | import com.github.florent37.inlineactivityresult.kotlin.startForResult 21 | import com.shaon2016.propicker.R 22 | import com.shaon2016.propicker.pro_image_picker.model.Picker 23 | import com.shaon2016.propicker.pro_image_picker.model.ImageProvider 24 | import com.shaon2016.propicker.pro_image_picker.ui.ProPickerActivity 25 | import com.shaon2016.propicker.util.FileUtil 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.withContext 28 | 29 | object ProPicker { 30 | internal const val EXTRA_MIME_TYPES = "extra.mime_types" 31 | internal const val EXTRA_IMAGE_PROVIDER = "extra.image_provider" 32 | internal const val EXTRA_MULTI_SELECTION = "extra.multi_selection" 33 | internal const val EXTRA_SELECTED_IMAGES = "extra.selected_images" 34 | internal const val EXTRA_IMAGE_MAX_SIZE = "extra.image_max_size" 35 | internal const val EXTRA_CROP = "extra.crop" 36 | internal const val EXTRA_CROP_X = "extra.crop_x" 37 | internal const val EXTRA_CROP_Y = "extra.crop_y" 38 | internal const val EXTRA_MAX_WIDTH = "extra.max_width" 39 | internal const val EXTRA_MAX_HEIGHT = "extra.max_height" 40 | internal const val EXTRA_IS_TO_COMPRESS = "extra._is_to_compress" 41 | 42 | @JvmStatic 43 | fun with(activity: Activity): Builder { 44 | return Builder(activity) 45 | } 46 | 47 | @JvmStatic 48 | fun with(fragment: Fragment): Builder { 49 | return Builder(fragment) 50 | } 51 | 52 | /** 53 | * Get all the selected images 54 | * @param intent 55 | * */ 56 | @JvmStatic 57 | fun getSelectedPickerDatas(intent: Intent) = 58 | intent.getParcelableArrayListExtra(EXTRA_SELECTED_IMAGES) ?: ArrayList() 59 | 60 | /** 61 | * Get all the selected images 62 | * @param intent 63 | * */ 64 | @JvmStatic 65 | fun getPickerData(intent: Intent): Picker? { 66 | val images = getSelectedPickerDatas(intent) 67 | return if (images.isNotEmpty()) images[0] else null 68 | } 69 | 70 | /** 71 | * Get selected images as Byte Array 72 | * */ 73 | @JvmStatic 74 | fun getPickerDataAsByteArray(context: Context, intent: Intent): ArrayList { 75 | val arrays = ArrayList() 76 | 77 | getSelectedPickerDatas(intent).forEach { 78 | val byteArray = context.contentResolver.openInputStream(it.uri)?.readBytes() 79 | byteArray?.let { 80 | arrays.add(byteArray) 81 | } 82 | } 83 | 84 | return arrays 85 | } 86 | 87 | /** 88 | * It copy file to a directory 89 | * */ 90 | // fun getImagesAsFile(context: Context, intent: Intent): ArrayList { 91 | // val files = ArrayList() 92 | // 93 | // (context as AppCompatActivity).lifecycleScope.launch { 94 | // getImages(intent).forEach { 95 | // files.add(getFile(context, it.contentUri)) 96 | // } 97 | // } 98 | // 99 | // return files 100 | // } 101 | 102 | 103 | private suspend fun getFile(context: Context, uri: Uri) = withContext(Dispatchers.IO) { 104 | FileUtil.fileFromContentUri(context, uri) 105 | } 106 | 107 | 108 | class Builder(private val activity: Activity) { 109 | 110 | private var fragment: Fragment? = null 111 | 112 | private var imageProvider = ImageProvider.BOTH 113 | 114 | // Mime types restrictions for gallery. by default all mime types are valid 115 | private var mimeTypes: Array = arrayOf("image/png", "image/jpeg", "image/jpg") 116 | 117 | /* 118 | * Crop Parameters 119 | */ 120 | private var cropX: Float = 0f 121 | private var cropY: Float = 0f 122 | private var crop: Boolean = false 123 | 124 | // Compress 125 | private var isToCompress: Boolean = false 126 | 127 | 128 | /* 129 | * Resize Parameters 130 | */ 131 | private var maxWidth: Int = 0 132 | private var maxHeight: Int = 0 133 | 134 | // Image selection length 135 | private var isMultiSelection = false 136 | 137 | /** 138 | * Max File Size 139 | */ 140 | private var maxSize: Long = 0 141 | 142 | constructor(fragment: Fragment) : this(fragment.requireActivity()) { 143 | this.fragment = fragment 144 | } 145 | 146 | /** 147 | * Only Capture image using Camera. 148 | */ 149 | fun cameraOnly(): Builder { 150 | this.imageProvider = ImageProvider.CAMERA 151 | return this 152 | } 153 | 154 | /** 155 | * Only Pick image from gallery. 156 | */ 157 | fun galleryOnly(): Builder { 158 | this.imageProvider = ImageProvider.GALLERY 159 | return this 160 | } 161 | 162 | /** 163 | * Only pick one image 164 | * */ 165 | fun singleSelection(): Builder { 166 | isMultiSelection = false 167 | return this 168 | } 169 | 170 | /** 171 | * Pick many image. By default user can pick 5 images 172 | * */ 173 | fun multiSelection(): Builder { 174 | isMultiSelection = true 175 | return this 176 | } 177 | 178 | /** 179 | * Set an aspect ratio for crop bounds. 180 | * User won't see the menu with other ratios options. 181 | * 182 | * @param x aspect ratio X 183 | * @param y aspect ratio Y 184 | */ 185 | fun crop(x: Float, y: Float): Builder { 186 | cropX = x 187 | cropY = y 188 | return crop() 189 | } 190 | 191 | /** 192 | * Crop an image and let user set the aspect ratio. 193 | */ 194 | fun crop(): Builder { 195 | this.crop = true 196 | return this 197 | } 198 | 199 | /** 200 | * Crop Square Image, Useful for Profile Image. 201 | * 202 | */ 203 | fun cropSquare(): Builder { 204 | return crop(1f, 1f) 205 | } 206 | 207 | /** 208 | * Max Width and Height of final image 209 | */ 210 | fun maxResultSize(width: Int, height: Int): Builder { 211 | this.maxWidth = width 212 | this.maxHeight = height 213 | return this 214 | } 215 | 216 | /** 217 | * @param maxWidth must be greater than 10 218 | * @param maxHeight must be greater than 10 219 | * */ 220 | @JvmSuppressWildcards 221 | @JvmOverloads 222 | fun compressImage( maxWidth: Int = 612, maxHeight: Int = 816): Builder { 223 | if (maxHeight > 10 && maxWidth > 10) { 224 | this.maxWidth = maxWidth 225 | this.maxHeight = maxHeight 226 | } 227 | 228 | isToCompress = true 229 | return this 230 | } 231 | 232 | // TODO 233 | private fun compressVideo() : Builder { 234 | 235 | return this 236 | } 237 | 238 | 239 | // TODO will implement later 240 | /** 241 | * Restrict mime types during gallery fetching, for instance if you do not want GIF images, 242 | * you can use arrayOf("image/png","image/jpeg","image/jpg") 243 | * by default array is arrayOf("image/png","image/jpeg","image/jpg"), 244 | * @param mimeTypes 245 | */ 246 | fun galleryMimeTypes(mimeTypes: Array): Builder { 247 | this.mimeTypes = mimeTypes 248 | return this 249 | } 250 | 251 | /** 252 | * Select image from gallery 253 | * */ 254 | fun onlyImage(): Builder { 255 | this.mimeTypes = arrayOf("image/*") 256 | return this 257 | } 258 | 259 | /** 260 | * Select video from gallery 261 | * */ 262 | fun onlyVideo(): Builder { 263 | this.mimeTypes = arrayOf("video/*") 264 | return this 265 | } 266 | 267 | 268 | /** 269 | * Start Image Picker Activity 270 | */ 271 | @JvmOverloads 272 | @JvmSuppressWildcards 273 | fun start(completionHandler: ((resultCode: Int, data: Intent?) -> Unit)? = null) { 274 | if (imageProvider == ImageProvider.BOTH) { 275 | // Pick Image Provider if not specified 276 | showImageProviderDialog(completionHandler) 277 | } else { 278 | startActivity(completionHandler) 279 | } 280 | } 281 | 282 | private fun showImageProviderDialog(completionHandler: ((resultCode: Int, data: Intent?) -> Unit)? = null) { 283 | val v = View.inflate(activity.baseContext, R.layout.dialog_image_picker_chooser, null) 284 | 285 | val d = Dialog(activity, R.style.Theme_AppCompat_Dialog_Alert) 286 | d.setContentView(v) 287 | 288 | v.findViewById(R.id.btnCamera).setOnClickListener { 289 | imageProvider = ImageProvider.CAMERA 290 | start(completionHandler) 291 | d.dismiss() 292 | } 293 | 294 | v.findViewById(R.id.btnGallery).setOnClickListener { 295 | imageProvider = ImageProvider.GALLERY 296 | start(completionHandler) 297 | d.dismiss() 298 | } 299 | 300 | d.show() 301 | 302 | } 303 | 304 | /** 305 | * Start ImagePickerActivity with given Argument 306 | */ 307 | private fun startActivity(completionHandler: ((resultCode: Int, data: Intent?) -> Unit)? = null) { 308 | val intent = Intent(activity, ProPickerActivity::class.java) 309 | intent.putExtras(getBundle()) 310 | if (fragment != null) { 311 | 312 | fragment?.startForResult(intent) { result -> 313 | completionHandler?.invoke(result.resultCode, result.data) 314 | }?.onFailed { result -> 315 | completionHandler?.invoke(result.resultCode, result.data) 316 | } 317 | } else { 318 | (activity as AppCompatActivity).startForResult(intent) { result -> 319 | completionHandler?.invoke(result.resultCode, result.data) 320 | }.onFailed { result -> 321 | completionHandler?.invoke(result.resultCode, result.data) 322 | } 323 | } 324 | } 325 | 326 | /** 327 | * Get Bundle for ProImagePickerActivity 328 | */ 329 | private fun getBundle(): Bundle { 330 | return Bundle().apply { 331 | putSerializable(EXTRA_IMAGE_PROVIDER, imageProvider) 332 | putStringArray(EXTRA_MIME_TYPES, mimeTypes) 333 | putBoolean(EXTRA_MULTI_SELECTION, isMultiSelection) 334 | 335 | putBoolean(EXTRA_CROP, crop) 336 | putBoolean(EXTRA_IS_TO_COMPRESS, isToCompress) 337 | 338 | putLong(EXTRA_IMAGE_MAX_SIZE, maxSize) 339 | putFloat(EXTRA_CROP_X, cropX) 340 | putFloat(EXTRA_CROP_Y, cropY) 341 | putInt(EXTRA_MAX_WIDTH, maxWidth) 342 | putInt(EXTRA_MAX_HEIGHT, maxHeight) 343 | 344 | } 345 | } 346 | 347 | } 348 | 349 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/pro_image_picker/ui/ImageProviderFragment.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. 3 | * @author Md Ashiqul Islam 4 | * @since 2020/10/22 5 | */ 6 | 7 | package com.shaon2016.propicker.pro_image_picker.ui 8 | 9 | import android.content.Context 10 | import android.content.Intent 11 | import android.content.res.Configuration 12 | import android.hardware.display.DisplayManager 13 | import android.net.Uri 14 | import android.os.Bundle 15 | import android.util.DisplayMetrics 16 | import android.util.Log 17 | import android.view.* 18 | import android.widget.ImageView 19 | import android.widget.RelativeLayout 20 | import android.widget.SeekBar 21 | import androidx.appcompat.app.AppCompatActivity 22 | import androidx.camera.core.* 23 | import androidx.camera.lifecycle.ProcessCameraProvider 24 | import androidx.camera.view.PreviewView 25 | import androidx.core.content.ContextCompat 26 | import androidx.fragment.app.Fragment 27 | import androidx.lifecycle.Observer 28 | import androidx.lifecycle.lifecycleScope 29 | import com.shaon2016.propicker.R 30 | import com.shaon2016.propicker.pro_image_picker.ProviderHelper 31 | import com.shaon2016.propicker.util.FileUtil 32 | import kotlinx.coroutines.launch 33 | import java.util.concurrent.ExecutorService 34 | import java.util.concurrent.Executors 35 | import kotlin.math.abs 36 | import kotlin.math.max 37 | import kotlin.math.min 38 | 39 | internal class ImageProviderFragment : Fragment() { 40 | private val TAG = "ImageProviderFragment" 41 | private lateinit var container: RelativeLayout 42 | private val providerHelper by lazy { ProviderHelper(requireActivity() as AppCompatActivity) } 43 | 44 | private var captureImageUri: Uri? = null 45 | 46 | private val pref by lazy { 47 | requireContext().getSharedPreferences("propicker", Context.MODE_PRIVATE) 48 | } 49 | 50 | // CameraX 51 | private var imageCapture: ImageCapture? = null 52 | private var lensFacing: Int = CameraSelector.LENS_FACING_BACK 53 | private lateinit var cameraExecutor: ExecutorService 54 | private lateinit var cameraProvider: ProcessCameraProvider 55 | private lateinit var viewFinder: PreviewView 56 | private var displayId: Int = -1 57 | private var camera: Camera? = null 58 | 59 | private val displayManager by lazy { 60 | requireContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 61 | } 62 | 63 | 64 | /** 65 | * We need a display listener for orientation changes that do not trigger a configuration 66 | * change, for example if we choose to override config change in manifest or for 180-degree 67 | * orientation changes. 68 | */ 69 | private val displayListener = object : DisplayManager.DisplayListener { 70 | override fun onDisplayAdded(displayId: Int) = Unit 71 | override fun onDisplayRemoved(displayId: Int) = Unit 72 | override fun onDisplayChanged(displayId: Int) = view?.let { view -> 73 | if (displayId == this@ImageProviderFragment.displayId) { 74 | Log.d(TAG, "Rotation changed: ${view.display.rotation}") 75 | imageCapture?.targetRotation = view.display.rotation 76 | } 77 | } ?: Unit 78 | } 79 | 80 | 81 | override fun onCreateView( 82 | inflater: LayoutInflater, container: ViewGroup?, 83 | savedInstanceState: Bundle? 84 | ): View? { 85 | return inflater.inflate(R.layout.fragment_image_provider, container, false) 86 | } 87 | 88 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 89 | container = view as RelativeLayout 90 | viewFinder = container.findViewById(R.id.viewFinder) 91 | 92 | viewFinder.post { 93 | setupCamera() 94 | 95 | updateCameraUI() 96 | } 97 | 98 | cameraExecutor = Executors.newSingleThreadExecutor() 99 | 100 | // Every time the orientation of device changes, update rotation for use cases 101 | displayManager.registerDisplayListener(displayListener, null) 102 | 103 | } 104 | 105 | override fun onConfigurationChanged(newConfig: Configuration) { 106 | super.onConfigurationChanged(newConfig) 107 | 108 | updateCameraUI() 109 | updateCameraSwitchButton() 110 | } 111 | 112 | private fun updateCameraUI() { 113 | 114 | container.findViewById(R.id.fabCamera).setOnClickListener { 115 | takePhoto() 116 | } 117 | container.findViewById(R.id.flipCamera).setOnClickListener { 118 | flipCamera() 119 | } 120 | 121 | container.findViewById(R.id.zoomSeekBar) 122 | .setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { 123 | override fun onProgressChanged( 124 | seekBar: SeekBar?, 125 | progress: Int, 126 | fromUser: Boolean 127 | ) { 128 | camera!!.cameraControl.setLinearZoom(progress / 100.toFloat()) 129 | } 130 | 131 | override fun onStartTrackingTouch(seekBar: SeekBar?) {} 132 | 133 | override fun onStopTrackingTouch(seekBar: SeekBar?) {} 134 | }) 135 | } 136 | 137 | private fun takePhoto() { 138 | // Get a stable reference of the modifiable image capture use case 139 | val imageCapture = imageCapture ?: return 140 | 141 | // Create time-stamped output file to hold the image 142 | val photoFile = FileUtil.getImageOutputDirectory(requireContext()) 143 | 144 | // Setup image capture metadata 145 | val metadata = ImageCapture.Metadata().apply { 146 | 147 | // Mirror image when using the front camera 148 | isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT 149 | } 150 | // Create output options object which contains file + metadata 151 | val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile) 152 | .setMetadata(metadata) 153 | .build() 154 | 155 | // Set up image capture listener, which is triggered after photo has 156 | // been taken 157 | imageCapture.takePicture( 158 | outputOptions, 159 | ContextCompat.getMainExecutor(requireContext()), 160 | object : ImageCapture.OnImageSavedCallback { 161 | override fun onError(exc: ImageCaptureException) { 162 | Log.e(TAG, "Photo capture failed: ${exc.message}", exc) 163 | } 164 | 165 | override fun onImageSaved(output: ImageCapture.OutputFileResults) { 166 | 167 | captureImageUri = Uri.fromFile(photoFile) 168 | 169 | captureImageUri?.let { 170 | lifecycleScope.launch { 171 | providerHelper.performCameraOperation(captureImageUri!!) 172 | 173 | val msg = "Photo capture succeeded: $captureImageUri" 174 | //Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() 175 | Log.d(TAG, msg) 176 | } 177 | } 178 | } 179 | }) 180 | } 181 | 182 | private fun setupCamera() { 183 | val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) 184 | 185 | cameraProviderFuture.addListener({ 186 | // Used to bind the lifecycle of cameras to the lifecycle owner 187 | cameraProvider = cameraProviderFuture.get() 188 | 189 | // Select lensFacing depending on the available cameras 190 | lensFacing = when { 191 | hasBackCamera() -> CameraSelector.LENS_FACING_BACK 192 | hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT 193 | else -> throw IllegalStateException("Back and front camera are unavailable") 194 | } 195 | 196 | // Enable or disable switching between cameras 197 | updateCameraSwitchButton() 198 | 199 | // Build and bind the camera use cases 200 | bindCameraUseCases() 201 | }, ContextCompat.getMainExecutor(requireContext())) 202 | 203 | val scaleGestureDetector = ScaleGestureDetector(requireContext(), pinchZoomListener) 204 | 205 | viewFinder.setOnTouchListener { _, event -> 206 | scaleGestureDetector.onTouchEvent(event) 207 | return@setOnTouchListener true 208 | } 209 | } 210 | 211 | private val pinchZoomListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() { 212 | override fun onScale(detector: ScaleGestureDetector): Boolean { 213 | val currentZoomRatio = camera!!.cameraInfo.zoomState.value?.zoomRatio ?: 0F 214 | val delta = detector.scaleFactor 215 | camera!!.cameraControl.setZoomRatio(currentZoomRatio * delta) 216 | return true 217 | } 218 | } 219 | 220 | private var orientationEventListener: OrientationEventListener? = null 221 | 222 | private fun bindCameraUseCases() { 223 | // Get screen metrics used to setup camera for full screen resolution 224 | val metrics = DisplayMetrics().also { viewFinder.display.getRealMetrics(it) } 225 | Log.d(TAG, "Screen metrics: ${metrics.widthPixels} x ${metrics.heightPixels}") 226 | 227 | val screenAspectRatio = aspectRatio(metrics.widthPixels, metrics.heightPixels) 228 | Log.d(TAG, "Preview aspect ratio: $screenAspectRatio") 229 | 230 | val rotation = viewFinder.display.rotation 231 | 232 | 233 | // Preview 234 | val preview = Preview.Builder() 235 | // We request aspect ratio but no resolution 236 | .setTargetAspectRatio(screenAspectRatio) 237 | // Set initial target rotation 238 | .setTargetRotation(rotation) 239 | .build().also { 240 | it.setSurfaceProvider(viewFinder.surfaceProvider) 241 | } 242 | 243 | imageCapture = ImageCapture.Builder() 244 | .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) 245 | .setTargetAspectRatio(screenAspectRatio) 246 | .build() 247 | 248 | orientationEventListener = object : OrientationEventListener(requireContext()) { 249 | override fun onOrientationChanged(orientation: Int) { 250 | // Monitors orientation values to determine the target rotation value 251 | val rotation = when (orientation) { 252 | in 45..134 -> Surface.ROTATION_270 253 | in 135..224 -> Surface.ROTATION_180 254 | in 225..314 -> Surface.ROTATION_90 255 | else -> Surface.ROTATION_0 256 | } 257 | 258 | try { 259 | if (lensFacing == CameraSelector.LENS_FACING_FRONT) { 260 | if (rotation == Surface.ROTATION_0) 261 | pref.edit().putBoolean("front_camera_vertical", true).apply() 262 | } 263 | } catch (e: Exception) { 264 | e.printStackTrace() 265 | } 266 | 267 | imageCapture?.targetRotation = rotation 268 | } 269 | } 270 | orientationEventListener?.enable() 271 | 272 | 273 | // Select back camera as a default 274 | val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build() 275 | 276 | try { 277 | // Unbind use cases before rebinding 278 | cameraProvider.unbindAll() 279 | 280 | // Bind use cases to camera 281 | camera = cameraProvider.bindToLifecycle( 282 | this, cameraSelector, preview, imageCapture 283 | ) 284 | 285 | camera?.let { 286 | initFlash() 287 | } 288 | 289 | } catch (exc: Exception) { 290 | Log.e(TAG, "Use case binding failed", exc) 291 | } 292 | } 293 | 294 | /** Returns true if the device has an available back camera. False otherwise */ 295 | private fun hasBackCamera() = cameraProvider.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) 296 | 297 | /** Returns true if the device has an available front camera. False otherwise */ 298 | private fun hasFrontCamera() = cameraProvider.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) 299 | 300 | /** Enabled or disabled a button to switch cameras depending on the available cameras */ 301 | private fun updateCameraSwitchButton() { 302 | val switchCamerasButton = view?.findViewById(R.id.flipCamera) 303 | try { 304 | switchCamerasButton?.isEnabled = hasBackCamera() && hasFrontCamera() 305 | } catch (exception: CameraInfoUnavailableException) { 306 | switchCamerasButton?.isEnabled = false 307 | } 308 | } 309 | 310 | private fun flipCamera() { 311 | lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) { 312 | CameraSelector.LENS_FACING_BACK 313 | } else { 314 | CameraSelector.LENS_FACING_FRONT 315 | } 316 | // Re-bind use cases to update selected camera 317 | bindCameraUseCases() 318 | } 319 | 320 | private fun initFlash() { 321 | val btnFlash = view?.findViewById(R.id.btnFlash) 322 | 323 | if (camera!!.cameraInfo.hasFlashUnit()) { 324 | btnFlash?.visibility = View.VISIBLE 325 | 326 | btnFlash?.setOnClickListener { 327 | camera!!.cameraControl.enableTorch(camera!!.cameraInfo.torchState.value == TorchState.OFF) 328 | } 329 | } else btnFlash?.visibility = View.GONE 330 | 331 | camera!!.cameraInfo.torchState.observe(viewLifecycleOwner, Observer { torchState -> 332 | if (torchState == TorchState.OFF) { 333 | btnFlash?.setImageResource(R.drawable.ic_baseline_flash_on_24) 334 | } else { 335 | btnFlash?.setImageResource(R.drawable.ic_baseline_flash_off_24) 336 | } 337 | }) 338 | } 339 | 340 | /** 341 | * [androidx.camera.core.ImageAnalysisConfig] requires enum value of 342 | * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9. 343 | * 344 | * Detecting the most suitable ratio for dimensions provided in @params by counting absolute 345 | * of preview ratio to one of the provided values. 346 | * 347 | * @param width - preview width 348 | * @param height - preview height 349 | * @return suitable aspect ratio 350 | */ 351 | private fun aspectRatio(width: Int, height: Int): Int { 352 | val previewRatio = max(width, height).toDouble() / min(width, height) 353 | if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) { 354 | return AspectRatio.RATIO_4_3 355 | } 356 | return AspectRatio.RATIO_16_9 357 | } 358 | 359 | // For Ucrop Result 360 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 361 | super.onActivityResult(requestCode, resultCode, data) 362 | 363 | lifecycleScope.launch { 364 | providerHelper.handleUCropResult(requestCode, resultCode, data, captureImageUri) 365 | } 366 | } 367 | 368 | override fun onDestroy() { 369 | super.onDestroy() 370 | cameraExecutor.shutdown() 371 | displayManager.unregisterDisplayListener(displayListener) 372 | 373 | } 374 | 375 | override fun onStop() { 376 | super.onStop() 377 | 378 | orientationEventListener?.disable() 379 | orientationEventListener = null 380 | } 381 | 382 | companion object { 383 | @JvmStatic 384 | fun newInstance() = ImageProviderFragment() 385 | 386 | private const val RATIO_4_3_VALUE = 4.0 / 3.0 387 | private const val RATIO_16_9_VALUE = 16.0 / 9.0 388 | 389 | } 390 | } -------------------------------------------------------------------------------- /propicker/src/main/java/com/shaon2016/propicker/util/TouchImageView.kt: -------------------------------------------------------------------------------- 1 | package com.shaon2016.propicker.util 2 | 3 | import android.annotation.TargetApi 4 | import android.content.Context 5 | import android.content.res.Configuration 6 | import android.graphics.Bitmap 7 | import android.graphics.Canvas 8 | import android.graphics.Matrix 9 | import android.graphics.PointF 10 | import android.graphics.RectF 11 | import android.graphics.drawable.Drawable 12 | import android.net.Uri 13 | import android.os.Build.VERSION 14 | import android.os.Build.VERSION_CODES 15 | import android.os.Bundle 16 | import android.os.Parcelable 17 | import android.util.AttributeSet 18 | import android.view.GestureDetector 19 | import android.view.GestureDetector.OnDoubleTapListener 20 | import android.view.GestureDetector.SimpleOnGestureListener 21 | import android.view.MotionEvent 22 | import android.view.ScaleGestureDetector 23 | import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener 24 | import android.view.View 25 | import android.view.animation.AccelerateDecelerateInterpolator 26 | import android.view.animation.LinearInterpolator 27 | import android.widget.OverScroller 28 | import androidx.appcompat.widget.AppCompatImageView 29 | 30 | @Suppress("unused") 31 | open class TouchImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : AppCompatImageView(context, attrs, defStyle) { 32 | /** 33 | * Get the current zoom. This is the zoom relative to the initial 34 | * scale, not the original resource. 35 | * 36 | * @return current zoom multiplier. 37 | */ 38 | // Scale of image ranges from minScale to maxScale, where minScale == 1 39 | // when the image is stretched to fit view. 40 | var currentZoom = 0f 41 | private set 42 | 43 | // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal. 44 | // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix saved prior to the screen rotating. 45 | private var touchMatrix: Matrix? = null 46 | private var prevMatrix: Matrix? = null 47 | var isZoomEnabled = true 48 | private var isRotateImageToFitScreen = false 49 | 50 | enum class FixedPixel { 51 | CENTER, TOP_LEFT, BOTTOM_RIGHT 52 | } 53 | 54 | var orientationChangeFixedPixel: FixedPixel? = FixedPixel.CENTER 55 | var viewSizeChangeFixedPixel: FixedPixel? = FixedPixel.CENTER 56 | private var orientationJustChanged = false 57 | 58 | private enum class State { 59 | NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM 60 | } 61 | 62 | private var state: State? = null 63 | private var userSpecifiedMinScale = 0f 64 | private var minScale = 0f 65 | private var maxScaleIsSetByMultiplier = false 66 | private var maxScaleMultiplier = 0f 67 | private var maxScale = 0f 68 | private var superMinScale = 0f 69 | private var superMaxScale = 0f 70 | private var floatMatrix: FloatArray? = null 71 | /** 72 | * Get zoom multiplier for double tap 73 | * 74 | * @return double tap zoom multiplier. 75 | */ 76 | /** 77 | * Set custom zoom multiplier for double tap. 78 | * By default maxScale will be used as value for double tap zoom multiplier. 79 | * 80 | */ 81 | var doubleTapScale = 0f 82 | private var fling: Fling? = null 83 | private var orientation = 0 84 | private var touchScaleType: ScaleType? = null 85 | private var imageRenderedAtLeastOnce = false 86 | private var onDrawReady = false 87 | private var delayedZoomVariables: ZoomVariables? = null 88 | 89 | // Size of view and previous view size (ie before rotation) 90 | private var viewWidth = 0 91 | private var viewHeight = 0 92 | private var prevViewWidth = 0 93 | private var prevViewHeight = 0 94 | 95 | // Size of image when it is stretched to fit view. Before and After rotation. 96 | private var matchViewWidth = 0f 97 | private var matchViewHeight = 0f 98 | private var prevMatchViewWidth = 0f 99 | private var prevMatchViewHeight = 0f 100 | private var mScaleDetector: ScaleGestureDetector? = null 101 | private var mGestureDetector: GestureDetector? = null 102 | private var doubleTapListener: OnDoubleTapListener? = null 103 | private var userTouchListener: OnTouchListener? = null 104 | private var touchImageViewListener: OnTouchImageViewListener? = null 105 | 106 | init { 107 | super.setClickable(true) 108 | orientation = resources.configuration.orientation 109 | mScaleDetector = ScaleGestureDetector(context, ScaleListener()) 110 | mGestureDetector = GestureDetector(context, GestureListener()) 111 | touchMatrix = Matrix() 112 | prevMatrix = Matrix() 113 | floatMatrix = FloatArray(9) 114 | currentZoom = 1f 115 | if (touchScaleType == null) { 116 | touchScaleType = ScaleType.FIT_CENTER 117 | } 118 | minScale = 1f 119 | maxScale = 3f 120 | superMinScale = SUPER_MIN_MULTIPLIER * minScale 121 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale 122 | imageMatrix = touchMatrix 123 | scaleType = ScaleType.MATRIX 124 | setState(State.NONE) 125 | onDrawReady = false 126 | super.setOnTouchListener(PrivateOnTouchListener()) 127 | // val attributes = context.theme.obtainStyledAttributes(attrs, R.styleable.TouchImageView, defStyle, 0) 128 | // try { 129 | // if (!isInEditMode) { 130 | // isZoomEnabled = attributes.getBoolean(R.styleable.TouchImageView_zoom_enabled, true) 131 | // } 132 | // } finally { 133 | // // release the TypedArray so that it can be reused. 134 | // attributes.recycle() 135 | // } 136 | } 137 | 138 | fun setRotateImageToFitScreen(rotateImageToFitScreen: Boolean) { 139 | isRotateImageToFitScreen = rotateImageToFitScreen 140 | } 141 | 142 | override fun setOnTouchListener(onTouchListener: OnTouchListener) { 143 | userTouchListener = onTouchListener 144 | } 145 | 146 | fun setOnTouchImageViewListener(onTouchImageViewListener: OnTouchImageViewListener) { 147 | touchImageViewListener = onTouchImageViewListener 148 | } 149 | 150 | fun setOnDoubleTapListener(onDoubleTapListener: OnDoubleTapListener) { 151 | doubleTapListener = onDoubleTapListener 152 | } 153 | 154 | override fun setImageResource(resId: Int) { 155 | imageRenderedAtLeastOnce = false 156 | super.setImageResource(resId) 157 | savePreviousImageValues() 158 | fitImageToView() 159 | } 160 | 161 | override fun setImageBitmap(bm: Bitmap) { 162 | imageRenderedAtLeastOnce = false 163 | super.setImageBitmap(bm) 164 | savePreviousImageValues() 165 | fitImageToView() 166 | } 167 | 168 | override fun setImageDrawable(drawable: Drawable?) { 169 | imageRenderedAtLeastOnce = false 170 | super.setImageDrawable(drawable) 171 | savePreviousImageValues() 172 | fitImageToView() 173 | } 174 | 175 | override fun setImageURI(uri: Uri?) { 176 | imageRenderedAtLeastOnce = false 177 | super.setImageURI(uri) 178 | savePreviousImageValues() 179 | fitImageToView() 180 | } 181 | 182 | override fun setScaleType(type: ScaleType) { 183 | if (type == ScaleType.MATRIX) { 184 | super.setScaleType(ScaleType.MATRIX) 185 | } else { 186 | touchScaleType = type 187 | if (onDrawReady) { 188 | // 189 | // If the image is already rendered, scaleType has been called programmatically 190 | // and the TouchImageView should be updated with the new scaleType. 191 | // 192 | setZoom(this) 193 | } 194 | } 195 | } 196 | 197 | override fun getScaleType(): ScaleType { 198 | return touchScaleType!! 199 | } 200 | 201 | /** 202 | * Returns false if image is in initial, unzoomed state. False, otherwise. 203 | * 204 | * @return true if image is zoomed 205 | */ 206 | val isZoomed: Boolean 207 | get() = currentZoom != 1f 208 | 209 | /** 210 | * Return a Rect representing the zoomed image. 211 | * 212 | * @return rect representing zoomed image 213 | */ 214 | val zoomedRect: RectF 215 | get() { 216 | if (touchScaleType == ScaleType.FIT_XY) { 217 | throw UnsupportedOperationException("getZoomedRect() not supported with FIT_XY") 218 | } 219 | val topLeft = transformCoordTouchToBitmap(0f, 0f, true) 220 | val bottomRight = transformCoordTouchToBitmap(viewWidth.toFloat(), viewHeight.toFloat(), true) 221 | val w = getDrawableWidth(drawable).toFloat() 222 | val h = getDrawableHeight(drawable).toFloat() 223 | return RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h) 224 | } 225 | 226 | /** 227 | * Save the current matrix and view dimensions 228 | * in the prevMatrix and prevView variables. 229 | */ 230 | fun savePreviousImageValues() { 231 | if (touchMatrix != null && viewHeight != 0 && viewWidth != 0) { 232 | touchMatrix!!.getValues(floatMatrix) 233 | prevMatrix!!.setValues(floatMatrix) 234 | prevMatchViewHeight = matchViewHeight 235 | prevMatchViewWidth = matchViewWidth 236 | prevViewHeight = viewHeight 237 | prevViewWidth = viewWidth 238 | } 239 | } 240 | 241 | public override fun onSaveInstanceState(): Parcelable? { 242 | val bundle = Bundle() 243 | bundle.putParcelable("instanceState", super.onSaveInstanceState()) 244 | bundle.putInt("orientation", orientation) 245 | bundle.putFloat("saveScale", currentZoom) 246 | bundle.putFloat("matchViewHeight", matchViewHeight) 247 | bundle.putFloat("matchViewWidth", matchViewWidth) 248 | bundle.putInt("viewWidth", viewWidth) 249 | bundle.putInt("viewHeight", viewHeight) 250 | touchMatrix!!.getValues(floatMatrix) 251 | bundle.putFloatArray("matrix", floatMatrix) 252 | bundle.putBoolean("imageRendered", imageRenderedAtLeastOnce) 253 | bundle.putSerializable("viewSizeChangeFixedPixel", viewSizeChangeFixedPixel) 254 | bundle.putSerializable("orientationChangeFixedPixel", orientationChangeFixedPixel) 255 | return bundle 256 | } 257 | 258 | public override fun onRestoreInstanceState(state: Parcelable) { 259 | if (state is Bundle) { 260 | val bundle = state 261 | currentZoom = bundle.getFloat("saveScale") 262 | floatMatrix = bundle.getFloatArray("matrix") 263 | prevMatrix!!.setValues(floatMatrix) 264 | prevMatchViewHeight = bundle.getFloat("matchViewHeight") 265 | prevMatchViewWidth = bundle.getFloat("matchViewWidth") 266 | prevViewHeight = bundle.getInt("viewHeight") 267 | prevViewWidth = bundle.getInt("viewWidth") 268 | imageRenderedAtLeastOnce = bundle.getBoolean("imageRendered") 269 | viewSizeChangeFixedPixel = bundle.getSerializable("viewSizeChangeFixedPixel") as FixedPixel? 270 | orientationChangeFixedPixel = bundle.getSerializable("orientationChangeFixedPixel") as FixedPixel? 271 | val oldOrientation = bundle.getInt("orientation") 272 | if (orientation != oldOrientation) { 273 | orientationJustChanged = true 274 | } 275 | super.onRestoreInstanceState(bundle.getParcelable("instanceState")) 276 | return 277 | } 278 | super.onRestoreInstanceState(state) 279 | } 280 | 281 | override fun onDraw(canvas: Canvas) { 282 | onDrawReady = true 283 | imageRenderedAtLeastOnce = true 284 | if (delayedZoomVariables != null) { 285 | setZoom(delayedZoomVariables!!.scale, delayedZoomVariables!!.focusX, delayedZoomVariables!!.focusY, delayedZoomVariables!!.scaleType) 286 | delayedZoomVariables = null 287 | } 288 | super.onDraw(canvas) 289 | } 290 | 291 | public override fun onConfigurationChanged(newConfig: Configuration) { 292 | super.onConfigurationChanged(newConfig) 293 | val newOrientation = resources.configuration.orientation 294 | if (newOrientation != orientation) { 295 | orientationJustChanged = true 296 | orientation = newOrientation 297 | } 298 | savePreviousImageValues() 299 | } 300 | 301 | /** 302 | * Get the max zoom multiplier. 303 | * 304 | * @return max zoom multiplier. 305 | */ 306 | /** 307 | * Set the max zoom multiplier to a constant. Default value: 3. 308 | * 309 | */ 310 | var maxZoom: Float 311 | get() = maxScale 312 | set(max) { 313 | maxScale = max 314 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale 315 | maxScaleIsSetByMultiplier = false 316 | } 317 | 318 | /** 319 | * Set the max zoom multiplier as a multiple of minZoom, whatever minZoom may change to. By 320 | * default, this is not done, and maxZoom has a fixed value of 3. 321 | * 322 | * @param max max zoom multiplier, as a multiple of minZoom 323 | */ 324 | fun setMaxZoomRatio(max: Float) { 325 | maxScaleMultiplier = max 326 | maxScale = minScale * maxScaleMultiplier 327 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale 328 | maxScaleIsSetByMultiplier = true 329 | } 330 | 331 | /** 332 | * Get the min zoom multiplier. 333 | * 334 | * @return min zoom multiplier. 335 | */// CENTER_CROP 336 | /** 337 | * Set the min zoom multiplier. Default value: 1. 338 | * 339 | */ 340 | var minZoom: Float 341 | get() = minScale 342 | set(min) { 343 | userSpecifiedMinScale = min 344 | if (min == AUTOMATIC_MIN_ZOOM) { 345 | if (touchScaleType == ScaleType.CENTER || touchScaleType == ScaleType.CENTER_CROP) { 346 | val drawable = drawable 347 | val drawableWidth = getDrawableWidth(drawable) 348 | val drawableHeight = getDrawableHeight(drawable) 349 | if (drawable != null && drawableWidth > 0 && drawableHeight > 0) { 350 | val widthRatio = viewWidth.toFloat() / drawableWidth 351 | val heightRatio = viewHeight.toFloat() / drawableHeight 352 | minScale = if (touchScaleType == ScaleType.CENTER) { 353 | Math.min(widthRatio, heightRatio) 354 | } else { // CENTER_CROP 355 | Math.min(widthRatio, heightRatio) / Math.max(widthRatio, heightRatio) 356 | } 357 | } 358 | } else { 359 | minScale = 1.0f 360 | } 361 | } else { 362 | minScale = userSpecifiedMinScale 363 | } 364 | if (maxScaleIsSetByMultiplier) { 365 | setMaxZoomRatio(maxScaleMultiplier) 366 | } 367 | superMinScale = SUPER_MIN_MULTIPLIER * minScale 368 | } 369 | 370 | /** 371 | * Reset zoom and translation to initial state. 372 | */ 373 | fun resetZoom() { 374 | currentZoom = 1f 375 | fitImageToView() 376 | } 377 | 378 | fun resetZoomAnimated() { 379 | setZoomAnimated(1f, 0.5f, 0.5f) 380 | } 381 | 382 | /** 383 | * Set zoom to the specified scale. Image will be centered by default. 384 | */ 385 | fun setZoom(scale: Float) { 386 | setZoom(scale, 0.5f, 0.5f) 387 | } 388 | 389 | /** 390 | * Set zoom to the specified scale. Image will be centered around the point 391 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point 392 | * as a fraction from the left and top of the view. For example, the top left 393 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 394 | */ 395 | fun setZoom(scale: Float, focusX: Float, focusY: Float) { 396 | setZoom(scale, focusX, focusY, touchScaleType) 397 | } 398 | 399 | /** 400 | * Set zoom to the specified scale. Image will be centered around the point 401 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point 402 | * as a fraction from the left and top of the view. For example, the top left 403 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 404 | */ 405 | fun setZoom(scale: Float, focusX: Float, focusY: Float, scaleType: ScaleType?) { 406 | // 407 | // setZoom can be called before the image is on the screen, but at this point, 408 | // image and view sizes have not yet been calculated in onMeasure. Thus, we should 409 | // delay calling setZoom until the view has been measured. 410 | // 411 | if (!onDrawReady) { 412 | delayedZoomVariables = ZoomVariables(scale, focusX, focusY, scaleType) 413 | return 414 | } 415 | if (userSpecifiedMinScale == AUTOMATIC_MIN_ZOOM) { 416 | minZoom = AUTOMATIC_MIN_ZOOM 417 | if (currentZoom < minScale) { 418 | currentZoom = minScale 419 | } 420 | } 421 | if (scaleType != touchScaleType) { 422 | setScaleType(scaleType!!) 423 | } 424 | resetZoom() 425 | scaleImage(scale.toDouble(), viewWidth / 2.toFloat(), viewHeight / 2.toFloat(), true) 426 | touchMatrix!!.getValues(floatMatrix) 427 | floatMatrix!![Matrix.MTRANS_X] = -(focusX * imageWidth - viewWidth * 0.5f) 428 | floatMatrix!![Matrix.MTRANS_Y] = -(focusY * imageHeight - viewHeight * 0.5f) 429 | touchMatrix!!.setValues(floatMatrix) 430 | fixTrans() 431 | savePreviousImageValues() 432 | imageMatrix = touchMatrix 433 | } 434 | 435 | /** 436 | * Set zoom parameters equal to another TouchImageView. Including scale, position, 437 | * and ScaleType. 438 | */ 439 | fun setZoom(img: TouchImageView) { 440 | val center = img.scrollPosition 441 | setZoom(img.currentZoom, center.x, center.y, img.scaleType) 442 | } 443 | 444 | /** 445 | * Return the point at the center of the zoomed image. The PointF coordinates range 446 | * in value between 0 and 1 and the focus point is denoted as a fraction from the left 447 | * and top of the view. For example, the top left corner of the image would be (0, 0). 448 | * And the bottom right corner would be (1, 1). 449 | * 450 | * @return PointF representing the scroll position of the zoomed image. 451 | */ 452 | val scrollPosition: PointF 453 | get() { 454 | val drawable = drawable ?: return PointF(.5f, .5f) 455 | val drawableWidth = getDrawableWidth(drawable) 456 | val drawableHeight = getDrawableHeight(drawable) 457 | val point = transformCoordTouchToBitmap(viewWidth / 2.toFloat(), viewHeight / 2.toFloat(), true) 458 | point.x /= drawableWidth.toFloat() 459 | point.y /= drawableHeight.toFloat() 460 | return point 461 | } 462 | 463 | private fun orientationMismatch(drawable: Drawable?): Boolean { 464 | return viewWidth > viewHeight != drawable!!.intrinsicWidth > drawable.intrinsicHeight 465 | } 466 | 467 | private fun getDrawableWidth(drawable: Drawable?): Int { 468 | return if (orientationMismatch(drawable) && isRotateImageToFitScreen) { 469 | drawable!!.intrinsicHeight 470 | } else drawable!!.intrinsicWidth 471 | } 472 | 473 | private fun getDrawableHeight(drawable: Drawable?): Int { 474 | return if (orientationMismatch(drawable) && isRotateImageToFitScreen) { 475 | drawable!!.intrinsicWidth 476 | } else drawable!!.intrinsicHeight 477 | } 478 | 479 | /** 480 | * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the 481 | * left and top of the view. The focus points can range in value between 0 and 1. 482 | */ 483 | fun setScrollPosition(focusX: Float, focusY: Float) { 484 | setZoom(currentZoom, focusX, focusY) 485 | } 486 | 487 | /** 488 | * Performs boundary checking and fixes the image matrix if it 489 | * is out of bounds. 490 | */ 491 | private fun fixTrans() { 492 | touchMatrix!!.getValues(floatMatrix) 493 | val transX = floatMatrix!![Matrix.MTRANS_X] 494 | val transY = floatMatrix!![Matrix.MTRANS_Y] 495 | var offset = 0f 496 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) { 497 | offset = imageWidth 498 | } 499 | val fixTransX = getFixTrans(transX, viewWidth.toFloat(), imageWidth, offset) 500 | val fixTransY = getFixTrans(transY, viewHeight.toFloat(), imageHeight, 0f) 501 | touchMatrix!!.postTranslate(fixTransX, fixTransY) 502 | } 503 | 504 | /** 505 | * When transitioning from zooming from focus to zoom from center (or vice versa) 506 | * the image can become unaligned within the view. This is apparent when zooming 507 | * quickly. When the content size is less than the view size, the content will often 508 | * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and 509 | * then makes sure the image is centered correctly within the view. 510 | */ 511 | private fun fixScaleTrans() { 512 | fixTrans() 513 | touchMatrix!!.getValues(floatMatrix) 514 | if (imageWidth < viewWidth) { 515 | var xOffset = (viewWidth - imageWidth) / 2 516 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) { 517 | xOffset += imageWidth 518 | } 519 | floatMatrix!![Matrix.MTRANS_X] = xOffset 520 | } 521 | if (imageHeight < viewHeight) { 522 | floatMatrix!![Matrix.MTRANS_Y] = (viewHeight - imageHeight) / 2 523 | } 524 | touchMatrix!!.setValues(floatMatrix) 525 | } 526 | 527 | private fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float, offset: Float): Float { 528 | val minTrans: Float 529 | val maxTrans: Float 530 | if (contentSize <= viewSize) { 531 | minTrans = offset 532 | maxTrans = offset + viewSize - contentSize 533 | } else { 534 | minTrans = offset + viewSize - contentSize 535 | maxTrans = offset 536 | } 537 | if (trans < minTrans) return -trans + minTrans 538 | return if (trans > maxTrans) -trans + maxTrans else 0f 539 | } 540 | 541 | private fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float { 542 | return if (contentSize <= viewSize) { 543 | 0f 544 | } else 545 | delta 546 | } 547 | 548 | private val imageWidth: Float 549 | get() = matchViewWidth * currentZoom 550 | 551 | private val imageHeight: Float 552 | get() = matchViewHeight * currentZoom 553 | 554 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 555 | val drawable = drawable 556 | if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) { 557 | setMeasuredDimension(0, 0) 558 | return 559 | } 560 | val drawableWidth = getDrawableWidth(drawable) 561 | val drawableHeight = getDrawableHeight(drawable) 562 | val widthSize = MeasureSpec.getSize(widthMeasureSpec) 563 | val widthMode = MeasureSpec.getMode(widthMeasureSpec) 564 | val heightSize = MeasureSpec.getSize(heightMeasureSpec) 565 | val heightMode = MeasureSpec.getMode(heightMeasureSpec) 566 | val totalViewWidth = setViewSize(widthMode, widthSize, drawableWidth) 567 | val totalViewHeight = setViewSize(heightMode, heightSize, drawableHeight) 568 | if (!orientationJustChanged) { 569 | savePreviousImageValues() 570 | } 571 | 572 | // Image view width, height must consider padding 573 | val width = totalViewWidth - paddingLeft - paddingRight 574 | val height = totalViewHeight - paddingTop - paddingBottom 575 | 576 | // Set view dimensions 577 | setMeasuredDimension(width, height) 578 | } 579 | 580 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 581 | super.onSizeChanged(w, h, oldw, oldh) 582 | 583 | // 584 | // Fit content within view. 585 | // 586 | // onMeasure may be called multiple times for each layout change, including orientation 587 | // changes. For example, if the TouchImageView is inside a ConstraintLayout, onMeasure may 588 | // be called with: 589 | // widthMeasureSpec == "AT_MOST 2556" and then immediately with 590 | // widthMeasureSpec == "EXACTLY 1404", then back and forth multiple times in quick 591 | // succession, as the ConstraintLayout tries to solve its constraints. 592 | // 593 | // onSizeChanged is called once after the final onMeasure is called. So we make all changes 594 | // to class members, such as fitting the image into the new shape of the TouchImageView, 595 | // here, after the final size has been determined. This helps us avoid both 596 | // repeated computations, and making irreversible changes (e.g. making the View temporarily too 597 | // big or too small, thus making the current zoom fall outside of an automatically-changing 598 | // minZoom and maxZoom). 599 | // 600 | viewWidth = w 601 | viewHeight = h 602 | fitImageToView() 603 | } 604 | 605 | /** 606 | * This function can be called: 607 | * 1. When the TouchImageView is first loaded (onMeasure). 608 | * 2. When a new image is loaded (setImageResource|Bitmap|Drawable|URI). 609 | * 3. On rotation (onSaveInstanceState, then onRestoreInstanceState, then onMeasure). 610 | * 4. When the view is resized (onMeasure). 611 | * 5. When the zoom is reset (resetZoom). 612 | * 613 | * 614 | * In cases 2, 3 and 4, we try to maintain the zoom state and position as directed by 615 | * orientationChangeFixedPixel or viewSizeChangeFixedPixel (if there is an existing zoom state 616 | * and position, which there might not be in case 2). 617 | * 618 | * 619 | * If the normalizedScale is equal to 1, then the image is made to fit the View. Otherwise, we 620 | * maintain zoom level and attempt to roughly put the same part of the image in the View as was 621 | * there before, paying attention to orientationChangeFixedPixel or viewSizeChangeFixedPixel. 622 | */ 623 | private fun fitImageToView() { 624 | val fixedPixel = if (orientationJustChanged) orientationChangeFixedPixel else viewSizeChangeFixedPixel 625 | orientationJustChanged = false 626 | val drawable = drawable 627 | if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) { 628 | return 629 | } 630 | if (touchMatrix == null || prevMatrix == null) { 631 | return 632 | } 633 | if (userSpecifiedMinScale == AUTOMATIC_MIN_ZOOM) { 634 | minZoom = AUTOMATIC_MIN_ZOOM 635 | if (currentZoom < minScale) { 636 | currentZoom = minScale 637 | } 638 | } 639 | val drawableWidth = getDrawableWidth(drawable) 640 | val drawableHeight = getDrawableHeight(drawable) 641 | 642 | // 643 | // Scale image for view 644 | // 645 | var scaleX = viewWidth.toFloat() / drawableWidth 646 | var scaleY = viewHeight.toFloat() / drawableHeight 647 | when (touchScaleType) { 648 | ScaleType.CENTER -> { 649 | scaleY = 1f 650 | scaleX = scaleY 651 | } 652 | ScaleType.CENTER_CROP -> { 653 | scaleY = Math.max(scaleX, scaleY) 654 | scaleX = scaleY 655 | } 656 | ScaleType.CENTER_INSIDE -> { 657 | run { 658 | scaleY = Math.min(1f, Math.min(scaleX, scaleY)) 659 | scaleX = scaleY 660 | } 661 | run { 662 | scaleY = Math.min(scaleX, scaleY) 663 | scaleX = scaleY 664 | } 665 | } 666 | ScaleType.FIT_CENTER, ScaleType.FIT_START, ScaleType.FIT_END -> { 667 | scaleY = Math.min(scaleX, scaleY) 668 | scaleX = scaleY 669 | } 670 | ScaleType.FIT_XY -> { 671 | } 672 | else -> { 673 | } 674 | } 675 | 676 | // Put the image's center in the right place. 677 | val redundantXSpace = viewWidth - scaleX * drawableWidth 678 | val redundantYSpace = viewHeight - scaleY * drawableHeight 679 | matchViewWidth = viewWidth - redundantXSpace 680 | matchViewHeight = viewHeight - redundantYSpace 681 | if (!isZoomed && !imageRenderedAtLeastOnce) { 682 | 683 | // Stretch and center image to fit view 684 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) { 685 | touchMatrix!!.setRotate(90f) 686 | touchMatrix!!.postTranslate(drawableWidth.toFloat(), 0f) 687 | touchMatrix!!.postScale(scaleX, scaleY) 688 | } else { 689 | touchMatrix!!.setScale(scaleX, scaleY) 690 | } 691 | when (touchScaleType) { 692 | ScaleType.FIT_START -> touchMatrix!!.postTranslate(0f, 0f) 693 | ScaleType.FIT_END -> touchMatrix!!.postTranslate(redundantXSpace, redundantYSpace) 694 | else -> touchMatrix!!.postTranslate(redundantXSpace / 2, redundantYSpace / 2) 695 | } 696 | currentZoom = 1f 697 | } else { 698 | // These values should never be 0 or we will set viewWidth and viewHeight 699 | // to NaN in newTranslationAfterChange. To avoid this, call savePreviousImageValues 700 | // to set them equal to the current values. 701 | if (prevMatchViewWidth == 0f || prevMatchViewHeight == 0f) { 702 | savePreviousImageValues() 703 | } 704 | 705 | // Use the previous matrix as our starting point for the new matrix. 706 | prevMatrix!!.getValues(floatMatrix) 707 | 708 | // Rescale Matrix if appropriate 709 | floatMatrix!![Matrix.MSCALE_X] = matchViewWidth / drawableWidth * currentZoom 710 | floatMatrix!![Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * currentZoom 711 | 712 | // TransX and TransY from previous matrix 713 | val transX = floatMatrix!![Matrix.MTRANS_X] 714 | val transY = floatMatrix!![Matrix.MTRANS_Y] 715 | 716 | // X position 717 | val prevActualWidth = prevMatchViewWidth * currentZoom 718 | val actualWidth = imageWidth 719 | floatMatrix!![Matrix.MTRANS_X] = newTranslationAfterChange(transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth, fixedPixel) 720 | 721 | // Y position 722 | val prevActualHeight = prevMatchViewHeight * currentZoom 723 | val actualHeight = imageHeight 724 | floatMatrix!![Matrix.MTRANS_Y] = newTranslationAfterChange(transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight, fixedPixel) 725 | 726 | // Set the matrix to the adjusted scale and translation values. 727 | touchMatrix!!.setValues(floatMatrix) 728 | } 729 | fixTrans() 730 | imageMatrix = touchMatrix 731 | } 732 | 733 | /** 734 | * Set view dimensions based on layout params 735 | */ 736 | private fun setViewSize(mode: Int, size: Int, drawableWidth: Int): Int { 737 | val viewSize: Int 738 | viewSize = when (mode) { 739 | MeasureSpec.EXACTLY -> size 740 | MeasureSpec.AT_MOST -> Math.min(drawableWidth, size) 741 | MeasureSpec.UNSPECIFIED -> drawableWidth 742 | else -> size 743 | } 744 | return viewSize 745 | } 746 | 747 | /** 748 | * After any change described in the comments for fitImageToView, the matrix needs to be 749 | * translated. This function translates the image so that the fixed pixel in the image 750 | * stays in the same place in the View. 751 | * 752 | * @param trans the value of trans in that axis before the rotation 753 | * @param prevImageSize the width/height of the image before the rotation 754 | * @param imageSize width/height of the image after rotation 755 | * @param prevViewSize width/height of view before rotation 756 | * @param viewSize width/height of view after rotation 757 | * @param drawableSize width/height of drawable 758 | * @param sizeChangeFixedPixel how we should choose the fixed pixel 759 | */ 760 | private fun newTranslationAfterChange(trans: Float, prevImageSize: Float, imageSize: Float, prevViewSize: Int, viewSize: Int, drawableSize: Int, sizeChangeFixedPixel: FixedPixel?): Float { 761 | return if (imageSize < viewSize) { 762 | // 763 | // The width/height of image is less than the view's width/height. Center it. 764 | // 765 | (viewSize - drawableSize * floatMatrix!![Matrix.MSCALE_X]) * 0.5f 766 | } else if (trans > 0) { 767 | // 768 | // The image is larger than the view, but was not before the view changed. Center it. 769 | // 770 | -((imageSize - viewSize) * 0.5f) 771 | } else { 772 | // 773 | // Where is the pixel in the View that we are keeping stable, as a fraction of the 774 | // width/height of the View? 775 | // 776 | var fixedPixelPositionInView = 0.5f // CENTER 777 | if (sizeChangeFixedPixel == FixedPixel.BOTTOM_RIGHT) { 778 | fixedPixelPositionInView = 1.0f 779 | } else if (sizeChangeFixedPixel == FixedPixel.TOP_LEFT) { 780 | fixedPixelPositionInView = 0.0f 781 | } 782 | // 783 | // Where is the pixel in the Image that we are keeping stable, as a fraction of the 784 | // width/height of the Image? 785 | // 786 | val fixedPixelPositionInImage = (-trans + fixedPixelPositionInView * prevViewSize) / prevImageSize 787 | // 788 | // Here's what the new translation should be so that, after whatever change triggered 789 | // this function to be called, the pixel at fixedPixelPositionInView of the View is 790 | // still the pixel at fixedPixelPositionInImage of the image. 791 | // 792 | -(fixedPixelPositionInImage * imageSize - viewSize * fixedPixelPositionInView) 793 | } 794 | } 795 | 796 | private fun setState(state: State) { 797 | this.state = state 798 | } 799 | 800 | @Deprecated("") 801 | fun canScrollHorizontallyFroyo(direction: Int): Boolean { 802 | return canScrollHorizontally(direction) 803 | } 804 | 805 | override fun canScrollHorizontally(direction: Int): Boolean { 806 | touchMatrix!!.getValues(floatMatrix) 807 | val x = floatMatrix!![Matrix.MTRANS_X] 808 | return if (imageWidth < viewWidth) { 809 | false 810 | } else if (x >= -1 && direction < 0) { 811 | false 812 | } else Math.abs(x) + viewWidth + 1 < imageWidth || direction <= 0 813 | } 814 | 815 | override fun canScrollVertically(direction: Int): Boolean { 816 | touchMatrix!!.getValues(floatMatrix) 817 | val y = floatMatrix!![Matrix.MTRANS_Y] 818 | return if (imageHeight < viewHeight) { 819 | false 820 | } else if (y >= -1 && direction < 0) { 821 | false 822 | } else Math.abs(y) + viewHeight + 1 < imageHeight || direction <= 0 823 | } 824 | 825 | /** 826 | * Gesture Listener detects a single click or long click and passes that on 827 | * to the view's listener. 828 | */ 829 | private inner class GestureListener : SimpleOnGestureListener() { 830 | override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { 831 | // Pass on to the OnDoubleTapListener if it is present, otherwise let the View handle the click. 832 | return doubleTapListener?.onSingleTapConfirmed(e) ?: performClick() 833 | } 834 | 835 | override fun onLongPress(e: MotionEvent?) { 836 | performLongClick() 837 | } 838 | 839 | override fun onFling(e1: MotionEvent?, e2: MotionEvent?, velocityX: Float, velocityY: Float): Boolean { 840 | // If a previous fling is still active, it should be cancelled so that two flings 841 | // are not run simultaneously. 842 | fling?.cancelFling() 843 | fling = Fling(velocityX.toInt(), velocityY.toInt()) 844 | .also { compatPostOnAnimation(it) } 845 | return super.onFling(e1, e2, velocityX, velocityY) 846 | } 847 | 848 | override fun onDoubleTap(e: MotionEvent?): Boolean { 849 | var consumed = false 850 | if (e != null && isZoomEnabled) { 851 | doubleTapListener?.let { 852 | consumed = it.onDoubleTap(e) 853 | } 854 | if (state == State.NONE) { 855 | val maxZoomScale = if (doubleTapScale == 0f) maxScale else doubleTapScale 856 | val targetZoom = if (currentZoom == minScale) maxZoomScale else minScale 857 | val doubleTap = DoubleTapZoom(targetZoom, e.x, e.y, false) 858 | compatPostOnAnimation(doubleTap) 859 | consumed = true 860 | } 861 | } 862 | return consumed 863 | } 864 | 865 | override fun onDoubleTapEvent(e: MotionEvent?): Boolean { 866 | return doubleTapListener?.onDoubleTapEvent(e) ?: false 867 | } 868 | } 869 | 870 | interface OnTouchImageViewListener { 871 | fun onMove() 872 | } 873 | 874 | /** 875 | * Responsible for all touch events. Handles the heavy lifting of drag and also sends 876 | * touch events to Scale Detector and Gesture Detector. 877 | */ 878 | private inner class PrivateOnTouchListener : OnTouchListener { 879 | 880 | // Remember last point position for dragging 881 | private val last = PointF() 882 | override fun onTouch(v: View, event: MotionEvent): Boolean { 883 | if (drawable == null) { 884 | setState(State.NONE) 885 | return false 886 | } 887 | if (isZoomEnabled) { 888 | mScaleDetector!!.onTouchEvent(event) 889 | } 890 | mGestureDetector!!.onTouchEvent(event) 891 | val curr = PointF(event.x, event.y) 892 | if (state == State.NONE || state == State.DRAG || state == State.FLING) { 893 | when (event.action) { 894 | MotionEvent.ACTION_DOWN -> { 895 | last.set(curr) 896 | if (fling != null) fling!!.cancelFling() 897 | setState(State.DRAG) 898 | } 899 | MotionEvent.ACTION_MOVE -> if (state == State.DRAG) { 900 | val deltaX = curr.x - last.x 901 | val deltaY = curr.y - last.y 902 | val fixTransX = getFixDragTrans(deltaX, viewWidth.toFloat(), imageWidth) 903 | val fixTransY = getFixDragTrans(deltaY, viewHeight.toFloat(), imageHeight) 904 | touchMatrix!!.postTranslate(fixTransX, fixTransY) 905 | fixTrans() 906 | last[curr.x] = curr.y 907 | } 908 | MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> setState(State.NONE) 909 | } 910 | } 911 | imageMatrix = touchMatrix 912 | 913 | // 914 | // User-defined OnTouchListener 915 | // 916 | if (userTouchListener != null) { 917 | userTouchListener!!.onTouch(v, event) 918 | } 919 | 920 | // 921 | // OnTouchImageViewListener is set: TouchImageView dragged by user. 922 | // 923 | if (touchImageViewListener != null) { 924 | touchImageViewListener!!.onMove() 925 | } 926 | 927 | // 928 | // indicate event was handled 929 | // 930 | return true 931 | } 932 | } 933 | 934 | /** 935 | * ScaleListener detects user two finger scaling and scales image. 936 | */ 937 | private inner class ScaleListener : SimpleOnScaleGestureListener() { 938 | override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { 939 | setState(State.ZOOM) 940 | return true 941 | } 942 | 943 | override fun onScale(detector: ScaleGestureDetector): Boolean { 944 | scaleImage(detector.scaleFactor.toDouble(), detector.focusX, detector.focusY, true) 945 | 946 | // 947 | // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user. 948 | // 949 | if (touchImageViewListener != null) { 950 | touchImageViewListener!!.onMove() 951 | } 952 | return true 953 | } 954 | 955 | override fun onScaleEnd(detector: ScaleGestureDetector) { 956 | super.onScaleEnd(detector) 957 | setState(State.NONE) 958 | var animateToZoomBoundary = false 959 | var targetZoom: Float = currentZoom 960 | if (currentZoom > maxScale) { 961 | targetZoom = maxScale 962 | animateToZoomBoundary = true 963 | } else if (currentZoom < minScale) { 964 | targetZoom = minScale 965 | animateToZoomBoundary = true 966 | } 967 | if (animateToZoomBoundary) { 968 | val doubleTap = DoubleTapZoom(targetZoom, (viewWidth / 2).toFloat(), (viewHeight / 2).toFloat(), true) 969 | compatPostOnAnimation(doubleTap) 970 | } 971 | } 972 | } 973 | 974 | private fun scaleImage(deltaScale: Double, focusX: Float, focusY: Float, stretchImageToSuper: Boolean) { 975 | var deltaScaleLocal = deltaScale 976 | val lowerScale: Float 977 | val upperScale: Float 978 | if (stretchImageToSuper) { 979 | lowerScale = superMinScale 980 | upperScale = superMaxScale 981 | } else { 982 | lowerScale = minScale 983 | upperScale = maxScale 984 | } 985 | val origScale = currentZoom 986 | currentZoom *= deltaScaleLocal.toFloat() 987 | if (currentZoom > upperScale) { 988 | currentZoom = upperScale 989 | deltaScaleLocal = upperScale / origScale.toDouble() 990 | } else if (currentZoom < lowerScale) { 991 | currentZoom = lowerScale 992 | deltaScaleLocal = lowerScale / origScale.toDouble() 993 | } 994 | touchMatrix!!.postScale(deltaScaleLocal.toFloat(), deltaScaleLocal.toFloat(), focusX, focusY) 995 | fixScaleTrans() 996 | } 997 | 998 | /** 999 | * DoubleTapZoom calls a series of runnables which apply 1000 | * an animated zoom in/out graphic to the image. 1001 | */ 1002 | private inner class DoubleTapZoom internal constructor(targetZoom: Float, focusX: Float, focusY: Float, stretchImageToSuper: Boolean) : Runnable { 1003 | private val startTime: Long 1004 | private val startZoom: Float 1005 | private val targetZoom: Float 1006 | private val bitmapX: Float 1007 | private val bitmapY: Float 1008 | private val stretchImageToSuper: Boolean 1009 | private val interpolator = AccelerateDecelerateInterpolator() 1010 | private val startTouch: PointF 1011 | private val endTouch: PointF 1012 | override fun run() { 1013 | if (drawable == null) { 1014 | setState(State.NONE) 1015 | return 1016 | } 1017 | val t = interpolate() 1018 | val deltaScale = calculateDeltaScale(t) 1019 | scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper) 1020 | translateImageToCenterTouchPosition(t) 1021 | fixScaleTrans() 1022 | imageMatrix = touchMatrix 1023 | 1024 | // double tap runnable updates listener with every frame. 1025 | if (touchImageViewListener != null) { 1026 | touchImageViewListener!!.onMove() 1027 | } 1028 | if (t < 1f) { 1029 | // We haven't finished zooming 1030 | compatPostOnAnimation(this) 1031 | } else { 1032 | // Finished zooming 1033 | setState(State.NONE) 1034 | } 1035 | } 1036 | 1037 | /** 1038 | * Interpolate between where the image should start and end in order to translate 1039 | * the image so that the point that is touched is what ends up centered at the end 1040 | * of the zoom. 1041 | */ 1042 | private fun translateImageToCenterTouchPosition(t: Float) { 1043 | val targetX = startTouch.x + t * (endTouch.x - startTouch.x) 1044 | val targetY = startTouch.y + t * (endTouch.y - startTouch.y) 1045 | val curr = transformCoordBitmapToTouch(bitmapX, bitmapY) 1046 | touchMatrix!!.postTranslate(targetX - curr.x, targetY - curr.y) 1047 | } 1048 | 1049 | /** 1050 | * Use interpolator to get t 1051 | */ 1052 | private fun interpolate(): Float { 1053 | val currTime = System.currentTimeMillis() 1054 | var elapsed = (currTime - startTime) / DEFAULT_ZOOM_TIME.toFloat() 1055 | elapsed = Math.min(1f, elapsed) 1056 | return interpolator.getInterpolation(elapsed) 1057 | } 1058 | 1059 | /** 1060 | * Interpolate the current targeted zoom and get the delta 1061 | * from the current zoom. 1062 | */ 1063 | private fun calculateDeltaScale(t: Float): Double { 1064 | val zoom = startZoom + t * (targetZoom - startZoom).toDouble() 1065 | return zoom / currentZoom 1066 | } 1067 | 1068 | init { 1069 | setState(State.ANIMATE_ZOOM) 1070 | startTime = System.currentTimeMillis() 1071 | startZoom = currentZoom 1072 | this.targetZoom = targetZoom 1073 | this.stretchImageToSuper = stretchImageToSuper 1074 | val bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false) 1075 | bitmapX = bitmapPoint.x 1076 | bitmapY = bitmapPoint.y 1077 | 1078 | // Used for translating image during scaling 1079 | startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY) 1080 | endTouch = PointF((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat()) 1081 | } 1082 | } 1083 | 1084 | /** 1085 | * This function will transform the coordinates in the touch event to the coordinate 1086 | * system of the drawable that the imageview contain 1087 | * 1088 | * @param x x-coordinate of touch event 1089 | * @param y y-coordinate of touch event 1090 | * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value 1091 | * to the bounds of the bitmap size. 1092 | * @return Coordinates of the point touched, in the coordinate system of the original drawable. 1093 | */ 1094 | protected fun transformCoordTouchToBitmap(x: Float, y: Float, clipToBitmap: Boolean): PointF { 1095 | touchMatrix!!.getValues(floatMatrix) 1096 | val origW = drawable.intrinsicWidth.toFloat() 1097 | val origH = drawable.intrinsicHeight.toFloat() 1098 | val transX = floatMatrix!![Matrix.MTRANS_X] 1099 | val transY = floatMatrix!![Matrix.MTRANS_Y] 1100 | var finalX = (x - transX) * origW / imageWidth 1101 | var finalY = (y - transY) * origH / imageHeight 1102 | if (clipToBitmap) { 1103 | finalX = Math.min(Math.max(finalX, 0f), origW) 1104 | finalY = Math.min(Math.max(finalY, 0f), origH) 1105 | } 1106 | return PointF(finalX, finalY) 1107 | } 1108 | 1109 | /** 1110 | * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the 1111 | * drawable's coordinate system to the view's coordinate system. 1112 | * 1113 | * @param bx x-coordinate in original bitmap coordinate system 1114 | * @param by y-coordinate in original bitmap coordinate system 1115 | * @return Coordinates of the point in the view's coordinate system. 1116 | */ 1117 | protected fun transformCoordBitmapToTouch(bx: Float, by: Float): PointF { 1118 | touchMatrix!!.getValues(floatMatrix) 1119 | val origW = drawable.intrinsicWidth.toFloat() 1120 | val origH = drawable.intrinsicHeight.toFloat() 1121 | val px = bx / origW 1122 | val py = by / origH 1123 | val finalX = floatMatrix!![Matrix.MTRANS_X] + imageWidth * px 1124 | val finalY = floatMatrix!![Matrix.MTRANS_Y] + imageHeight * py 1125 | return PointF(finalX, finalY) 1126 | } 1127 | 1128 | /** 1129 | * Fling launches sequential runnables which apply 1130 | * the fling graphic to the image. The values for the translation 1131 | * are interpolated by the Scroller. 1132 | */ 1133 | private inner class Fling internal constructor(velocityX: Int, velocityY: Int) : Runnable { 1134 | var scroller: CompatScroller? 1135 | var currX: Int 1136 | var currY: Int 1137 | fun cancelFling() { 1138 | if (scroller != null) { 1139 | setState(State.NONE) 1140 | scroller!!.forceFinished(true) 1141 | } 1142 | } 1143 | 1144 | override fun run() { 1145 | 1146 | // OnTouchImageViewListener is set: TouchImageView listener has been flung by user. 1147 | // Listener runnable updated with each frame of fling animation. 1148 | if (touchImageViewListener != null) { 1149 | touchImageViewListener!!.onMove() 1150 | } 1151 | if (scroller!!.isFinished) { 1152 | scroller = null 1153 | return 1154 | } 1155 | if (scroller!!.computeScrollOffset()) { 1156 | val newX = scroller!!.currX 1157 | val newY = scroller!!.currY 1158 | val transX = newX - currX 1159 | val transY = newY - currY 1160 | currX = newX 1161 | currY = newY 1162 | touchMatrix!!.postTranslate(transX.toFloat(), transY.toFloat()) 1163 | fixTrans() 1164 | imageMatrix = touchMatrix 1165 | compatPostOnAnimation(this) 1166 | } 1167 | } 1168 | 1169 | init { 1170 | setState(State.FLING) 1171 | scroller = CompatScroller(context) 1172 | touchMatrix!!.getValues(floatMatrix) 1173 | var startX = floatMatrix!![Matrix.MTRANS_X].toInt() 1174 | val startY = floatMatrix!![Matrix.MTRANS_Y].toInt() 1175 | val minX: Int 1176 | val maxX: Int 1177 | val minY: Int 1178 | val maxY: Int 1179 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) { 1180 | startX -= imageWidth.toInt() 1181 | } 1182 | if (imageWidth > viewWidth) { 1183 | minX = viewWidth - imageWidth.toInt() 1184 | maxX = 0 1185 | } else { 1186 | maxX = startX 1187 | minX = maxX 1188 | } 1189 | if (imageHeight > viewHeight) { 1190 | minY = viewHeight - imageHeight.toInt() 1191 | maxY = 0 1192 | } else { 1193 | maxY = startY 1194 | minY = maxY 1195 | } 1196 | scroller!!.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) 1197 | currX = startX 1198 | currY = startY 1199 | } 1200 | } 1201 | 1202 | @TargetApi(VERSION_CODES.GINGERBREAD) 1203 | private inner class CompatScroller internal constructor(context: Context?) { 1204 | var overScroller: OverScroller 1205 | fun fling(startX: Int, startY: Int, velocityX: Int, velocityY: Int, minX: Int, maxX: Int, minY: Int, maxY: Int) { 1206 | overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) 1207 | } 1208 | 1209 | fun forceFinished(finished: Boolean) { 1210 | overScroller.forceFinished(finished) 1211 | } 1212 | 1213 | val isFinished: Boolean 1214 | get() = overScroller.isFinished 1215 | 1216 | fun computeScrollOffset(): Boolean { 1217 | overScroller.computeScrollOffset() 1218 | return overScroller.computeScrollOffset() 1219 | } 1220 | 1221 | val currX: Int 1222 | get() = overScroller.currX 1223 | 1224 | val currY: Int 1225 | get() = overScroller.currY 1226 | 1227 | init { 1228 | overScroller = OverScroller(context) 1229 | } 1230 | } 1231 | 1232 | @TargetApi(VERSION_CODES.JELLY_BEAN) 1233 | private fun compatPostOnAnimation(runnable: Runnable) { 1234 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 1235 | postOnAnimation(runnable) 1236 | } else { 1237 | postDelayed(runnable, 1000 / 60.toLong()) 1238 | } 1239 | } 1240 | 1241 | private inner class ZoomVariables internal constructor(var scale: Float, var focusX: Float, var focusY: Float, var scaleType: ScaleType?) 1242 | 1243 | interface OnZoomFinishedListener { 1244 | fun onZoomFinished() 1245 | } 1246 | 1247 | /** 1248 | * Set zoom to the specified scale with a linearly interpolated animation. Image will be 1249 | * centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the 1250 | * focus point as a fraction from the left and top of the view. For example, the top left 1251 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 1252 | */ 1253 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float) { 1254 | setZoomAnimated(scale, focusX, focusY, DEFAULT_ZOOM_TIME) 1255 | } 1256 | 1257 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, zoomTimeMs: Int) { 1258 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), zoomTimeMs) 1259 | compatPostOnAnimation(animation) 1260 | } 1261 | 1262 | /** 1263 | * Set zoom to the specified scale with a linearly interpolated animation. Image will be 1264 | * centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the 1265 | * focus point as a fraction from the left and top of the view. For example, the top left 1266 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 1267 | * 1268 | * @param listener the listener, which will be notified, once the animation ended 1269 | */ 1270 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, zoomTimeMs: Int, listener: OnZoomFinishedListener?) { 1271 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), zoomTimeMs) 1272 | animation.setListener(listener) 1273 | compatPostOnAnimation(animation) 1274 | } 1275 | 1276 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, listener: OnZoomFinishedListener?) { 1277 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), DEFAULT_ZOOM_TIME) 1278 | animation.setListener(listener) 1279 | compatPostOnAnimation(animation) 1280 | } 1281 | 1282 | /** 1283 | * AnimatedZoom calls a series of runnables which apply 1284 | * an animated zoom to the specified target focus at the specified zoom level. 1285 | */ 1286 | private inner class AnimatedZoom internal constructor(targetZoom: Float, focus: PointF, zoomTimeMillis: Int) : Runnable { 1287 | private val zoomTimeMillis: Int 1288 | private val startTime: Long 1289 | private val startZoom: Float 1290 | private val targetZoom: Float 1291 | private val startFocus: PointF 1292 | private val targetFocus: PointF 1293 | private val interpolator = LinearInterpolator() 1294 | private var listener: OnZoomFinishedListener? = null 1295 | override fun run() { 1296 | val t = interpolate() 1297 | 1298 | // Calculate the next focus and zoom based on the progress of the interpolation 1299 | val nextZoom = startZoom + (targetZoom - startZoom) * t 1300 | val nextX = startFocus.x + (targetFocus.x - startFocus.x) * t 1301 | val nextY = startFocus.y + (targetFocus.y - startFocus.y) * t 1302 | setZoom(nextZoom, nextX, nextY) 1303 | if (t < 1f) { 1304 | // We haven't finished zooming 1305 | compatPostOnAnimation(this) 1306 | } else { 1307 | // Finished zooming 1308 | setState(State.NONE) 1309 | if (listener != null) listener!!.onZoomFinished() 1310 | } 1311 | } 1312 | 1313 | /** 1314 | * Use interpolator to get t 1315 | * 1316 | * @return progress of the interpolation 1317 | */ 1318 | private fun interpolate(): Float { 1319 | var elapsed = (System.currentTimeMillis() - startTime) / zoomTimeMillis.toFloat() 1320 | elapsed = Math.min(1f, elapsed) 1321 | return interpolator.getInterpolation(elapsed) 1322 | } 1323 | 1324 | fun setListener(listener: OnZoomFinishedListener?) { 1325 | this.listener = listener 1326 | } 1327 | 1328 | init { 1329 | setState(State.ANIMATE_ZOOM) 1330 | startTime = System.currentTimeMillis() 1331 | startZoom = currentZoom 1332 | this.targetZoom = targetZoom 1333 | this.zoomTimeMillis = zoomTimeMillis 1334 | 1335 | // Used for translating image during zooming 1336 | startFocus = scrollPosition 1337 | targetFocus = focus 1338 | } 1339 | } 1340 | 1341 | companion object { 1342 | private const val DEBUG = "DEBUG" 1343 | 1344 | // SuperMin and SuperMax multipliers. Determine how much the image can be 1345 | // zoomed below or above the zoom boundaries, before animating back to the 1346 | // min/max zoom boundary. 1347 | private const val SUPER_MIN_MULTIPLIER = .75f 1348 | private const val SUPER_MAX_MULTIPLIER = 1.25f 1349 | private const val DEFAULT_ZOOM_TIME = 500 1350 | 1351 | /** 1352 | * If setMinZoom(AUTOMATIC_MIN_ZOOM), then we'll set the min scale to include the whole image. 1353 | */ 1354 | const val AUTOMATIC_MIN_ZOOM = -1.0f 1355 | } 1356 | 1357 | } --------------------------------------------------------------------------------