├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── ssimagepicker
│ │ └── app
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── ssimagepicker
│ │ │ └── app
│ │ │ ├── ExtensionsUtils.kt
│ │ │ ├── PickerOptions.kt
│ │ │ └── ui
│ │ │ ├── DemoFragment.kt
│ │ │ ├── FragmentDemoActivity.kt
│ │ │ ├── ImageDataAdapter.kt
│ │ │ ├── LaunchActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── PickerOptionsBottomSheet.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── drawable_bottom_sheet_dialog.xml
│ │ ├── ic_arrow_back_ios.xml
│ │ ├── ic_baseline_check_circle.xml
│ │ ├── ic_launcher_background.xml
│ │ ├── selector_button_bg.xml
│ │ ├── selector_button_text_color.xml
│ │ ├── selector_switch_thumb.xml
│ │ └── selector_switch_track.xml
│ │ ├── font
│ │ └── poppins_medium.ttf
│ │ ├── layout
│ │ ├── activity_fragment_demo.xml
│ │ ├── activity_launch.xml
│ │ ├── activity_main.xml
│ │ ├── bottom_sheet_picker_options.xml
│ │ ├── fragment_demo.xml
│ │ ├── list_item_image_data.xml
│ │ └── toolbar_app.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── values-night
│ │ ├── colors.xml
│ │ └── themes.xml
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ └── file_path.xml
│ └── test
│ └── java
│ └── com
│ └── ssimagepicker
│ └── app
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── App.kt
│ ├── Extensions.kt
│ └── Plugins.kt
├── docs
├── migration.md
├── options_bottom_sheet_customization.md
├── picker_config.md
└── picker_ui_customization.md
├── gifs
├── camera_picker.gif
├── crop_options.gif
├── extension_options.gif
├── gallery_multi_selection.gif
├── gallery_picker.gif
├── picker_option_bottom_sheet.gif
└── system_photo_picker.gif
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── imagepickerlibrary
├── .gitignore
├── build.gradle.kts
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── app
│ │ └── imagepickerlibrary
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── app
│ │ │ └── imagepickerlibrary
│ │ │ ├── Constants.kt
│ │ │ ├── Extensions.kt
│ │ │ ├── ImagePicker.kt
│ │ │ ├── ImagePickerFileProvider.kt
│ │ │ ├── listener
│ │ │ ├── ImagePickerResultListener.kt
│ │ │ └── ItemClickListener.kt
│ │ │ ├── model
│ │ │ └── DataModels.kt
│ │ │ ├── ui
│ │ │ ├── activity
│ │ │ │ └── ImagePickerActivity.kt
│ │ │ ├── adapter
│ │ │ │ ├── BaseAdapter.kt
│ │ │ │ ├── FolderAdapter.kt
│ │ │ │ └── ImageAdapter.kt
│ │ │ ├── bottomsheet
│ │ │ │ └── SSPickerOptionsBottomSheet.kt
│ │ │ ├── dialog
│ │ │ │ └── FullScreenImageDialogFragment.kt
│ │ │ └── fragment
│ │ │ │ ├── BaseFragment.kt
│ │ │ │ ├── FolderFragment.kt
│ │ │ │ └── ImageFragment.kt
│ │ │ ├── util
│ │ │ ├── BindingAdapters.kt
│ │ │ ├── PickerConfigManager.kt
│ │ │ └── Util.kt
│ │ │ └── viewmodel
│ │ │ └── ImagePickerViewModel.kt
│ └── res
│ │ ├── drawable
│ │ ├── bg_ss_drawable_shadow.xml
│ │ ├── bg_ss_picker_option.xml
│ │ ├── bg_ss_picker_option_button.xml
│ │ ├── bg_ss_picker_option_button_cancel.xml
│ │ ├── ic_ss_arrow_back.xml
│ │ ├── ic_ss_camera.xml
│ │ ├── ic_ss_check_circle.xml
│ │ ├── ic_ss_done.xml
│ │ └── ic_ss_zoom_eye.xml
│ │ ├── ic_launcher.png
│ │ ├── layout-v23
│ │ └── list_item_image.xml
│ │ ├── layout
│ │ ├── activity_image_picker.xml
│ │ ├── bottom_sheet_image_picker_options.xml
│ │ ├── dialog_fragment_full_screen_image.xml
│ │ ├── fragment_folder.xml
│ │ ├── fragment_image.xml
│ │ ├── list_item_folder.xml
│ │ ├── list_item_image.xml
│ │ └── toolbar_image_picker.xml
│ │ ├── mipmap
│ │ └── ic_launcher_round.png
│ │ ├── values
│ │ ├── attr.xml
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ └── file_path.xml
│ └── test
│ └── java
│ └── com
│ └── app
│ └── imagepickerlibrary
│ └── ExampleUnitTest.kt
├── jitpack.yml
├── library_banner.png
└── settings.gradle
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | developer@simform.com.
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series
86 | of actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or
93 | permanent ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within
113 | the community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.0, available at
119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120 |
121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct
122 | enforcement ladder](https://github.com/mozilla/diversity).
123 |
124 | [homepage]: https://www.contributor-covenant.org
125 |
126 | For answers to common questions about this code of conduct, see the FAQ at
127 | https://www.contributor-covenant.org/faq. Translations are available at
128 | https://www.contributor-covenant.org/translations.
129 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Way to contribute
2 |
3 | 1. Fork the repo and create your branch from `master`.
4 | 2. Clone the project to your own machine. (Please have a look at [**Readme.md**](https://github.com/SimformSolutionsPvtLtd/SSImagePicker/blob/master/README.md) to understand how to run this project on your machine)
5 | 3. Commit changes to your own branch
6 | 4. Make sure your code lints.
7 | 5. Push your work back up to your fork.
8 | 6. Issue that pull request!
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id(Plugins.ANDROID_APPLICATION)
3 | kotlin(Plugins.Kotlin.ANDROID)
4 | kotlin(Plugins.Kotlin.KAPT)
5 | id(Plugins.Kotlin.PARCELIZE)
6 | }
7 |
8 | android {
9 | compileSdk = libs.versions.compile.sdk.get().toInt()
10 | namespace = App.ID
11 | defaultConfig {
12 | applicationId = App.ID
13 | minSdk = libs.versions.min.sdk.get().toInt()
14 | targetSdk = libs.versions.target.sdk.get().toInt()
15 | versionCode = libs.versions.version.code.get().toInt()
16 | versionName = libs.versions.version.name.get()
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | multiDexEnabled = App.MULTI_DEX
19 | javaCompileOptions {
20 | annotationProcessorOptions {
21 | arguments["room.incremental"] = "true"
22 | }
23 | }
24 | }
25 |
26 | buildTypes {
27 | getByName(App.BuildType.RELEASE) {
28 | isMinifyEnabled = false
29 | proguardFiles(
30 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
31 | )
32 | manifestPlaceholders["enableCrashReporting"] = true
33 | }
34 |
35 | getByName(App.BuildType.DEBUG) {
36 | isMinifyEnabled = false
37 | proguardFiles(
38 | getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
39 | )
40 | manifestPlaceholders["enableCrashReporting"] = false
41 | }
42 | }
43 |
44 | buildFeatures {
45 | dataBinding = true
46 | }
47 |
48 | compileOptions {
49 | sourceCompatibility = JavaVersion.VERSION_17
50 | targetCompatibility = JavaVersion.VERSION_17
51 | }
52 |
53 | kotlinOptions {
54 | jvmTarget = JavaVersion.VERSION_17.toString()
55 | }
56 | }
57 |
58 |
59 | dependencies {
60 | implementation(defaultFileTree())
61 |
62 | // Core
63 | implementation(libs.androidx.core.ktx)
64 |
65 | // UI
66 | implementation(libs.androidx.appcompat)
67 | implementation(libs.androidx.constraintlayout)
68 |
69 | // Jetpack
70 | implementation(libs.androidx.activity.ktx)
71 | implementation(libs.androidx.fragment.ktx)
72 | implementation(libs.androidx.recyclerview)
73 |
74 | // LiveData
75 | implementation(libs.androidx.lifecycle.livedata)
76 | implementation(libs.lifecycle.livedata.ktx)
77 |
78 | // Material
79 | implementation(libs.material)
80 |
81 | // INTUIT DIMEN SSP and SDP
82 | implementation(libs.sdp.android)
83 | implementation(libs.ssp.android)
84 |
85 | // Unit testing
86 | testImplementation(libs.junit)
87 | testImplementation(libs.androidx.core.testing)
88 | testImplementation(libs.androidx.junit)
89 |
90 | // UI testing
91 | androidTestImplementation(libs.androidx.runner)
92 | androidTestImplementation(libs.androidx.junit)
93 | androidTestImplementation(libs.rules)
94 | androidTestImplementation(libs.androidx.espresso.core)
95 | androidTestImplementation(libs.androidx.espresso.contrib)
96 |
97 | // Glide
98 | implementation(libs.glide)
99 | kapt(libs.compiler)
100 |
101 | implementation(project(":imagepickerlibrary"))
102 | }
--------------------------------------------------------------------------------
/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.kts.
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/ssimagepicker/app/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app
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.ssimagepicker.app", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
8 |
9 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
32 |
33 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ExtensionsUtils.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app
2 |
3 | import android.os.Build
4 | import android.view.View
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.appcompat.widget.AppCompatImageView
7 | import androidx.core.view.ViewCompat
8 | import androidx.core.view.WindowInsetsCompat
9 | import com.app.imagepickerlibrary.R
10 | import com.bumptech.glide.Glide
11 | import com.bumptech.glide.load.engine.DiskCacheStrategy
12 | import com.bumptech.glide.load.resource.bitmap.CenterCrop
13 | import com.bumptech.glide.load.resource.bitmap.RoundedCorners
14 | import com.bumptech.glide.request.RequestOptions
15 |
16 | /**
17 | * Extension function to load image into image view with Glide.
18 | */
19 | fun AppCompatImageView.loadImage(
20 | url: Any?,
21 | isCircle: Boolean = false,
22 | isRoundedCorners: Boolean = false,
23 | func: RequestOptions.() -> Unit = {}
24 | ) {
25 | url?.let { image ->
26 | val options = RequestOptions().placeholder(R.mipmap.ic_launcher_round)
27 | .error(R.mipmap.ic_launcher_round)
28 | .skipMemoryCache(true)
29 | .diskCacheStrategy(DiskCacheStrategy.ALL)
30 | .apply(func)
31 | var requestBuilder = Glide.with(context).load(image).apply(options)
32 | if (isCircle) {
33 | requestBuilder = requestBuilder.apply(options.circleCrop())
34 | } else if (isRoundedCorners) {
35 | requestBuilder =
36 | requestBuilder.apply(options.transform(CenterCrop(), RoundedCorners(18)))
37 | }
38 | requestBuilder.into(this)
39 | }
40 | }
41 |
42 | //If you are using custom theming and need to change the status bar color,
43 | // it may not work unless you specify a particular view object, like a toolbar.
44 | fun AppCompatActivity.enableEdgeToEdge(view: View?) {
45 | view?.let {
46 | ViewCompat.setOnApplyWindowInsetsListener(it) { view, windowInsets ->
47 | val systemBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
48 | view.setPadding(
49 | systemBarInsets.left,
50 | systemBarInsets.top,
51 | systemBarInsets.right,
52 | 0
53 | )
54 | windowInsets
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Function to check if the system is at least android 11+
61 | */
62 | fun isAtLeast11() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/PickerOptions.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app
2 |
3 | import android.os.Parcelable
4 | import com.app.imagepickerlibrary.model.AspectRatio
5 | import com.app.imagepickerlibrary.model.PickExtension
6 | import com.app.imagepickerlibrary.model.PickerType
7 | import kotlinx.parcelize.Parcelize
8 |
9 | /**
10 | * Data class to manage the picker options from bottom sheet to main activity.
11 | */
12 | @Parcelize
13 | data class PickerOptions(
14 | val pickerType: PickerType,
15 | val showCountInToolBar: Boolean,
16 | val showFolders: Boolean,
17 | val allowMultipleSelection: Boolean,
18 | val maxPickCount: Int,
19 | val maxPickSizeMB: Float,
20 | val pickExtension: PickExtension,
21 | val showCameraIconInGallery: Boolean,
22 | val isDoneIcon: Boolean,
23 | val openCropOptions: Boolean,
24 | val openSystemPicker: Boolean,
25 | val compressImage: Boolean,
26 | var aspectRatio: AspectRatio?
27 | ) : Parcelable {
28 | companion object {
29 | fun default(): PickerOptions {
30 | return PickerOptions(
31 | pickerType = PickerType.GALLERY,
32 | showCountInToolBar = true,
33 | showFolders = true,
34 | allowMultipleSelection = false,
35 | maxPickCount = 15,
36 | maxPickSizeMB = 2.5f,
37 | pickExtension = PickExtension.ALL,
38 | showCameraIconInGallery = true,
39 | isDoneIcon = true,
40 | openCropOptions = false,
41 | openSystemPicker = false,
42 | compressImage = false,
43 | aspectRatio = null
44 | )
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/DemoFragment.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.net.Uri
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.databinding.DataBindingUtil
9 | import androidx.fragment.app.Fragment
10 | import com.app.imagepickerlibrary.ImagePicker
11 | import com.app.imagepickerlibrary.ImagePicker.Companion.registerImagePicker
12 | import com.app.imagepickerlibrary.listener.ImagePickerResultListener
13 | import com.app.imagepickerlibrary.model.ImageProvider
14 | import com.app.imagepickerlibrary.model.PickerType
15 | import com.app.imagepickerlibrary.ui.bottomsheet.SSPickerOptionsBottomSheet
16 | import com.ssimagepicker.app.PickerOptions
17 | import com.ssimagepicker.app.R
18 | import com.ssimagepicker.app.databinding.FragmentDemoBinding
19 | import com.ssimagepicker.app.isAtLeast11
20 |
21 | class DemoFragment : Fragment(), View.OnClickListener,
22 | SSPickerOptionsBottomSheet.ImagePickerClickListener,
23 | ImagePickerResultListener, PickerOptionsBottomSheet.PickerOptionsListener {
24 |
25 | private lateinit var binding: FragmentDemoBinding
26 |
27 | companion object {
28 | private const val IMAGE_LIST = "IMAGE_LIST"
29 | }
30 |
31 | private val imagePicker: ImagePicker by lazy {
32 | registerImagePicker(this)
33 | }
34 | private val imageList = mutableListOf()
35 | private val imageDataAdapter = ImageDataAdapter(imageList)
36 | private var pickerOptions = PickerOptions.default()
37 |
38 | override fun onCreateView(
39 | inflater: LayoutInflater, container: ViewGroup?,
40 | savedInstanceState: Bundle?
41 | ): View {
42 | binding = DataBindingUtil.inflate(inflater, R.layout.fragment_demo, container, false)
43 | return binding.root
44 | }
45 |
46 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
47 | super.onViewCreated(view, savedInstanceState)
48 | binding.clickHandler = this
49 | setUI(savedInstanceState)
50 | }
51 |
52 | private fun setUI(savedInstanceState: Bundle?) {
53 | binding.imageRecyclerView.adapter = imageDataAdapter
54 | if (savedInstanceState != null && savedInstanceState.containsKey(IMAGE_LIST)) {
55 | val uriList: List =
56 | savedInstanceState.getParcelableArrayList(IMAGE_LIST) ?: listOf()
57 | updateImageList(uriList)
58 | }
59 | }
60 |
61 | override fun onClick(v: View) {
62 | when (v.id) {
63 | R.id.options_button -> {
64 | openPickerOptions()
65 | }
66 | R.id.open_picker_button -> {
67 | openImagePicker()
68 | }
69 | R.id.open_sheet_button -> {
70 | val fragment =
71 | SSPickerOptionsBottomSheet.newInstance(R.style.CustomPickerBottomSheet)
72 | fragment.show(childFragmentManager, SSPickerOptionsBottomSheet.BOTTOM_SHEET_TAG)
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * This method receives the selected picker type option from the bottom sheet.
79 | */
80 | override fun onImageProvider(provider: ImageProvider) {
81 | when (provider) {
82 | ImageProvider.GALLERY -> {
83 | pickerOptions = pickerOptions.copy(pickerType = PickerType.GALLERY)
84 | openImagePicker()
85 | }
86 | ImageProvider.CAMERA -> {
87 | pickerOptions = pickerOptions.copy(pickerType = PickerType.CAMERA)
88 | openImagePicker()
89 | }
90 | ImageProvider.NONE -> {
91 | //User has pressed cancel show anything or just leave it blank.
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * Opens the options for picker. The picker option is bottom sheet with many input parameters.
98 | */
99 | private fun openPickerOptions() {
100 | val fragment = PickerOptionsBottomSheet.newInstance(pickerOptions)
101 | fragment.setClickListener(this)
102 | fragment.show(childFragmentManager, PickerOptionsBottomSheet.BOTTOM_SHEET_TAG)
103 | }
104 |
105 | /**
106 | * Once the picker options are selected in bottom sheet
107 | * we will receive the latest picker options in this method
108 | */
109 | override fun onPickerOptions(pickerOptions: PickerOptions) {
110 | this.pickerOptions = pickerOptions
111 | openImagePicker()
112 | }
113 |
114 | /**
115 | * Open the image picker according to picker type and the ui options.
116 | * The new system picker is only available for Android 13+.
117 | */
118 | private fun openImagePicker() {
119 | imagePicker
120 | .title("My Picker")
121 | .multipleSelection(pickerOptions.allowMultipleSelection, pickerOptions.maxPickCount)
122 | .showCountInToolBar(pickerOptions.showCountInToolBar)
123 | .showFolder(pickerOptions.showFolders)
124 | .cameraIcon(pickerOptions.showCameraIconInGallery)
125 | .doneIcon(pickerOptions.isDoneIcon)
126 | .allowCropping(pickerOptions.openCropOptions)
127 | .compressImage(pickerOptions.compressImage)
128 | .maxImageSize(pickerOptions.maxPickSizeMB)
129 | .extension(pickerOptions.pickExtension)
130 | if (isAtLeast11()) {
131 | imagePicker.systemPicker(pickerOptions.openSystemPicker)
132 | }
133 | imagePicker.open(pickerOptions.pickerType)
134 | }
135 |
136 | /**
137 | * Single Selection and the image captured from camera will be received in this method.
138 | */
139 | override fun onImagePick(uri: Uri?) {
140 | uri?.let { updateImageList(listOf(it)) }
141 | }
142 |
143 | /**
144 | * Multiple Selection uris will be received in this method
145 | */
146 | override fun onMultiImagePick(uris: List?) {
147 | if (!uris.isNullOrEmpty()) {
148 | updateImageList(uris)
149 | }
150 | }
151 |
152 | private fun updateImageList(list: List) {
153 | imageList.clear()
154 | imageList.addAll(list)
155 | imageDataAdapter.notifyDataSetChanged()
156 | }
157 |
158 | override fun onSaveInstanceState(outState: Bundle) {
159 | outState.putParcelableArrayList(IMAGE_LIST, ArrayList(imageList))
160 | super.onSaveInstanceState(outState)
161 | }
162 | }
163 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/FragmentDemoActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.databinding.DataBindingUtil
6 | import com.ssimagepicker.app.R
7 | import com.ssimagepicker.app.databinding.ActivityFragmentDemoBinding
8 |
9 | class FragmentDemoActivity : AppCompatActivity() {
10 |
11 | private lateinit var binding: ActivityFragmentDemoBinding
12 |
13 | override fun onCreate(savedInstanceState: Bundle?) {
14 | super.onCreate(savedInstanceState)
15 | binding = DataBindingUtil.setContentView(this, R.layout.activity_fragment_demo)
16 | title = getString(R.string.fragment_demo)
17 | supportFragmentManager.beginTransaction().add(R.id.frame_layout, DemoFragment())
18 | .commit()
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/ImageDataAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.net.Uri
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.ssimagepicker.app.R
9 | import com.ssimagepicker.app.databinding.ListItemImageDataBinding
10 | import com.ssimagepicker.app.loadImage
11 |
12 | /**
13 | * ImageDataAdapter class to display list of picked images from the picker.
14 | */
15 | class ImageDataAdapter(private val imageList: List) :
16 | RecyclerView.Adapter() {
17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageDataViewHolder {
18 | val binding: ListItemImageDataBinding = DataBindingUtil.inflate(
19 | LayoutInflater.from(parent.context), R.layout.list_item_image_data, parent, false
20 | )
21 | return ImageDataViewHolder(binding)
22 | }
23 |
24 | override fun onBindViewHolder(holder: ImageDataViewHolder, position: Int) {
25 | holder.binding.imageView.loadImage(imageList[position])
26 | }
27 |
28 | override fun getItemCount(): Int {
29 | return imageList.size
30 | }
31 |
32 | inner class ImageDataViewHolder(val binding: ListItemImageDataBinding) :
33 | RecyclerView.ViewHolder(binding.root)
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/LaunchActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.databinding.DataBindingUtil
8 | import com.ssimagepicker.app.R
9 | import com.ssimagepicker.app.databinding.ActivityLaunchBinding
10 | import com.ssimagepicker.app.enableEdgeToEdge
11 |
12 | class LaunchActivity : AppCompatActivity(), View.OnClickListener {
13 |
14 | private lateinit var binding: ActivityLaunchBinding
15 |
16 | override fun onCreate(savedInstanceState: Bundle?) {
17 | super.onCreate(savedInstanceState)
18 | binding = DataBindingUtil.setContentView(this, R.layout.activity_launch)
19 | binding.clickHandler = this
20 | setUpToolbar()
21 | enableEdgeToEdge(binding.toolbar.root)
22 | }
23 |
24 | override fun onClick(v: View) {
25 | when (v.id) {
26 | R.id.activity_button -> {
27 | goToScreen(MainActivity::class.java)
28 | }
29 | R.id.fragment_button -> {
30 | goToScreen(FragmentDemoActivity::class.java)
31 | }
32 | }
33 | }
34 |
35 | private fun goToScreen(activity: Class) {
36 | startActivity(Intent(this, activity))
37 | }
38 |
39 | private fun setUpToolbar() {
40 | binding.toolbar.apply {
41 | title = this@LaunchActivity.title.toString()
42 | imageBackButton.visibility = View.GONE
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.net.Uri
4 | import android.os.Bundle
5 | import android.view.View
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.databinding.DataBindingUtil
8 | import com.app.imagepickerlibrary.ImagePicker
9 | import com.app.imagepickerlibrary.ImagePicker.Companion.registerImagePicker
10 | import com.app.imagepickerlibrary.listener.ImagePickerResultListener
11 | import com.app.imagepickerlibrary.model.ImageProvider
12 | import com.app.imagepickerlibrary.model.PickerType
13 | import com.app.imagepickerlibrary.ui.bottomsheet.SSPickerOptionsBottomSheet
14 | import com.ssimagepicker.app.PickerOptions
15 | import com.ssimagepicker.app.R
16 | import com.ssimagepicker.app.databinding.ActivityMainBinding
17 | import com.ssimagepicker.app.enableEdgeToEdge
18 | import com.ssimagepicker.app.isAtLeast11
19 |
20 | /**
21 | * MainActivity which displays all the functionality of the ImagePicker library. All the attributes are modified with the ui.
22 | */
23 | class MainActivity : AppCompatActivity(), View.OnClickListener,
24 | SSPickerOptionsBottomSheet.ImagePickerClickListener,
25 | ImagePickerResultListener, PickerOptionsBottomSheet.PickerOptionsListener {
26 |
27 | companion object {
28 | private const val IMAGE_LIST = "IMAGE_LIST"
29 | }
30 |
31 | private lateinit var binding: ActivityMainBinding
32 | private val imagePicker: ImagePicker by lazy {
33 | registerImagePicker(this@MainActivity)
34 | }
35 | private val imageList = mutableListOf()
36 | private val imageDataAdapter = ImageDataAdapter(imageList)
37 | private var pickerOptions = PickerOptions.default()
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 | binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
42 | title = getString(R.string.activity_demo)
43 | binding.clickHandler = this
44 | setUpToolbar()
45 | setUI(savedInstanceState)
46 | enableEdgeToEdge(binding.toolbar.root)
47 | }
48 |
49 | private fun setUI(savedInstanceState: Bundle?) {
50 | binding.imageRecyclerView.adapter = imageDataAdapter
51 | if (savedInstanceState != null && savedInstanceState.containsKey(IMAGE_LIST)) {
52 | val uriList: List =
53 | savedInstanceState.getParcelableArrayList(IMAGE_LIST) ?: listOf()
54 | updateImageList(uriList)
55 | }
56 | }
57 |
58 | override fun onClick(v: View) {
59 | when (v.id) {
60 | R.id.options_button -> {
61 | openPickerOptions()
62 | }
63 | R.id.open_picker_button -> {
64 | openImagePicker()
65 | }
66 | R.id.open_sheet_button -> {
67 | val fragment =
68 | SSPickerOptionsBottomSheet.newInstance(R.style.CustomPickerBottomSheet)
69 | fragment.show(supportFragmentManager, SSPickerOptionsBottomSheet.BOTTOM_SHEET_TAG)
70 | }
71 | }
72 | }
73 |
74 | /**
75 | * This method receives the selected picker type option from the bottom sheet.
76 | */
77 | override fun onImageProvider(provider: ImageProvider) {
78 | when (provider) {
79 | ImageProvider.GALLERY -> {
80 | pickerOptions = pickerOptions.copy(pickerType = PickerType.GALLERY)
81 | openImagePicker()
82 | }
83 | ImageProvider.CAMERA -> {
84 | pickerOptions = pickerOptions.copy(pickerType = PickerType.CAMERA)
85 | openImagePicker()
86 | }
87 | ImageProvider.NONE -> {
88 | //User has pressed cancel show anything or just leave it blank.
89 | }
90 | }
91 | }
92 |
93 | /**
94 | * Opens the options for picker. The picker option is bottom sheet with many input parameters.
95 | */
96 | private fun openPickerOptions() {
97 | val fragment = PickerOptionsBottomSheet.newInstance(pickerOptions)
98 | fragment.setClickListener(this)
99 | fragment.show(supportFragmentManager, PickerOptionsBottomSheet.BOTTOM_SHEET_TAG)
100 | }
101 |
102 | /**
103 | * Once the picker options are selected in bottom sheet
104 | * we will receive the latest picker options in this method
105 | */
106 | override fun onPickerOptions(pickerOptions: PickerOptions) {
107 | this.pickerOptions = pickerOptions
108 | openImagePicker()
109 | }
110 |
111 | /**
112 | * Open the image picker according to picker type and the ui options.
113 | * The new system picker is only available for Android 13+.
114 | */
115 | private fun openImagePicker() {
116 | imagePicker
117 | .title("My Picker")
118 | .multipleSelection(pickerOptions.allowMultipleSelection, pickerOptions.maxPickCount)
119 | .showCountInToolBar(pickerOptions.showCountInToolBar)
120 | .showFolder(pickerOptions.showFolders)
121 | .cameraIcon(pickerOptions.showCameraIconInGallery)
122 | .doneIcon(pickerOptions.isDoneIcon)
123 | .allowCropping(pickerOptions.openCropOptions)
124 | .compressImage(pickerOptions.compressImage)
125 | .maxImageSize(pickerOptions.maxPickSizeMB)
126 | .extension(pickerOptions.pickExtension)
127 | .aspectRatio(pickerOptions.aspectRatio)
128 | if (isAtLeast11()) {
129 | imagePicker.systemPicker(pickerOptions.openSystemPicker)
130 | }
131 | imagePicker.open(pickerOptions.pickerType)
132 | }
133 |
134 | /**
135 | * Single Selection and the image captured from camera will be received in this method.
136 | */
137 | override fun onImagePick(uri: Uri?) {
138 | uri?.let { updateImageList(listOf(it)) }
139 | }
140 |
141 | /**
142 | * Multiple Selection uris will be received in this method
143 | */
144 | override fun onMultiImagePick(uris: List?) {
145 | if (!uris.isNullOrEmpty()) {
146 | updateImageList(uris)
147 | }
148 | }
149 |
150 | private fun updateImageList(list: List) {
151 | imageList.clear()
152 | imageList.addAll(list)
153 | imageDataAdapter.notifyDataSetChanged()
154 | }
155 |
156 | override fun onSaveInstanceState(outState: Bundle) {
157 | outState.putParcelableArrayList(IMAGE_LIST, ArrayList(imageList))
158 | super.onSaveInstanceState(outState)
159 | }
160 |
161 | private fun setUpToolbar() {
162 | binding.toolbar.apply {
163 | title = this@MainActivity.title.toString()
164 | clickListener = View.OnClickListener {
165 | onBackPressedDispatcher.onBackPressed()
166 | }
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/ssimagepicker/app/ui/PickerOptionsBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app.ui
2 |
3 | import android.app.Dialog
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import android.widget.Toast
9 | import androidx.core.os.bundleOf
10 | import com.app.imagepickerlibrary.model.PickExtension
11 | import com.app.imagepickerlibrary.model.PickerType
12 | import com.google.android.material.bottomsheet.BottomSheetBehavior
13 | import com.google.android.material.bottomsheet.BottomSheetDialog
14 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
15 | import com.ssimagepicker.app.PickerOptions
16 | import com.ssimagepicker.app.R
17 | import com.ssimagepicker.app.databinding.BottomSheetPickerOptionsBinding
18 |
19 | /**
20 | * PickerOptionsBottomSheet to display picker option related to new Image Picker.
21 | */
22 | class PickerOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
23 | companion object {
24 | const val BOTTOM_SHEET_TAG = "PICKER_BOTTOM_SHEET_TAG"
25 | const val PICKER_OPTIONS = "PICKER_OPTIONS"
26 |
27 | fun newInstance(pickerOptions: PickerOptions): PickerOptionsBottomSheet {
28 | val bundle = bundleOf(PICKER_OPTIONS to pickerOptions)
29 | val pickerOptionsBottomSheet = PickerOptionsBottomSheet()
30 | pickerOptionsBottomSheet.arguments = bundle
31 | return pickerOptionsBottomSheet
32 | }
33 | }
34 |
35 | private var mListener: PickerOptionsListener? = null
36 | private lateinit var binding: BottomSheetPickerOptionsBinding
37 |
38 | override fun getTheme(): Int {
39 | return R.style.PickerOptionsBottomSheetDialog
40 | }
41 |
42 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
43 | return BottomSheetDialog(requireContext(), this.theme)
44 | }
45 |
46 | override fun onCreateView(
47 | inflater: LayoutInflater,
48 | container: ViewGroup?,
49 | savedInstanceState: Bundle?
50 | ): View {
51 | dialog?.setOnShowListener { dialog ->
52 | val bottomSheetDialog = dialog as BottomSheetDialog
53 | val bottomSheetInternal =
54 | bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) as View
55 | BottomSheetBehavior.from(bottomSheetInternal).state =
56 | BottomSheetBehavior.STATE_HALF_EXPANDED
57 | }
58 | binding = BottomSheetPickerOptionsBinding.inflate(inflater, container, false)
59 | return binding.root
60 | }
61 |
62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 | super.onViewCreated(view, savedInstanceState)
64 | setUI()
65 | }
66 |
67 | /**
68 | * Setting previously selected options with picker option object
69 | */
70 | private fun setUI() {
71 | binding.apply {
72 | clickHandler = this@PickerOptionsBottomSheet
73 | val pickerOptions = arguments?.getParcelable(PICKER_OPTIONS) ?: PickerOptions.default()
74 |
75 | val checkedPickerTypeId = if (pickerOptions.pickerType == PickerType.CAMERA) {
76 | R.id.camera_button
77 | } else {
78 | R.id.gallery_button
79 | }
80 | pickerTypeGroup.check(checkedPickerTypeId)
81 |
82 | multiSelectionSwitch.isChecked = pickerOptions.allowMultipleSelection
83 | countToolbarSwitch.isChecked = pickerOptions.showCountInToolBar
84 | folderSwitch.isChecked = pickerOptions.showFolders
85 | cameraInGallery.isChecked = pickerOptions.showCameraIconInGallery
86 | doneSwitch.isChecked = pickerOptions.isDoneIcon
87 | openCropSwitch.isChecked = pickerOptions.openCropOptions
88 | systemPickerSwitch.isChecked = pickerOptions.openSystemPicker
89 | compressImageSwitch.isChecked = pickerOptions.compressImage
90 |
91 | val checkedExtensionId = when (pickerOptions.pickExtension) {
92 | PickExtension.PNG -> R.id.png_button
93 | PickExtension.JPEG -> R.id.jpeg_button
94 | PickExtension.WEBP -> R.id.webp_button
95 | PickExtension.ALL -> R.id.all_button
96 | }
97 | extensionTypeGroup.check(checkedExtensionId)
98 |
99 | pickCountSlider.valueFrom = 1f
100 | pickCountSlider.valueTo = 15f
101 | pickCountSlider.stepSize = 1f
102 | pickCountSlider.value = pickerOptions.maxPickCount.toFloat()
103 |
104 | if (pickerOptions.maxPickSizeMB != Float.MAX_VALUE) {
105 | pickSizeTie.setText(pickerOptions.maxPickSizeMB.toString())
106 | }
107 | }
108 | }
109 |
110 | override fun onDetach() {
111 | super.onDetach()
112 | mListener = null
113 | }
114 |
115 | override fun onClick(v: View) {
116 | when (v.id) {
117 | R.id.done_text -> {
118 | if (isDataValid()) {
119 | openImagePicker()
120 | }
121 | }
122 | }
123 | }
124 |
125 | private fun openImagePicker() {
126 | val countValue = binding.pickCountSlider.value.toInt()
127 | val sizeValue = binding.pickSizeTie.text.toString().toFloat()
128 | val pickExtension = when (binding.extensionTypeGroup.checkedButtonId) {
129 | R.id.all_button -> PickExtension.ALL
130 | R.id.png_button -> PickExtension.PNG
131 | R.id.jpeg_button -> PickExtension.JPEG
132 | R.id.webp_button -> PickExtension.WEBP
133 | else -> PickExtension.ALL
134 | }
135 | val pickerType =
136 | if (binding.pickerTypeGroup.checkedButtonId == R.id.gallery_button) {
137 | PickerType.GALLERY
138 | } else {
139 | PickerType.CAMERA
140 | }
141 | val pickerOptions = PickerOptions(
142 | pickerType = pickerType,
143 | showCountInToolBar = binding.countToolbarSwitch.isChecked,
144 | showFolders = binding.folderSwitch.isChecked,
145 | allowMultipleSelection = binding.multiSelectionSwitch.isChecked,
146 | maxPickCount = countValue,
147 | maxPickSizeMB = sizeValue,
148 | pickExtension = pickExtension,
149 | showCameraIconInGallery = binding.cameraInGallery.isChecked,
150 | isDoneIcon = binding.doneSwitch.isChecked,
151 | openCropOptions = binding.openCropSwitch.isChecked,
152 | openSystemPicker = binding.systemPickerSwitch.isChecked,
153 | compressImage = binding.compressImageSwitch.isChecked,
154 | aspectRatio = null
155 | )
156 | dismiss()
157 | mListener?.onPickerOptions(pickerOptions)
158 | }
159 |
160 | /**
161 | * Check whether the max pick count and max size is valid or not.
162 | * If all data is valid it will return true.
163 | */
164 | private fun isDataValid(): Boolean {
165 | val sizeValue = binding.pickSizeTie.text.toString().toFloatOrNull()
166 | if (sizeValue == null || sizeValue <= 0) {
167 | Toast.makeText(requireContext(), R.string.error_size_value, Toast.LENGTH_LONG).show()
168 | return false
169 | }
170 | return true
171 | }
172 |
173 | fun setClickListener(listener: PickerOptionsListener) {
174 | this.mListener = listener
175 | }
176 |
177 | interface PickerOptionsListener {
178 | fun onPickerOptions(pickerOptions: PickerOptions)
179 | }
180 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/drawable_bottom_sheet_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back_ios.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_check_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_button_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_button_text_color.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_switch_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/selector_switch_track.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/font/poppins_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_fragment_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_launch.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
17 |
18 |
24 |
25 |
36 |
37 |
47 |
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
17 |
18 |
24 |
25 |
36 |
37 |
47 |
48 |
58 |
59 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
18 |
19 |
28 |
29 |
37 |
38 |
46 |
47 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/list_item_image_data.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/toolbar_app.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
14 |
15 |
16 |
19 |
20 |
26 |
27 |
30 |
31 |
41 |
42 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values-night/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFFFFF
4 | #222222
5 | @color/black
6 | @color/white
7 | @color/white
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
21 |
22 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2D647D
4 | #2D647D
5 | #46A0C6
6 | #FF000000
7 | #FFFFFFFF
8 | #FFFFFF
9 | #194F54
10 | #808080
11 | @color/colorPrimary
12 | @color/black
13 | @color/white
14 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SSImagePicker
3 |
4 | "Camera"
5 | "Gallery"
6 | Cancel
7 | Allow multi selection
8 | Show count in toolbar
9 | Picker type
10 | Show folder
11 | Camera icon in gallery
12 | Max pick count
13 | Max size (MB)
14 | Done icon
15 | Enable crop
16 | System picker (Android 11+)
17 | Compress image
18 | Extension
19 | All
20 | PNG
21 | JPEG
22 | WEBP
23 | Choose Picker Type
24 | Open image picker
25 | Enter valid max size value. It should be greater than 0.
26 | Customize Picker Options
27 | Picker Options
28 | Done
29 | Activity Demo
30 | Fragment Demo
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
13 |
21 |
22 |
27 |
28 |
31 |
32 |
37 |
38 |
43 |
44 |
53 |
54 |
63 |
64 |
70 |
71 |
79 |
80 |
88 |
89 |
95 |
96 |
99 |
100 |
106 |
107 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/file_path.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/ssimagepicker/app/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.ssimagepicker.app
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.android.application) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.android.library) apply false
6 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | repositories {
2 | mavenCentral()
3 | }
4 |
5 | plugins {
6 | `kotlin-dsl`
7 | }
8 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/App.kt:
--------------------------------------------------------------------------------
1 | object App {
2 | const val ID = "com.ssimagepicker.app"
3 | const val MULTI_DEX = true
4 |
5 | object BuildType {
6 | const val RELEASE = "release"
7 | const val DEBUG = "debug"
8 | }
9 |
10 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Extensions.kt:
--------------------------------------------------------------------------------
1 | import org.gradle.api.Project
2 |
3 | /**
4 | * Extension to add file tree dependency
5 | */
6 | fun Project.defaultFileTree() = fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/Plugins.kt:
--------------------------------------------------------------------------------
1 | object Plugins {
2 | const val ANDROID_APPLICATION = "com.android.application"
3 |
4 | object Kotlin {
5 | const val ANDROID = "android"
6 | const val KAPT = "kapt"
7 | const val PARCELIZE = "kotlin-parcelize"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/docs/migration.md:
--------------------------------------------------------------------------------
1 | # Migration
2 |
3 | - [**1.8** :arrow_right: **2.0**](#from-18-to-20)
4 |
5 | ## From 1.8 to 2.0
6 | 1. Remove all the reference of the **ImagePickerActivityClass**. Follow the [How It Works](../README.md#books-how-it-works) section for initialization.
7 | 2. After doing that just open the image picker activity from your desired location via registering for ImagePicker object. The open method takes the type of picker which which will open the desired picker type.
8 | ```kotlin
9 | private val imagePicker: ImagePicker = registerImagePicker(callback = this)
10 |
11 | override fun onCreate(savedInstanceState: Bundle?) {
12 | super.onCreate(savedInstanceState)
13 | ...
14 | }
15 |
16 | //To display the picker screen call open method on image picker object passing the picker type.
17 | imagePicker.open(PickerType.GALLERY)
18 | ```
19 | 3. You can add customization like multiple selection, max image size via the available methods in the **`ImagePicker`** class.
20 | ```kotlin
21 | imagePicker
22 | .title("My Picker")
23 | .multipleSelection(enable = true, maxCount = 5)
24 | .showCountInToolBar(false)
25 | .showFolder(true)
26 | .cameraIcon(true)
27 | .doneIcon(true)
28 | .allowCropping(true)
29 | .compressImage(false)
30 | .maxImageSize(2)
31 | .extension(PickExtension.JPEG)
32 | imagePicker.open(PickerType.GALLERY)
33 | ```
34 | 4. Get the result by implementing the **`ImagePickerResultListener`**.
35 | ```kotlin
36 | override fun onImagePick(uri: Uri?) {
37 | //For single selection or image captured with camera.
38 | }
39 |
40 | override fun onMultiImagePick(uris: List?) {
41 | //For multiple image selection from gallery.
42 | }
43 | ```
44 | 5. The **`ImagePickerBottomsheet`** is renamed to **`SSPickerOptionsBottomSheet`**. You can open the bottom sheet like this.
45 | ```kotlin
46 | val pickerOptionBottomSheet = SSPickerOptionsBottomSheet.newInstance()
47 | pickerOptionBottomSheet.show(supportFragmentManager,"tag")
48 | ```
49 | 6. To get bottom sheet selection result override **`SSPickerOptionsBottomSheet.ImagePickerClickListener`** in your activity or fragment. You can receive the selected provider type in the **`onImageProvider`** method.
50 | ```kotlin
51 | class MainActivity : AppCompatActivity(), SSPickerOptionsBottomSheet.ImagePickerClickListener {
52 |
53 | override fun onImageProvider(provider: ImageProvider) {
54 | when (provider) {
55 | ImageProvider.GALLERY -> {
56 | //Open gallery picker
57 | }
58 | ImageProvider.CAMERA -> {
59 | //Open camera picker
60 | }
61 | ImageProvider.NONE -> {}
62 | }
63 | }
64 |
65 | }
66 | ```
67 | 7. The **`loadImage()`** extension function on the **`AppCompatImageView`** has been removed. If you are using the function you can add the function with below implementation. The **`loadImage()`** extension function used [Glide](https://github.com/bumptech/glide) internally to load the image.
68 | ```kotlin
69 | fun AppCompatImageView.loadImage(url: Any?, isCircle: Boolean = false, isRoundedCorners: Boolean = false, func: RequestOptions.() -> Unit) {
70 | url?.let { image ->
71 | val options = RequestOptions().placeholder(R.mipmap.ic_launcher_round)
72 | .error(R.mipmap.ic_launcher_round)
73 | .skipMemoryCache(true)
74 | .diskCacheStrategy(DiskCacheStrategy.ALL)
75 | .apply(func)
76 | val requestBuilder = Glide.with(context).load(image).apply(options)
77 | if (isCircle) {
78 | requestBuilder.apply(options.circleCrop())
79 | } else if(isRoundedCorners){
80 | requestBuilder.apply(options.transforms(CenterCrop(), RoundedCorners(18)))
81 | }
82 | requestBuilder.into(this)
83 | }
84 | }
85 | ```
--------------------------------------------------------------------------------
/docs/options_bottom_sheet_customization.md:
--------------------------------------------------------------------------------
1 | # Picker Options Bottom Sheet UI Customization :
2 | * To customize Picker Option Bottom Sheet UI, override the default bottom sheet theme **`SSImagePickerBaseBottomSheetDialog`** in your styles.xml or themes.xml. Make sure that the parent theme is set to **`SSImagePickerBaseBottomSheetDialog`**.
3 |
4 | ```xml
5 |
8 | ```
9 | * Override one or more bottom sheet picker option ui attributes from the available [picker option ui attributes table](#picker-option-bottom-sheet-ui-attributes-table). For example,
10 | ```xml
11 |
16 |
17 |
20 |
25 |
26 |
27 |
32 | ```
33 | * Pass the custom style while creating the bottom sheet instance.
34 | ```kotlin
35 | val fragment = SSPickerOptionsBottomSheet.newInstance(R.style.CustomPickerBottomSheet)
36 | fragment.show(supportFragmentManager,SSPickerOptionsBottomSheet.BOTTOM_SHEET_TAG)
37 | ```
38 |
39 | ## Picker Option Bottom Sheet UI Attributes Table
40 |
41 | | UI attribute | Format | Default value/style | Description |
42 | |------------------------------------|-----------------|---------------------------------------------|-----------------------------------------|
43 | | **ssSheetCameraText** | string | `"Camera"` | Text for the camera button. |
44 | | **ssSheetGalleryText** | string | `"Gallery"` | Text for the gallery button. |
45 | | **ssSheetCancelText** | string | `"Cancel"` | Text for the cancel button. |
46 | | **ssSheetCameraButtonBackground** | reference/color | @drawable/bg_ss_picker_option_button | Background for the camera button. |
47 | | **ssSheetGalleryButtonBackground** | reference/color | @drawable/bg_ss_picker_option_button | Background for the gallery button. |
48 | | **ssSheetCancelButtonBackground** | reference/color | @drawable/bg_ss_picker_option_button_cancel | Background for the cancel button. |
49 | | **ssSheetCameraViewStyle** | reference | SSBottomSheetTextViewStyle | Text view style for the camera button. |
50 | | **ssSheetGalleryViewStyle** | reference | SSBottomSheetTextViewStyle | Text view style for the gallery button. |
51 | | **ssSheetCancelViewStyle** | reference | SSBottomSheetTextViewStyle | Text view style for the cancel button. |
52 | | **ssSheetBackground** | reference/color | @drawable/bg_ss_picker_option | Background for entire bottom sheet. |
53 |
--------------------------------------------------------------------------------
/docs/picker_ui_customization.md:
--------------------------------------------------------------------------------
1 | # Picker UI Customization
2 | * To customize ImagePickerActivity UI, override the default **`SSImagePicker`** theme in your styles.xml or themes.xml. Make sure that the parent theme is set to **`SSImagePicker`**.
3 |
4 | ```xml
5 |
8 | ```
9 | * Override one or more ui attributes for the picker screen from the available [ui attributes table](#picker-screen-ui-attributes-table). For example,
10 | ```xml
11 |
16 |
17 |
20 |
25 |
26 |
27 |
32 | ```
33 | * Set your custom theme into AndroidManifest.xml where you have defined the ImagePickerActivity
34 | ```xml
35 |
39 | ```
40 |
41 | ## Picker Screen UI Attributes Table
42 |
43 | | UI attribute | Format | Default value/style | Description |
44 | |-------------------------------------|-----------------|-------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
45 | | **ssStatusBarColor** | color | `#2D647D` | Status bar color |
46 | | **ssStatusBarLightMode** | boolean | `true` | Used to determine status bar is in light or dark mode |
47 | | **ssToolbarBackground** | reference/color | `#2D647D` | Toolbar background |
48 | | **ssToolbarBackIcon** | reference | @drawable/ic_ss_arrow_back | Toolbar back button drawable image |
49 | | **ssToolbarTextAppearance** | reference | SSToolbarTitleTextAppearance | Toolbar title text appearance |
50 | | **ssToolbarCameraIcon** | reference | @drawable/ic_ss_camera | Toolbar camera button drawable image. You can hide the icon via **cameraIcon(false)** in picker configuration. |
51 | | **ssToolbarDoneIcon** | reference | @drawable/ic_ss_done | Toolbar done button drawable image. By default the icon will be displayed for done. |
52 | | **ssToolbarDoneText** | string | `"Done"` | Toolbar done button text. You can show the text via **doneIcon(false)** in picker configuration |
53 | | **ssToolbarDoneTextAppearance** | reference | SSToolbarDoneTextAppearance | Toolbar done button text appearance. |
54 | | **ssPickerBackground** | reference/color | `#FFFFFF` | Picker screen surface background. |
55 | | **ssProgressIndicatorStyle** | reference | SSProgressIndicatorStyle | Style for the circular progress indicator which is displayed while the images are being fetched. |
56 | | **ssFolderTextViewStyle** | reference | SSFolderTextStyle | Text view style for the folder name which appears on the folder. |
57 | | **ssNoDataText** | string | `"No data found"` | Text which is showed to user when there is no image in a folder or no images are found. |
58 | | **ssNoDataTextViewStyle** | reference | SSNoDataTextStyle | Text view style for the no data text view. |
59 | | **ssImagePickerLimitText** | string | `"Maximum selection reached"` | The text which will be displayed to user when the user tries to select more images then the specified pick limit. This message will be displayed as **Toast**. |
60 | | **ssPickerGridCount** | integer | `2` | Grid count for the picker screen in **Portrait** mode. It will be same for both folder and image. |
61 | | **ssPickerGridCountLandscape** | integer | `4` | Grid count for the picker screen in **Landscape** mode. It will be same for both folder and image. |
62 | | **ssImageSelectIcon** | reference | @drawable/ic_ss_check_circle | Drawable icon which is used to indicate that the image is selected. It is displayed on top of selected image in multiple selection mode. |
63 | | **ssImageZoomIcon** | reference | @drawable/ic_ss_zoom_eye | Drawable icon which is used to open image in dialog mode to see the image in full view mode. |
64 | | **ssUCropToolbarColor** | color | Picker toolbar background (ssToolbarBackground) | UCrop activity toolbar color. The default value is ssToolbarBackground so that both can be same. You can override the UCrop value by specifying your color. |
65 | | **ssUCropStatusBarColor** | color | Picker status bar color (ssStatusBarColor) | UCrop activity status bar color. The default value is ssStatusBarColor so that both can be same. You can override the UCrop value by specifying your color. |
66 | | **ssUCropToolbarWidgetColor** | color | `#FFFFFF` | UCrop activity toolbar widget color. |
67 | | **ssUCropActiveControlWidgetColor** | color | `#2D647D` | UCrop activity active control widget color. |
--------------------------------------------------------------------------------
/gifs/camera_picker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/camera_picker.gif
--------------------------------------------------------------------------------
/gifs/crop_options.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/crop_options.gif
--------------------------------------------------------------------------------
/gifs/extension_options.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/extension_options.gif
--------------------------------------------------------------------------------
/gifs/gallery_multi_selection.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/gallery_multi_selection.gif
--------------------------------------------------------------------------------
/gifs/gallery_picker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/gallery_picker.gif
--------------------------------------------------------------------------------
/gifs/picker_option_bottom_sheet.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/picker_option_bottom_sheet.gif
--------------------------------------------------------------------------------
/gifs/system_photo_picker.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gifs/system_photo_picker.gif
--------------------------------------------------------------------------------
/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 -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | compile-sdk = "35"
3 | min-sdk = "21"
4 | target-sdk = "35"
5 | version-code = "1"
6 | version-name = "1.8"
7 |
8 | activity-ktx = "1.10.1"
9 | androidx-junit = "1.2.1"
10 | appcompat = "1.7.1"
11 | coil = "2.6.0"
12 | constraintlayout = "2.2.1"
13 | core-ktx = "1.16.0"
14 | core-testing = "2.2.0"
15 | espresso-contrib = "3.6.1"
16 | espresso-core = "3.6.1"
17 | fragment-ktx = "1.8.8"
18 | glide = "4.16.0"
19 | junit = "4.13.2"
20 | lifecycle-livedata = "2.9.1"
21 | lifecycle-livedata-ktx = "2.9.1"
22 | material = "1.12.0"
23 | recyclerview = "1.4.0"
24 | rules = "1.6.1"
25 | runner = "1.6.2"
26 | sdp-android = "1.1.0"
27 | ssp-android = "1.1.0"
28 | ucrop = "2.2.8"
29 | androidGradlePlugin = "8.6.1"
30 | androidLibraryPlugin = "8.6.1"
31 | kotlinGradlePlugin = "2.0.21"
32 |
33 | [libraries]
34 | androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "activity-ktx" }
35 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
36 | androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
37 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx" }
38 | androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "core-testing" }
39 | androidx-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espresso-contrib" }
40 | androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" }
41 | androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment-ktx" }
42 | androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
43 | androidx-lifecycle-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "lifecycle-livedata" }
44 | androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
45 | androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
46 | coil = { module = "io.coil-kt:coil", version.ref = "coil" }
47 | compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
48 | glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
49 | junit = { module = "junit:junit", version.ref = "junit" }
50 | lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle-livedata-ktx" }
51 | material = { module = "com.google.android.material:material", version.ref = "material" }
52 | rules = { module = "androidx.test:rules", version.ref = "rules" }
53 | sdp-android = { module = "com.intuit.sdp:sdp-android", version.ref = "sdp-android" }
54 | ssp-android = { module = "com.intuit.ssp:ssp-android", version.ref = "ssp-android" }
55 | ucrop = { module = "com.github.yalantis:ucrop", version.ref = "ucrop" }
56 |
57 | [plugins]
58 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
59 | android-library = { id = "com.android.library", version.ref = "androidLibraryPlugin" }
60 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlinGradlePlugin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 30 20:15:07 IST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/imagepickerlibrary/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/imagepickerlibrary/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.library")
3 | id("kotlin-android")
4 | id("kotlin-kapt")
5 | id("kotlin-parcelize")
6 | id("maven-publish")
7 | }
8 |
9 | afterEvaluate {
10 | publishing {
11 | publications {
12 | register("release") {
13 | groupId = "com.github.SimformSolutionsPvtLtd"
14 | artifactId = "SSImagePicker"
15 | version = "2.4"
16 |
17 | from(components["release"])
18 | }
19 | }
20 | }
21 | }
22 |
23 | android {
24 | compileSdk = 34
25 | namespace = "com.app.imagepickerlibrary"
26 | defaultConfig {
27 | minSdk = libs.versions.min.sdk.get().toInt()
28 | targetSdk = libs.versions.target.sdk.get().toInt()
29 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
30 | consumerProguardFiles("consumer-rules.pro")
31 | }
32 |
33 | buildFeatures {
34 | dataBinding = true
35 | buildConfig = true
36 | }
37 |
38 | buildTypes {
39 | getByName("release") {
40 | isMinifyEnabled = false
41 | proguardFiles(
42 | getDefaultProguardFile("proguard-android-optimize.txt"),
43 | "proguard-rules.pro"
44 | )
45 | }
46 | }
47 |
48 | compileOptions {
49 | sourceCompatibility = JavaVersion.VERSION_17
50 | targetCompatibility = JavaVersion.VERSION_17
51 | }
52 |
53 | kotlinOptions {
54 | jvmTarget = JavaVersion.VERSION_17.toString()
55 | }
56 | }
57 |
58 | dependencies {
59 | implementation(libs.androidx.core.ktx)
60 | implementation(libs.androidx.appcompat)
61 | implementation(libs.material)
62 | implementation(libs.sdp.android)
63 | implementation(libs.ssp.android)
64 | implementation(libs.androidx.fragment.ktx)
65 | implementation(libs.ucrop)
66 | implementation(libs.coil)
67 | implementation(libs.androidx.recyclerview)
68 | testImplementation(libs.junit)
69 | androidTestImplementation(libs.androidx.junit)
70 | androidTestImplementation(libs.androidx.espresso.core)
71 | }
72 |
--------------------------------------------------------------------------------
/imagepickerlibrary/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/imagepickerlibrary/consumer-rules.pro
--------------------------------------------------------------------------------
/imagepickerlibrary/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
--------------------------------------------------------------------------------
/imagepickerlibrary/src/androidTest/java/com/app/imagepickerlibrary/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary
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.app.imagepickerlibrary.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
16 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary
2 |
3 | internal const val dateFormatForTakePicture = "yyyyMMdd_HHmmss"
4 | internal const val EXTRA_IMAGE_PICKER_CONFIG = "extra-picker-config"
5 | internal const val MB_TO_BYTE_MULTIPLIER = 1e+6
6 | internal const val MAX_PICK_LIMIT = 15
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ImagePickerFileProvider.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary
2 |
3 | import androidx.core.content.FileProvider
4 |
5 | class ImagePickerFileProvider : FileProvider()
6 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/listener/ImagePickerResultListener.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.listener
2 |
3 | import android.net.Uri
4 |
5 | /**
6 | * Result listener for the image picker.
7 | */
8 | interface ImagePickerResultListener {
9 | fun onImagePick(uri: Uri?)
10 | fun onMultiImagePick(uris: List?)
11 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/listener/ItemClickListener.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.listener
2 |
3 | import androidx.annotation.IdRes
4 |
5 | /**
6 | * Item click listener for the recycler view.
7 | */
8 | interface ItemClickListener {
9 | /**
10 | * @param[item] The data which needed to pass
11 | * @param[position] The position of clicked item in recyclerview
12 | * @param[viewId] The id of view which was clicked
13 | */
14 | fun onItemClick(item: T, position: Int, @IdRes viewId: Int)
15 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/model/DataModels.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.model
2 |
3 | import android.net.Uri
4 | import android.os.Parcelable
5 | import android.provider.MediaStore
6 | import com.app.imagepickerlibrary.MAX_PICK_LIMIT
7 | import com.app.imagepickerlibrary.toByteSize
8 | import kotlinx.parcelize.Parcelize
9 | import java.util.Locale
10 |
11 | /**
12 | * PickerConfig to manage the picker. All the options are modified by ImagePicker builder.
13 | */
14 | @Parcelize
15 | internal data class PickerConfig(
16 | var pickerType: PickerType = PickerType.GALLERY,
17 | var pickerTitle: String = "Image Picker",
18 | var showCountInToolBar: Boolean = true,
19 | var showFolders: Boolean = true,
20 | var allowMultipleSelection: Boolean = false,
21 | var maxPickCount: Int = MAX_PICK_LIMIT,
22 | var maxPickSizeMB: Float = Float.MAX_VALUE,
23 | var pickExtension: PickExtension = PickExtension.ALL,
24 | var showCameraIconInGallery: Boolean = true,
25 | var isDoneIcon: Boolean = true,
26 | var openCropOptions: Boolean = false,
27 | var openSystemPicker: Boolean = false,
28 | var compressImage: Boolean = false,
29 | var compressQuality: Int = 75,
30 | var aspectRatio: AspectRatio? = null
31 | ) : Parcelable {
32 |
33 | companion object {
34 | fun defaultPicker(): PickerConfig {
35 | return PickerConfig()
36 | }
37 | }
38 |
39 | /**
40 | * Function to generate selection arguments for content resolver based on size and extension
41 | */
42 | fun generateSelectionArguments(): Pair> {
43 | val selection: StringBuilder = StringBuilder()
44 | val selectionArgs = mutableListOf()
45 | if (maxPickSizeMB != Float.MAX_VALUE) {
46 | selection.append("${MediaStore.Images.Media.SIZE} <= ?")
47 | selectionArgs.add(maxPickSizeMB.toByteSize().toString())
48 | }
49 | if (pickExtension != PickExtension.ALL) {
50 | if (selection.isNotEmpty()) {
51 | selection.append(" and ")
52 | }
53 | selection.append("${MediaStore.Images.Media.MIME_TYPE} =?")
54 | selectionArgs.add("image/${pickExtension.name.lowercase(Locale.getDefault())}")
55 | }
56 | return Pair(selection.toString(), selectionArgs.toTypedArray())
57 | }
58 | }
59 |
60 | /**
61 | * Type of picker that user wants to open.
62 | * User can also show the camera icon in GALLERY picker mode.
63 | */
64 | enum class PickerType { GALLERY, CAMERA }
65 |
66 | /**
67 | * Type of image that user wants to select.
68 | * By default ALL image will be displayed.
69 | */
70 | enum class PickExtension {
71 | PNG, JPEG, WEBP, ALL;
72 |
73 | /**
74 | * Internal function to get mime type of picked extension for the system launcher.
75 | */
76 | internal fun getMimeType(): String {
77 | return when (this) {
78 | PNG -> "image/png"
79 | JPEG -> "image/jpeg"
80 | WEBP -> "image/webp"
81 | ALL -> "image/*"
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * ImageProvider constants to determine user has selected which picker option from the bottom sheet.
88 | * This class is only related to bottom sheet which displays the picker options.
89 | */
90 | enum class ImageProvider {
91 | GALLERY,
92 | CAMERA,
93 | NONE
94 | }
95 |
96 | /**
97 | * This class represent an image in the picker.
98 | */
99 | @Parcelize
100 | internal data class Image(
101 | val id: Long,
102 | val uri: Uri,
103 | val name: String,
104 | val bucketId: Long,
105 | val bucketName: String,
106 | val size: Long,
107 | var isSelected: Boolean = false
108 | ) : Parcelable
109 |
110 | /**
111 | * This class represent a folder in the picker.
112 | * Folders are created on the bases of @param[bucketId]
113 | */
114 | @Parcelize
115 | internal data class Folder(
116 | val bucketId: Long,
117 | val bucketName: String,
118 | val uri: Uri,
119 | val images: List
120 | ) : Parcelable
121 |
122 | /**
123 | * This sealed class represent a state for picker
124 | * It contains the error, loading and success as state.
125 | */
126 | internal sealed class Result {
127 | data class Success(val data: T) : Result()
128 | data class Error(val exception: Exception) : Result()
129 | data object Loading : Result()
130 | }
131 |
132 | @Parcelize
133 | data class AspectRatio(val x: Float, val y : Float): Parcelable
134 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/adapter/BaseAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.annotation.LayoutRes
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import androidx.recyclerview.widget.RecyclerView
9 | import com.app.imagepickerlibrary.listener.ItemClickListener
10 |
11 | /**
12 | * BaseAdapter class to manage ImageAdapter and FolderAdapter
13 | * All the common functionalities related to recycler view item are implemented here.
14 | */
15 | internal abstract class BaseAdapter(protected val listener: ItemClickListener) :
16 | RecyclerView.Adapter.BaseVH>() {
17 | protected val itemList = mutableListOf()
18 |
19 | @LayoutRes
20 | abstract fun getLayoutId(): Int
21 |
22 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVH {
23 | val inflater = LayoutInflater.from(parent.context)
24 | val binding: ViewDataBinding =
25 | DataBindingUtil.inflate(inflater, getLayoutId(), parent, false)
26 | return BaseVH(binding)
27 | }
28 |
29 | override fun onBindViewHolder(holder: BaseVH, position: Int) {
30 | holder.bind(itemList[position])
31 | }
32 |
33 | override fun getItemCount() = itemList.size
34 |
35 | /**
36 | * Clearing previous image list item and adding all the new items
37 | * Using notifyDataSetChanged to manage both addition and removal of image items from list
38 | */
39 | fun setItemList(list: List) {
40 | itemList.clear()
41 | itemList.addAll(list)
42 | notifyDataSetChanged()
43 | }
44 |
45 | internal open inner class BaseVH(val binding: ViewDataBinding) :
46 | RecyclerView.ViewHolder(binding.root) {
47 | init {
48 | binding.root.setOnClickListener {
49 | listener.onItemClick(
50 | itemList[absoluteAdapterPosition],
51 | absoluteAdapterPosition,
52 | it.id
53 | )
54 | }
55 | }
56 |
57 | /**
58 | * This function is used to bind recycler data particular item wise.
59 | */
60 | fun bind(data: T) {
61 | setDataForListItemWithPosition(binding, data, absoluteAdapterPosition)
62 | binding.executePendingBindings()
63 | }
64 | }
65 |
66 | /**
67 | * This function is used to set data to list item.
68 | */
69 | open fun setDataForListItemWithPosition(
70 | binding: ViewDataBinding,
71 | data: T,
72 | adapterPosition: Int
73 | ) {
74 | }
75 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/adapter/FolderAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.adapter
2 |
3 | import androidx.databinding.ViewDataBinding
4 | import com.app.imagepickerlibrary.R
5 | import com.app.imagepickerlibrary.databinding.ListItemFolderBinding
6 | import com.app.imagepickerlibrary.listener.ItemClickListener
7 | import com.app.imagepickerlibrary.model.Folder
8 |
9 | /**
10 | * FolderAdapter class to display folder items.
11 | */
12 | internal class FolderAdapter(listener: ItemClickListener) : BaseAdapter(listener) {
13 | override fun getLayoutId(): Int = R.layout.list_item_folder
14 |
15 | override fun setDataForListItemWithPosition(
16 | binding: ViewDataBinding,
17 | data: Folder,
18 | adapterPosition: Int
19 | ) {
20 | super.setDataForListItemWithPosition(binding, data, adapterPosition)
21 | (binding as ListItemFolderBinding).folder = data
22 | }
23 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/adapter/ImageAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.core.view.isVisible
6 | import androidx.databinding.DataBindingUtil
7 | import androidx.databinding.ViewDataBinding
8 | import com.app.imagepickerlibrary.R
9 | import com.app.imagepickerlibrary.databinding.ListItemImageBinding
10 | import com.app.imagepickerlibrary.listener.ItemClickListener
11 | import com.app.imagepickerlibrary.model.Image
12 |
13 | /**
14 | * ImageAdapter class to display image items.
15 | */
16 | internal class ImageAdapter(listener: ItemClickListener) : BaseAdapter(listener) {
17 | override fun getLayoutId(): Int = R.layout.list_item_image
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseVH {
20 | val inflater = LayoutInflater.from(parent.context)
21 | val binding: ListItemImageBinding =
22 | DataBindingUtil.inflate(inflater, getLayoutId(), parent, false)
23 | return ImageVH(binding)
24 | }
25 |
26 | override fun setDataForListItemWithPosition(
27 | binding: ViewDataBinding,
28 | data: Image,
29 | adapterPosition: Int
30 | ) {
31 | super.setDataForListItemWithPosition(binding, data, adapterPosition)
32 | (binding as ListItemImageBinding).image = data
33 | binding.checkMark.isVisible = data.isSelected
34 | }
35 |
36 | internal inner class ImageVH(binding: ListItemImageBinding) : BaseVH(binding) {
37 | init {
38 | binding.imageZoom.setOnClickListener {
39 | listener.onItemClick(
40 | itemList[absoluteAdapterPosition],
41 | absoluteAdapterPosition,
42 | it.id
43 | )
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/bottomsheet/SSPickerOptionsBottomSheet.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.bottomsheet
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.annotation.StyleRes
10 | import androidx.core.os.bundleOf
11 | import com.app.imagepickerlibrary.R
12 | import com.app.imagepickerlibrary.databinding.BottomSheetImagePickerOptionsBinding
13 | import com.app.imagepickerlibrary.model.ImageProvider
14 | import com.google.android.material.bottomsheet.BottomSheetBehavior
15 | import com.google.android.material.bottomsheet.BottomSheetDialog
16 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment
17 |
18 | /**
19 | * SSPickerOptionsBottomSheet to display picker option related to Image Picker.
20 | * It displays a bottom sheet with options Gallery, Camera and Cancel.
21 | * The bottom sheet can be modified via SSImagePickerBaseBottomSheetDialog theme
22 | */
23 | class SSPickerOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
24 | companion object {
25 | const val BOTTOM_SHEET_TAG = "IMAGE_PICKER_BOTTOM_SHEET_TAG"
26 | const val THEME_ID = "theme_id"
27 | fun newInstance(@StyleRes themeId: Int): SSPickerOptionsBottomSheet {
28 | return SSPickerOptionsBottomSheet().apply {
29 | arguments = bundleOf(THEME_ID to themeId)
30 | }
31 | }
32 | }
33 |
34 | private var mListener: ImagePickerClickListener? = null
35 | private lateinit var binding: BottomSheetImagePickerOptionsBinding
36 |
37 | override fun getTheme(): Int {
38 | return R.style.SSImagePickerBaseBottomSheetDialog
39 | }
40 |
41 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
42 | val themeId = arguments?.getInt(THEME_ID, -1) ?: 0
43 | if (themeId > 0) {
44 | return try {
45 | BottomSheetDialog(requireContext(), themeId)
46 | } catch (e: Exception) {
47 | e.printStackTrace()
48 | BottomSheetDialog(requireContext(), this.theme)
49 | }
50 | }
51 | return BottomSheetDialog(requireContext(), this.theme)
52 | }
53 |
54 | override fun onCreateView(
55 | inflater: LayoutInflater,
56 | container: ViewGroup?,
57 | savedInstanceState: Bundle?
58 | ): View {
59 | dialog?.setOnShowListener { dialog ->
60 | val bottomSheetDialog = dialog as BottomSheetDialog
61 | val bottomSheetInternal =
62 | bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet) as View
63 | BottomSheetBehavior.from(bottomSheetInternal).state = BottomSheetBehavior.STATE_EXPANDED
64 | }
65 | binding = BottomSheetImagePickerOptionsBinding.inflate(inflater, container, false)
66 | return binding.root
67 | }
68 |
69 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
70 | super.onViewCreated(view, savedInstanceState)
71 | binding.clickHandler = this
72 | }
73 |
74 | override fun onAttach(context: Context) {
75 | super.onAttach(context)
76 | parentFragment?.let { fragment ->
77 | mListener =
78 | if (fragment is ImagePickerClickListener) fragment else throw IllegalStateException(
79 | getString(
80 | R.string.error_invalid_context_listener
81 | )
82 | )
83 | } ?: kotlin.run {
84 | mListener =
85 | if (context is ImagePickerClickListener) context else throw IllegalStateException(
86 | getString(
87 | R.string.error_invalid_context_listener
88 | )
89 | )
90 | }
91 | }
92 |
93 | override fun onDetach() {
94 | super.onDetach()
95 | mListener = null
96 | }
97 |
98 | override fun onClick(v: View?) {
99 | when (v?.id) {
100 | R.id.textViewChooseCamera -> {
101 | mListener?.onImageProvider(ImageProvider.CAMERA)
102 | dismiss()
103 | }
104 | R.id.textViewChooseGallery -> {
105 | mListener?.onImageProvider(ImageProvider.GALLERY)
106 | dismiss()
107 | }
108 | R.id.textViewChooseCancel -> {
109 | mListener?.onImageProvider(ImageProvider.NONE)
110 | dismiss()
111 | }
112 | }
113 | }
114 |
115 | interface ImagePickerClickListener {
116 | fun onImageProvider(provider: ImageProvider)
117 | }
118 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/dialog/FullScreenImageDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.dialog
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.os.bundleOf
8 | import androidx.fragment.app.DialogFragment
9 | import coil.load
10 | import com.app.imagepickerlibrary.databinding.DialogFragmentFullScreenImageBinding
11 | import com.app.imagepickerlibrary.getModel
12 | import com.app.imagepickerlibrary.model.Image
13 | import com.intuit.sdp.R as SdpR
14 |
15 | /**
16 | * FullScreenImageDialogFragment to display image full screen with transparent background.
17 | */
18 | internal class FullScreenImageDialogFragment : DialogFragment() {
19 | companion object {
20 | const val IMAGE = "image"
21 | const val FRAGMENT_TAG = "FullScreenImage"
22 | fun newInstance(image: Image): FullScreenImageDialogFragment {
23 | return FullScreenImageDialogFragment().apply {
24 | arguments = bundleOf(IMAGE to image)
25 | }
26 | }
27 | }
28 |
29 | private lateinit var binding: DialogFragmentFullScreenImageBinding
30 |
31 | override fun onStart() {
32 | super.onStart()
33 | dialog?.let {
34 | val width = ViewGroup.LayoutParams.MATCH_PARENT
35 | val height = resources.getDimensionPixelSize(SdpR.dimen._350sdp)
36 | it.window?.setLayout(width, height)
37 | it.window?.setBackgroundDrawableResource(android.R.color.transparent)
38 | }
39 | }
40 |
41 | override fun onCreateView(
42 | inflater: LayoutInflater,
43 | container: ViewGroup?,
44 | savedInstanceState: Bundle?
45 | ): View {
46 | binding = DialogFragmentFullScreenImageBinding.inflate(inflater, container, false)
47 | return binding.root
48 | }
49 |
50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
51 | super.onViewCreated(view, savedInstanceState)
52 | val image: Image? = arguments?.getModel(IMAGE)
53 | if (image == null) {
54 | dismiss()
55 | }
56 | binding.imageView.load(image?.uri)
57 | }
58 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/fragment/BaseFragment.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.fragment
2 |
3 | import android.content.res.Configuration
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.annotation.IdRes
9 | import androidx.annotation.LayoutRes
10 | import androidx.databinding.DataBindingUtil
11 | import androidx.databinding.ViewDataBinding
12 | import androidx.fragment.app.Fragment
13 | import androidx.fragment.app.activityViewModels
14 | import androidx.lifecycle.Lifecycle
15 | import androidx.lifecycle.ViewModelProvider
16 | import androidx.lifecycle.lifecycleScope
17 | import androidx.lifecycle.repeatOnLifecycle
18 | import androidx.recyclerview.widget.GridLayoutManager
19 | import androidx.recyclerview.widget.RecyclerView
20 | import com.app.imagepickerlibrary.R
21 | import com.app.imagepickerlibrary.getIntAttribute
22 | import com.app.imagepickerlibrary.listener.ItemClickListener
23 | import com.app.imagepickerlibrary.model.Image
24 | import com.app.imagepickerlibrary.model.Result
25 | import com.app.imagepickerlibrary.viewmodel.ImagePickerViewModel
26 | import kotlinx.coroutines.launch
27 |
28 | /**
29 | * Base fragment for all fragments.
30 | */
31 | internal abstract class BaseFragment : Fragment(),
32 | ItemClickListener {
33 | protected lateinit var binding: Binding
34 | protected val viewModel: ImagePickerViewModel by activityViewModels {
35 | ViewModelProvider.AndroidViewModelFactory(
36 | requireActivity().application
37 | )
38 | }
39 | private var disableInteraction = false
40 |
41 | /**
42 | * Get the layout resource ID for the screen.
43 | */
44 | @LayoutRes
45 | abstract fun getLayoutResId(): Int
46 |
47 | override fun onCreateView(
48 | inflater: LayoutInflater,
49 | container: ViewGroup?,
50 | savedInstanceState: Bundle?
51 | ): View? {
52 | binding = DataBindingUtil.inflate(inflater, getLayoutResId(), container, false)
53 | return binding.root
54 | }
55 |
56 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57 | super.onViewCreated(view, savedInstanceState)
58 | viewLifecycleOwner.lifecycleScope.launch {
59 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
60 | launch {
61 | viewModel.resultFlow.collect {
62 | handleResult(it)
63 | }
64 | }
65 | launch {
66 | viewModel.disableInteraction.collect {
67 | disableInteraction = it
68 | }
69 | }
70 | }
71 | }
72 | onViewCreated()
73 | }
74 |
75 | abstract fun onViewCreated()
76 |
77 | private fun handleResult(result: Result>) {
78 | when (result) {
79 | Result.Loading -> {
80 | handleLoading(true)
81 | }
82 | is Result.Error -> {
83 | handleLoading(false)
84 | handleError(result.exception)
85 | }
86 | is Result.Success -> {
87 | handleLoading(false)
88 | handleSuccess(result.data)
89 | }
90 | }
91 | }
92 |
93 | open fun handleLoading(visible: Boolean) {}
94 | open fun handleError(exception: Exception) {}
95 | open fun handleSuccess(images: List) {}
96 |
97 | /**
98 | * If the disableInteraction is set to true all the click events are ignored otherwise
99 | * they are passed to child fragment.
100 | */
101 | override fun onItemClick(item: T, position: Int, @IdRes viewId: Int) {
102 | if (disableInteraction) {
103 | return
104 | }
105 | handleItemClick(item, position, viewId)
106 | }
107 |
108 | /**
109 | * Setting recycler view for both folder and image fragments.
110 | * The span count is based on screen orientation so that the picker screen look better.
111 | */
112 | protected fun setRecyclerView(recyclerView: RecyclerView) {
113 | val spanCount =
114 | if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
115 | requireContext().getIntAttribute(R.attr.ssPickerGridCount)
116 | } else {
117 | requireContext().getIntAttribute(R.attr.ssPickerGridCountLandscape)
118 | }
119 | recyclerView.layoutManager = GridLayoutManager(requireContext(), spanCount)
120 | }
121 |
122 | override fun onConfigurationChanged(newConfig: Configuration) {
123 | super.onConfigurationChanged(newConfig)
124 | onConfigurationChange()
125 | }
126 |
127 | abstract fun onConfigurationChange()
128 |
129 | abstract fun handleItemClick(item: T, position: Int, viewId: Int)
130 | }
131 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/fragment/FolderFragment.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.fragment
2 |
3 | import androidx.core.view.isVisible
4 | import androidx.lifecycle.Lifecycle
5 | import androidx.lifecycle.lifecycleScope
6 | import androidx.lifecycle.repeatOnLifecycle
7 | import com.app.imagepickerlibrary.R
8 | import com.app.imagepickerlibrary.databinding.FragmentFolderBinding
9 | import com.app.imagepickerlibrary.model.Folder
10 | import com.app.imagepickerlibrary.model.Image
11 | import com.app.imagepickerlibrary.model.Result
12 | import com.app.imagepickerlibrary.ui.adapter.FolderAdapter
13 | import kotlinx.coroutines.launch
14 |
15 | /**
16 | * FolderFragment to display list of folders.
17 | */
18 | internal class FolderFragment : BaseFragment() {
19 | companion object {
20 | fun newInstance(): FolderFragment {
21 | return FolderFragment()
22 | }
23 | }
24 |
25 | private val folderAdapter = FolderAdapter(this)
26 |
27 | override fun getLayoutResId(): Int = R.layout.fragment_folder
28 |
29 | override fun onViewCreated() {
30 | setRecyclerView(binding.rvFolder)
31 | binding.rvFolder.adapter = folderAdapter
32 | viewLifecycleOwner.lifecycleScope.launch {
33 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
34 | launch {
35 | viewModel.folderFlow.collect {
36 | updateFolderList(it)
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | private fun updateFolderList(folderList: List) {
44 | if (viewModel.resultFlow.value is Result.Loading) {
45 | return
46 | }
47 | val isFolderListEmpty = folderList.isEmpty()
48 | binding.rvFolder.isVisible = !isFolderListEmpty
49 | binding.textNoData.isVisible = isFolderListEmpty
50 | folderAdapter.setItemList(folderList)
51 | }
52 |
53 | override fun handleSuccess(images: List) {
54 | viewModel.getFoldersFromImages(images)
55 | }
56 |
57 | override fun handleLoading(visible: Boolean) {
58 | binding.progressIndicator.isVisible = visible
59 | }
60 |
61 | override fun handleError(exception: Exception) {
62 | binding.rvFolder.isVisible = false
63 | binding.textNoData.isVisible = true
64 | }
65 |
66 | override fun handleItemClick(item: Folder, position: Int, viewId: Int) {
67 | viewModel.openFolder(item)
68 | }
69 |
70 | override fun onConfigurationChange() {
71 | setRecyclerView(binding.rvFolder)
72 | }
73 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/ui/fragment/ImageFragment.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.ui.fragment
2 |
3 | import androidx.core.os.bundleOf
4 | import androidx.core.view.isVisible
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.lifecycleScope
7 | import androidx.lifecycle.repeatOnLifecycle
8 | import com.app.imagepickerlibrary.R
9 | import com.app.imagepickerlibrary.databinding.FragmentImageBinding
10 | import com.app.imagepickerlibrary.getStringAttribute
11 | import com.app.imagepickerlibrary.model.Image
12 | import com.app.imagepickerlibrary.model.PickerConfig
13 | import com.app.imagepickerlibrary.model.Result
14 | import com.app.imagepickerlibrary.toast
15 | import com.app.imagepickerlibrary.ui.adapter.ImageAdapter
16 | import com.app.imagepickerlibrary.ui.dialog.FullScreenImageDialogFragment
17 | import kotlinx.coroutines.launch
18 |
19 | /**
20 | * ImageFragment to display list of images.
21 | */
22 | internal class ImageFragment : BaseFragment() {
23 | companion object {
24 | const val BUCKET_ID = "bucketId"
25 | fun newInstance(bucketId: Long): ImageFragment {
26 | return ImageFragment().apply {
27 | arguments = bundleOf(BUCKET_ID to bucketId)
28 | }
29 | }
30 |
31 | fun newInstance(): ImageFragment {
32 | return ImageFragment()
33 | }
34 | }
35 |
36 | private val imageAdapter = ImageAdapter(this)
37 | private var bucketId: Long? = null
38 | private var pickerConfig = PickerConfig.defaultPicker()
39 | private var maxPickError: String = ""
40 |
41 | override fun getLayoutResId(): Int = R.layout.fragment_image
42 |
43 | override fun onViewCreated() {
44 | bucketId = arguments?.getLong(BUCKET_ID)
45 | setRecyclerView(binding.rvImage)
46 | binding.rvImage.apply {
47 | adapter = imageAdapter
48 | setHasFixedSize(true)
49 | }
50 | maxPickError = requireContext().getStringAttribute(R.attr.ssImagePickerLimitText)
51 | viewLifecycleOwner.lifecycleScope.launch {
52 | viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.CREATED) {
53 | launch {
54 | viewModel.imageFlow.collect {
55 | updateImageList(it)
56 | }
57 | }
58 | launch {
59 | viewModel.pickerConfig.collect {
60 | pickerConfig = it
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | private fun updateImageList(imageList: List) {
68 | if (viewModel.resultFlow.value is Result.Loading) {
69 | return
70 | }
71 | val isImageListEmpty = imageList.isEmpty()
72 | binding.rvImage.isVisible = !isImageListEmpty
73 | binding.textNoData.isVisible = isImageListEmpty
74 | imageAdapter.setItemList(imageList)
75 | }
76 |
77 | override fun handleSuccess(images: List) {
78 | viewModel.getImagesFromFolder(bucketId, images)
79 | }
80 |
81 | override fun handleLoading(visible: Boolean) {
82 | binding.progressIndicator.isVisible = visible
83 | }
84 |
85 | override fun handleError(exception: Exception) {
86 | binding.rvImage.isVisible = false
87 | binding.textNoData.isVisible = true
88 | }
89 |
90 | override fun handleItemClick(item: Image, position: Int, viewId: Int) {
91 | if (viewId == R.id.root_item_image) {
92 | manageSelection(item, position)
93 | } else if (viewId == R.id.image_zoom) {
94 | showZoomImage(item)
95 | }
96 | }
97 |
98 | /**
99 | * The selection is done based on two configs.
100 | * If multiple selection is allowed then it checks whether the image is previously selected or not.
101 | * If the image is previously not selected then it will select the image and add it to the selected image list.
102 | * If the image is previously selected then it will un-select the image and remove it from the selected image list.
103 | * If multiple selection is not allowed then it adds the image to selected image list and marks the done via viewmodel.
104 | */
105 | private fun manageSelection(item: Image, position: Int) {
106 | if (pickerConfig.allowMultipleSelection) {
107 | if (viewModel.isImageSelected(item)) {
108 | selectImage(item, position, false)
109 | } else if (pickerConfig.maxPickCount > viewModel.getSelectedImages().size) {
110 | selectImage(item, position, true)
111 | } else {
112 | toast(maxPickError)
113 | }
114 | } else {
115 | viewModel.handleSelection(item, true)
116 | viewModel.handleDoneSelection()
117 | }
118 | }
119 |
120 | private fun selectImage(item: Image, position: Int, isSelected: Boolean) {
121 | item.isSelected = isSelected
122 | viewModel.handleSelection(item, isSelected)
123 | imageAdapter.notifyItemChanged(position)
124 | }
125 |
126 | private fun showZoomImage(item: Image) {
127 | val imageDialogFragment = FullScreenImageDialogFragment.newInstance(item)
128 | imageDialogFragment.show(childFragmentManager, FullScreenImageDialogFragment.FRAGMENT_TAG)
129 | }
130 |
131 | override fun onConfigurationChange() {
132 | setRecyclerView(binding.rvImage)
133 | }
134 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/util/BindingAdapters.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.util
2 |
3 | import android.net.Uri
4 | import android.widget.TextView
5 | import androidx.appcompat.widget.AppCompatImageView
6 | import androidx.databinding.BindingAdapter
7 | import coil.load
8 | import com.app.imagepickerlibrary.model.Folder
9 |
10 | /**
11 | * Binding adapter function to show image uri via xml it self.
12 | */
13 | @BindingAdapter("android:src")
14 | internal fun loadImage(imageView: AppCompatImageView, uri: Uri) {
15 | imageView.load(uri) {
16 | crossfade(true)
17 | }
18 | }
19 |
20 | /**
21 | * Binding adapter function to show folder name with images count
22 | */
23 | @BindingAdapter("folderName")
24 | internal fun setFolderName(textView: TextView, folder: Folder) {
25 | textView.text = buildString {
26 | append(folder.bucketName)
27 | append(" ")
28 | append("(${folder.images.size})")
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/util/PickerConfigManager.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.util
2 |
3 | import android.os.Bundle
4 | import androidx.core.os.bundleOf
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleEventObserver
7 | import androidx.savedstate.SavedStateRegistry
8 | import androidx.savedstate.SavedStateRegistryOwner
9 | import com.app.imagepickerlibrary.EXTRA_IMAGE_PICKER_CONFIG
10 | import com.app.imagepickerlibrary.getModel
11 | import com.app.imagepickerlibrary.model.PickerConfig
12 |
13 | internal class PickerConfigManager(registryOwner: SavedStateRegistryOwner) :
14 | SavedStateRegistry.SavedStateProvider {
15 | companion object {
16 | const val PICKER_CONFIG_MANAGER = "picker_config_manage"
17 | }
18 |
19 | private var pickerConfig = PickerConfig.defaultPicker()
20 |
21 | init {
22 | val registry = registryOwner.savedStateRegistry
23 | registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
24 | if (event == Lifecycle.Event.ON_CREATE) {
25 | if (registry.getSavedStateProvider(PICKER_CONFIG_MANAGER) == null) {
26 | registry.registerSavedStateProvider(PICKER_CONFIG_MANAGER, this)
27 | }
28 | val previousState = registry.consumeRestoredStateForKey(PICKER_CONFIG_MANAGER)
29 | if (previousState != null && previousState.containsKey(EXTRA_IMAGE_PICKER_CONFIG)) {
30 | pickerConfig = previousState.getModel() ?: PickerConfig.defaultPicker()
31 | }
32 | } else if (event == Lifecycle.Event.ON_DESTROY) {
33 | registry.unregisterSavedStateProvider(PICKER_CONFIG_MANAGER)
34 | }
35 | })
36 | }
37 |
38 | override fun saveState(): Bundle {
39 | return bundleOf(EXTRA_IMAGE_PICKER_CONFIG to pickerConfig)
40 | }
41 |
42 | fun getPickerConfig(): PickerConfig {
43 | return pickerConfig
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/util/Util.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.util
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Bitmap
6 | import android.graphics.BitmapFactory
7 | import android.graphics.Canvas
8 | import android.graphics.Matrix
9 | import android.graphics.Paint
10 | import android.net.Uri
11 | import android.os.Build
12 | import android.os.ext.SdkExtensions.getExtensionVersion
13 | import androidx.annotation.ChecksSdkIntAtLeast
14 | import androidx.exifinterface.media.ExifInterface
15 | import com.app.imagepickerlibrary.createImageFile
16 | import com.app.imagepickerlibrary.getRealPathFromURI
17 | import com.app.imagepickerlibrary.isNullOrEmptyOrBlank
18 | import java.io.FileNotFoundException
19 | import java.io.FileOutputStream
20 | import java.io.IOException
21 | import kotlin.math.roundToInt
22 |
23 | /**
24 | * Utility function to check that the system is running on at least android 13.
25 | */
26 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
27 | internal fun isAtLeast13(): Boolean {
28 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
29 | }
30 |
31 | /**
32 | * Utility function to check if system picker is available or not on Android 11+.
33 | * The function is provided by google to check whether the photo picker is available or not
34 | * [More Details](https://developer.android.com/training/data-storage/shared/photopicker#check-availability)
35 | *
36 | * Using SuppressLint to remove warning about the getExtensionVersion method.
37 | */
38 | @SuppressLint("NewApi")
39 | @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
40 | internal fun isPhotoPickerAvailable(): Boolean {
41 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
42 | true
43 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
44 | getExtensionVersion(Build.VERSION_CODES.R) >= 2
45 | } else {
46 | false
47 | }
48 | }
49 |
50 | /**
51 | * Utility function to compress the image.
52 | * The function returns the file path of the compressed image.
53 | */
54 | @Suppress("DEPRECATION")
55 | internal fun compress(context: Context, uri: Uri, name: String, compressQuality: Int): String? {
56 | val filePath = context.getRealPathFromURI(uri)
57 | if (filePath.isNullOrEmptyOrBlank()) {
58 | return null
59 | }
60 | var scaledBitmap: Bitmap? = null
61 | val options = BitmapFactory.Options()
62 | var bmp: Bitmap = BitmapFactory.decodeFile(filePath, options)
63 | var actualHeight = options.outHeight
64 | var actualWidth = options.outWidth
65 | val maxHeight = actualHeight / 1.5
66 | val maxWidth = actualWidth / 1.5
67 | var imgRatio =
68 | if (actualWidth < actualHeight) (actualWidth / actualHeight).toFloat() else (actualHeight / actualWidth).toFloat()
69 | val maxRatio = maxWidth / maxHeight
70 | if (actualHeight > maxHeight || actualWidth > maxWidth) {
71 | if (imgRatio < maxRatio) {
72 | imgRatio = (maxHeight / actualHeight).toFloat()
73 | actualWidth = (imgRatio * actualWidth).toInt()
74 | actualHeight = maxHeight.toInt()
75 | } else if (imgRatio > maxRatio) {
76 | imgRatio = (maxWidth / actualWidth).toFloat()
77 | actualHeight = (imgRatio * actualHeight).toInt()
78 | actualWidth = maxWidth.toInt()
79 | } else {
80 | actualHeight = maxHeight.toInt()
81 | actualWidth = maxWidth.toInt()
82 | }
83 | }
84 | options.inSampleSize = calculateInSampleSize(options, actualWidth, actualHeight)
85 | options.inJustDecodeBounds = false
86 | options.inPurgeable = true
87 | options.inInputShareable = true
88 | options.inTempStorage = ByteArray(16 * 1024)
89 | try {
90 | bmp = BitmapFactory.decodeFile(filePath, options)
91 | } catch (exception: OutOfMemoryError) {
92 | exception.printStackTrace()
93 | }
94 | try {
95 | scaledBitmap = Bitmap.createBitmap(actualWidth, actualHeight, Bitmap.Config.ARGB_8888)
96 | } catch (exception: OutOfMemoryError) {
97 | exception.printStackTrace()
98 | }
99 | val ratioX = actualWidth / options.outWidth.toFloat()
100 | val ratioY = actualHeight / options.outHeight.toFloat()
101 | val middleX = actualWidth / 2.0f
102 | val middleY = actualHeight / 2.0f
103 | val scaleMatrix = Matrix()
104 | scaleMatrix.setScale(ratioX, ratioY, middleX, middleY)
105 | if (scaledBitmap == null) {
106 | return null
107 | }
108 | val canvas = Canvas(scaledBitmap)
109 | canvas.setMatrix(scaleMatrix)
110 | canvas.drawBitmap(
111 | bmp,
112 | middleX - bmp.width / 2,
113 | middleY - bmp.height / 2,
114 | Paint(Paint.FILTER_BITMAP_FLAG)
115 | )
116 | val exif: ExifInterface
117 | try {
118 | exif = ExifInterface(filePath)
119 | val orientation: Int = exif.getAttributeInt(
120 | ExifInterface.TAG_ORIENTATION, 0
121 | )
122 | val matrix = Matrix()
123 | when (orientation) {
124 | 6 -> {
125 | matrix.postRotate(90f)
126 | }
127 | 3 -> {
128 | matrix.postRotate(180f)
129 | }
130 | 8 -> {
131 | matrix.postRotate(270F)
132 | }
133 | }
134 | scaledBitmap = Bitmap.createBitmap(
135 | scaledBitmap, 0, 0,
136 | scaledBitmap.width, scaledBitmap.height, matrix,
137 | true
138 | )
139 | } catch (e: IOException) {
140 | e.printStackTrace()
141 | }
142 | val out: FileOutputStream?
143 | val resultFilePath = context.createImageFile("COMPRESS_${name}").absolutePath
144 | try {
145 | out = FileOutputStream(resultFilePath)
146 | scaledBitmap?.compress(Bitmap.CompressFormat.JPEG, compressQuality, out)
147 | } catch (e: FileNotFoundException) {
148 | e.printStackTrace()
149 | }
150 | return resultFilePath
151 | }
152 |
153 | /**
154 | * Utility function to calculate the InSample Size
155 | */
156 | private fun calculateInSampleSize(
157 | options: BitmapFactory.Options,
158 | reqWidth: Int,
159 | reqHeight: Int
160 | ): Int {
161 | val height = options.outHeight
162 | val width = options.outWidth
163 | var inSampleSize = 1
164 | if (height > reqHeight || width > reqWidth) {
165 | val heightRatio = (height.toFloat() / reqHeight.toFloat()).roundToInt()
166 | val widthRatio = (width.toFloat() / reqWidth.toFloat()).roundToInt()
167 | inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio
168 | }
169 | val totalPixels = (width * height).toFloat()
170 | val totalReqPixelsCap = (reqWidth * reqHeight * 2).toFloat()
171 | while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {
172 | inSampleSize++
173 | }
174 | return inSampleSize
175 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/java/com/app/imagepickerlibrary/viewmodel/ImagePickerViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary.viewmodel
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import androidx.lifecycle.viewModelScope
6 | import com.app.imagepickerlibrary.getFileUri
7 | import com.app.imagepickerlibrary.getImagesList
8 | import com.app.imagepickerlibrary.isNullOrEmptyOrBlank
9 | import com.app.imagepickerlibrary.model.Folder
10 | import com.app.imagepickerlibrary.model.Image
11 | import com.app.imagepickerlibrary.model.PickerConfig
12 | import com.app.imagepickerlibrary.model.Result
13 | import com.app.imagepickerlibrary.util.compress
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.flow.MutableSharedFlow
16 | import kotlinx.coroutines.flow.MutableStateFlow
17 | import kotlinx.coroutines.flow.asSharedFlow
18 | import kotlinx.coroutines.flow.asStateFlow
19 | import kotlinx.coroutines.flow.update
20 | import kotlinx.coroutines.launch
21 | import kotlinx.coroutines.withContext
22 |
23 | /**
24 | * ImagePickerViewModel to manage all the interaction with UI and MediaStore.
25 | */
26 | internal class ImagePickerViewModel(application: Application) : AndroidViewModel(application) {
27 | private val _pickerConfig = MutableStateFlow(PickerConfig.defaultPicker())
28 | val pickerConfig = _pickerConfig.asStateFlow()
29 | private val _resultFlow = MutableStateFlow>>(Result.Loading)
30 | val resultFlow = _resultFlow.asStateFlow()
31 | private val _selectedFolder = MutableSharedFlow()
32 | val selectedFolder = _selectedFolder.asSharedFlow()
33 | private val _folderFlow = MutableSharedFlow>()
34 | val folderFlow = _folderFlow.asSharedFlow()
35 | private val _imageFlow = MutableSharedFlow>(replay = 1)
36 | val imageFlow = _imageFlow.asSharedFlow()
37 | private val _completeSelection = MutableSharedFlow()
38 | val completeSelection = _completeSelection.asSharedFlow()
39 | private val _selectedImages = mutableListOf()
40 | private val _updateImageCount = MutableSharedFlow()
41 | val updateImageCount = _updateImageCount.asSharedFlow()
42 | private val _disableInteraction = MutableSharedFlow()
43 | val disableInteraction = _disableInteraction.asSharedFlow()
44 |
45 | fun updatePickerConfig(pickerConfig: PickerConfig) {
46 | _pickerConfig.update { pickerConfig }
47 | }
48 |
49 | fun fetchImagesFromMediaStore() {
50 | _resultFlow.update { Result.Loading }
51 | viewModelScope.launch {
52 | val imageList = fetchImageList()
53 | _selectedImages.clear()
54 | _updateImageCount.emit(true)
55 | _resultFlow.update { Result.Success(imageList) }
56 | }
57 | }
58 |
59 | private suspend fun fetchImageList(): List {
60 | val config = pickerConfig.value
61 | val (selection, selectionArgs) = config.generateSelectionArguments()
62 | val context = (getApplication() as Application).applicationContext
63 | return context.getImagesList(selection, selectionArgs)
64 | }
65 |
66 | fun getFoldersFromImages(images: List) {
67 | viewModelScope.launch(Dispatchers.IO) {
68 | val folders = images
69 | .groupBy { it.bucketId }
70 | .filter { it.value.isNotEmpty() }
71 | .map {
72 | val image = it.value.first()
73 | Folder(it.key, image.bucketName, image.uri, it.value)
74 | }
75 | .sortedBy { it.bucketName }
76 | _folderFlow.emit(folders)
77 | }
78 | }
79 |
80 | fun getImagesFromFolder(bucketId: Long?, images: List) {
81 | if (bucketId == null) {
82 | _imageFlow.tryEmit(images)
83 | } else {
84 | viewModelScope.launch(Dispatchers.IO) {
85 | val filteredImages = images.filter { it.bucketId == bucketId }
86 | _imageFlow.tryEmit(filteredImages)
87 | }
88 | }
89 | }
90 |
91 | fun openFolder(folder: Folder) {
92 | viewModelScope.launch {
93 | _selectedFolder.emit(folder)
94 | }
95 | }
96 |
97 | fun handleSelection(image: Image, selected: Boolean) {
98 | viewModelScope.launch {
99 | val isImageAdded = _selectedImages.any { image.id == it.id }
100 | if (selected) {
101 | if (!isImageAdded) {
102 | _selectedImages.add(image)
103 | }
104 | } else {
105 | val index = _selectedImages.indexOfFirst { image.id == it.id }
106 | if (isImageAdded && index != -1) {
107 | _selectedImages.removeAt(index)
108 | }
109 | }
110 | _updateImageCount.emit(true)
111 | }
112 | }
113 |
114 | fun handleDoneSelection() {
115 | viewModelScope.launch {
116 | _completeSelection.emit(true)
117 | }
118 | }
119 |
120 | fun getSelectedImages(): List {
121 | return _selectedImages
122 | }
123 |
124 | fun isImageSelected(image: Image): Boolean {
125 | return _selectedImages.any { image.id == it.id }
126 | }
127 |
128 | suspend fun compressImage(list: List): List {
129 | val compressedImageList = mutableListOf()
130 | val context = (getApplication() as Application).applicationContext
131 | val compressQuality = pickerConfig.value.compressQuality
132 | return withContext(Dispatchers.IO) {
133 | _disableInteraction.emit(true)
134 | list.forEach { image ->
135 | val compressImagePath = compress(context, image.uri, image.name, compressQuality)
136 | if (compressImagePath.isNullOrEmptyOrBlank()) {
137 | compressedImageList.add(image.copy())
138 | } else {
139 | val fileUri = context.getFileUri(compressImagePath)
140 | fileUri?.let { compressedImageList.add(image.copy(uri = it)) }
141 | ?: compressedImageList.add(image.copy())
142 | }
143 | }
144 | _disableInteraction.emit(false)
145 | compressedImageList
146 | }
147 | }
148 | }
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/bg_ss_drawable_shadow.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/bg_ss_picker_option.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/bg_ss_picker_option_button.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/bg_ss_picker_option_button_cancel.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/ic_ss_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/ic_ss_camera.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/ic_ss_check_circle.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/ic_ss_done.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/drawable/ic_ss_zoom_eye.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/imagepickerlibrary/src/main/res/ic_launcher.png
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout-v23/list_item_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
19 |
20 |
33 |
34 |
46 |
47 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/activity_image_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
15 |
16 |
24 |
25 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/bottom_sheet_image_picker_options.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
13 |
17 |
18 |
32 |
33 |
40 |
41 |
55 |
56 |
63 |
64 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/dialog_fragment_full_screen_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/fragment_folder.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
22 |
23 |
33 |
34 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/fragment_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
22 |
23 |
33 |
34 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/list_item_folder.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
11 |
12 |
13 |
16 |
17 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/list_item_image.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
13 |
14 |
15 |
19 |
20 |
33 |
34 |
46 |
47 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/layout/toolbar_image_picker.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
12 |
15 |
16 |
22 |
23 |
26 |
27 |
37 |
38 |
49 |
50 |
56 |
57 |
68 |
69 |
75 |
76 |
87 |
88 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/mipmap/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/imagepickerlibrary/src/main/res/mipmap/ic_launcher_round.png
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/values/attr.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 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #2D647D
4 | #2D647D
5 | #46a0c6
6 |
7 | #FF000000
8 | #66000000
9 | #FFFFFFFF
10 | #B4F0F4
11 | #CFECE5
12 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SSImagePicker
4 |
5 | "Camera"
6 | "Gallery"
7 | Cancel
8 | Pass context which implements ImagePickerClickListener
9 | No data found
10 | Done
11 | Maximum selection reached
12 | %s (%d/%d)
13 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
34 |
35 |
41 |
42 |
46 |
47 |
58 |
59 |
71 |
72 |
75 |
76 |
95 |
96 |
100 |
101 |
107 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/main/res/xml/file_path.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/imagepickerlibrary/src/test/java/com/app/imagepickerlibrary/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.app.imagepickerlibrary
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 | }
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
--------------------------------------------------------------------------------
/library_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SimformSolutionsPvtLtd/SSImagePicker/ddfe809461c8da02ed0e9934446c34a5af78522b/library_banner.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenLocal()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenLocal()
14 | mavenCentral()
15 | maven { url = "https://jitpack.io" }
16 | }
17 | }
18 |
19 | include ':app'
20 | include ':imagepickerlibrary'
21 | rootProject.name = "SSImagePicker"
--------------------------------------------------------------------------------