├── .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" --------------------------------------------------------------------------------