├── .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 |
4 |
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 |
8 |
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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/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 |  
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | xmlns:android
33 |
34 | ^$
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | xmlns:.*
44 |
45 | ^$
46 |
47 |
48 | BY_NAME
49 |
50 |
51 |
52 |
53 |
54 |
55 | .*:id
56 |
57 | http://schemas.android.com/apk/res/android
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | .*:name
67 |
68 | http://schemas.android.com/apk/res/android
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | name
78 |
79 | ^$
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | style
89 |
90 | ^$
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | ^$
102 |
103 |
104 | BY_NAME
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | http://schemas.android.com/apk/res/android
114 |
115 |
116 | ANDROID_ATTRIBUTE_ORDER
117 |
118 |
119 |
120 |
121 |
122 |
123 | .*
124 |
125 | .*
126 |
127 |
128 | BY_NAME
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------