├── .gitignore ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── art ├── feature-graphic.png ├── gallery.png ├── preview.png ├── sample.gif ├── sample_optimized.gif ├── screen1-github.png ├── screen1-store.png ├── screen2-github.png ├── screen2-store.png ├── screen3-arsenal.png ├── screen3-github.png ├── screen3-store.png ├── transition_fixed.gif ├── transition_wrong0_go_different_page.gif ├── transition_wrong1_go_previous_image.gif ├── transition_wrong2_go_more_previuos_image.gif ├── transition_wrong3_update_position.gif ├── transition_wrong4_update_shared_preview.gif └── transition_wrong5_update_shared_gallery.gif ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── louvre ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── andremion │ │ └── louvre │ │ ├── Louvre.java │ │ ├── StoragePermissionActivity.java │ │ ├── data │ │ ├── MediaLoader.kt │ │ └── MediaQuery.kt │ │ ├── home │ │ ├── GalleryActivity.java │ │ ├── GalleryAdapter.java │ │ └── GalleryFragment.java │ │ ├── preview │ │ ├── PreviewActivity.java │ │ └── PreviewAdapter.java │ │ └── util │ │ ├── AnimationHelper.java │ │ ├── FabBehavior.java │ │ ├── HackyViewPager.java │ │ ├── ItemOffsetDecoration.java │ │ └── transition │ │ ├── MediaSharedElementCallback.java │ │ └── TransitionCallback.java │ └── res │ ├── anim │ ├── btn_checkbox_checked_mtrl_animation_interpolator_0.xml │ ├── btn_checkbox_checked_mtrl_animation_interpolator_1.xml │ ├── btn_checkbox_to_checked_box_inner_merged_animation.xml │ ├── btn_checkbox_to_checked_box_outer_merged_animation.xml │ ├── btn_checkbox_to_checked_icon_null_animation.xml │ ├── btn_checkbox_to_unchecked_box_inner_merged_animation.xml │ ├── btn_checkbox_to_unchecked_check_path_merged_animation.xml │ ├── btn_checkbox_to_unchecked_icon_null_animation.xml │ ├── btn_checkbox_unchecked_mtrl_animation_interpolator_0.xml │ └── btn_checkbox_unchecked_mtrl_animation_interpolator_1.xml │ ├── drawable-hdpi │ ├── ic_clear_white_24dp.png │ ├── ic_no_images_black_48dp.png │ └── ic_select_all_white_24dp.png │ ├── drawable-mdpi │ ├── ic_clear_white_24dp.png │ ├── ic_no_images_black_48dp.png │ └── ic_select_all_white_24dp.png │ ├── drawable-v21 │ ├── btn_check_material_anim.xml │ ├── btn_checkbox_checked_to_unchecked_mtrl_animation.xml │ └── btn_checkbox_unchecked_to_checked_mtrl_animation.xml │ ├── drawable-xhdpi │ ├── ic_clear_white_24dp.png │ ├── ic_no_images_black_48dp.png │ └── ic_select_all_white_24dp.png │ ├── drawable-xxhdpi │ ├── ic_clear_white_24dp.png │ ├── ic_no_images_black_48dp.png │ └── ic_select_all_white_24dp.png │ ├── drawable-xxxhdpi │ ├── ic_clear_white_24dp.png │ ├── ic_no_images_black_48dp.png │ └── ic_select_all_white_24dp.png │ ├── drawable │ ├── btn_check_material_anim.xml │ ├── btn_checkbox_checked_mtrl.xml │ ├── btn_checkbox_unchecked_mtrl.xml │ ├── ic_clear.xml │ ├── ic_done_white_24dp.xml │ ├── ic_no_images.xml │ └── ic_select_all.xml │ ├── layout │ ├── activity_gallery.xml │ ├── activity_preview.xml │ ├── checkbox.xml │ ├── fragment_gallery.xml │ ├── list_item_gallery_bucket.xml │ ├── list_item_gallery_media.xml │ └── page_item_preview.xml │ ├── menu │ └── gallery_menu.xml │ ├── transition-v21 │ ├── gallery_exit.xml │ ├── gallery_reenter.xml │ └── shared_element.xml │ ├── values-es │ └── strings.xml │ ├── values-v19 │ └── themes.xml │ └── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── andremion │ │ └── louvre │ │ └── sample │ │ ├── MainActivity.java │ │ ├── MainAdapter.java │ │ ├── MediaTypeFilterDialog.java │ │ └── NumberPickerDialog.java │ └── res │ ├── drawable-hdpi │ └── ic_add_to_photos_white_24dp.png │ ├── drawable-mdpi │ └── ic_add_to_photos_white_24dp.png │ ├── drawable-xhdpi │ └── ic_add_to_photos_white_24dp.png │ ├── drawable-xxhdpi │ └── ic_add_to_photos_white_24dp.png │ ├── drawable-xxxhdpi │ └── ic_add_to_photos_white_24dp.png │ ├── layout │ ├── activity_main.xml │ ├── content_main.xml │ └── list_item_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-es │ └── strings.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── themes.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the ART/Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | # Android Studio Navigation editor temp files 30 | .navigation/ 31 | 32 | # Android Studio captures folder 33 | captures/ 34 | 35 | # Intellij 36 | *.iml 37 | .idea/ 38 | 39 | # Keystore files 40 | *.jks 41 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [provide general introduction to the issue logging and why it is relevant to this repository] 2 | 3 | ## Context 4 | 5 | [provide more detailed introduction to the issue itself and why it is relevant] 6 | 7 | ## Process 8 | 9 | [ordered list the process to finding and recreating the issue, example below] 10 | 11 | 1. User goes to delete a dataset (to save space or whatever) 12 | 2. User gets popup modal warning 13 | 3. User deletes and it's lost forever 14 | 15 | ## Expected result 16 | 17 | [describe what you would expect to have resulted from this process] 18 | 19 | ## Current result 20 | 21 | [describe what you you currently experience from this process, and thereby explain the bug] 22 | 23 | ## Possible Fix 24 | 25 | [not obligatory, but suggest fixes or reasons for the bug] 26 | 27 | * Modal tells the user what dataset is being deleted, like “You are about to delete this dataset: car_crashes_2014.” 28 | * A temporary "Trashcan" where you can recover a just deleted dataset if you mess up (maybe it's only good for a few hours, and then it cleans the cache assuming you made the right decision). 29 | 30 | ## `name of issue` screenshot 31 | 32 | [if relevant, include a screenshot] 33 | 34 | 35 | ### Thanks! 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Icon 2 | 3 | 4 | 5 | # Louvre 6 | 7 | A small customizable image picker. Useful to handle an gallery image pick action built-in your app. 8 | 9 |
10 | 11 | [![License Apache 2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=true)](http://www.apache.org/licenses/LICENSE-2.0) 12 | ![minSdkVersion 19](https://img.shields.io/badge/minSdkVersion-19-red.svg?style=true) 13 | ![compileSdkVersion 25](https://img.shields.io/badge/compileSdkVersion-25-yellow.svg?style=true) 14 | [![maven-central](https://img.shields.io/maven-central/v/com.github.andremion/louvre.svg)](https://search.maven.org/#artifactdetails%7Ccom.github.andremion%7Clouvre%7C1.3.0%7Caar) 15 | 16 | [![Android Arsenal Louvre](https://img.shields.io/badge/Android%20Arsenal-Louvre-green.svg?style=true)](https://android-arsenal.com/details/1/5188) 17 | [![MaterialUp Louvre](https://img.shields.io/badge/MaterialUp-Louvre-blue.svg?style=true)](https://www.uplabs.com/posts/louvre) 18 | 19 |

20 | Sample
21 | *Images from Google Image Search 22 |

23 | 24 | ## Installation 25 | 26 | Add this in your root `build.gradle` file (**not** your app module `build.gradle` file): 27 | 28 | ```gradle 29 | allprojects { 30 | repositories { 31 | ... 32 | maven { url "https://jitpack.io" } 33 | } 34 | } 35 | ``` 36 | 37 | Then, add the library in your app module `build.gradle` 38 | 39 | ```groovy 40 | dependencies{ 41 | compile 'com.github.andremion:louvre:[LATEST VERSION]' 42 | } 43 | ``` 44 | 45 | ## Usage 46 | 47 | Choose one of the **Louvre** themes to use in `GalleryActivity` and override it to define your app color palette. 48 | 49 | ```xml 50 | 55 | ``` 56 | ```xml 57 | 62 | ``` 63 | ```xml 64 | 69 | ``` 70 | 71 | For `PreviewActivity` you just need to define the accent color. 72 | 73 | ```xml 74 | 77 | ``` 78 | 79 | Declare the **Louvre** activities in `AndroidManifest.xml` file using your new app themes. 80 | 81 | ```xml 82 | 85 | 88 | ``` 89 | 90 | Add `READ_EXTERNAL_STORAGE` permission in your `AndroidManifest.xml` file. 91 | 92 | ```xml 93 | 94 | ``` 95 | 96 | In your `Activity` you just need the below lines of code to open the **Louvre**. 97 | 98 | ```java 99 | Louvre.init(myActivity) 100 | .setRequestCode(LOUVRE_REQUEST_CODE) 101 | .open(); 102 | ``` 103 | 104 | You can also use a `Fragment` to open the **Louvre**. In this case, the `Fragment` will get the `onActivityResult` callback. 105 | 106 | ```java 107 | Louvre.init(myFragment) 108 | .setRequestCode(LOUVRE_REQUEST_CODE) 109 | .open(); 110 | ``` 111 | 112 | But you can customize the picker: 113 | 114 | ######Setting the max images allowed to pick 115 | ```java 116 | louvre.setMaxSelection(10) 117 | ``` 118 | 119 | ######Setting the current selected items 120 | ```java 121 | List selection; 122 | ... 123 | louvre.setSelection(selection) 124 | ``` 125 | 126 | ######Setting the media type to filter the query with a combination of one of these types: `Louvre.IMAGE_TYPE_BMP`, `Louvre.IMAGE_TYPE_JPEG`, `Louvre.IMAGE_TYPE_PNG` 127 | ```java 128 | louvre.setMediaTypeFilter(Louvre.IMAGE_TYPE_JPEG, Louvre.IMAGE_TYPE_PNG) 129 | ``` 130 | 131 | See more at the [sample](https://github.com/andremion/Louvre/tree/master/sample) 132 | 133 | ## Libraries and tools used in the project 134 | 135 | * [Design Support Library](http://developer.android.com/intl/pt-br/tools/support-library/features.html#design) 136 | The Design package provides APIs to support adding material design components and patterns to your apps. 137 | * [CounterFab](https://github.com/andremion/CounterFab) 138 | A FloatingActionButton subclass that shows a counter badge on right top corner. 139 | * [Glide](https://github.com/bumptech/glide) 140 | An image loading and caching library for Android focused on smooth scrolling 141 | * [PhotoView](https://github.com/chrisbanes/PhotoView) 142 | Implementation of ImageView for Android that supports zooming, by various touch gestures. 143 | 144 | ## Contributing 145 | 146 | Contributions are always welcome! 147 | 148 | **Issues:** 149 | Fell free to open a new issue. Follow the [ISSUE_TEMPLATE.MD](https://github.com/andremion/Louvre/tree/master/ISSUE_TEMPLATE.md) 150 | 151 | Follow the "fork-and-pull" Git workflow. 152 | 153 | 1. **Fork** the repo on GitHub 154 | 2. **Clone** the project to your own machine 155 | 3. **Commit** changes to your own branch 156 | 4. **Merge** with current *development* branch 157 | 5. **Push** your work back up to your fork 158 | 7. Submit a **Pull request** your changes can be reviewed (please refere the issue if reported) 159 | 160 | **Prevent** code-style related changes. Format the code before commiting. 161 | 162 | ## License 163 | 164 | Copyright 2017 André Mion 165 | 166 | Licensed under the Apache License, Version 2.0 (the "License"); 167 | you may not use this file except in compliance with the License. 168 | You may obtain a copy of the License at 169 | 170 | http://www.apache.org/licenses/LICENSE-2.0 171 | 172 | Unless required by applicable law or agreed to in writing, software 173 | distributed under the License is distributed on an "AS IS" BASIS, 174 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 175 | See the License for the specific language governing permissions and 176 | limitations under the License. 177 | -------------------------------------------------------------------------------- /art/feature-graphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/feature-graphic.png -------------------------------------------------------------------------------- /art/gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/gallery.png -------------------------------------------------------------------------------- /art/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/preview.png -------------------------------------------------------------------------------- /art/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/sample.gif -------------------------------------------------------------------------------- /art/sample_optimized.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/sample_optimized.gif -------------------------------------------------------------------------------- /art/screen1-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen1-github.png -------------------------------------------------------------------------------- /art/screen1-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen1-store.png -------------------------------------------------------------------------------- /art/screen2-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen2-github.png -------------------------------------------------------------------------------- /art/screen2-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen2-store.png -------------------------------------------------------------------------------- /art/screen3-arsenal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen3-arsenal.png -------------------------------------------------------------------------------- /art/screen3-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen3-github.png -------------------------------------------------------------------------------- /art/screen3-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/screen3-store.png -------------------------------------------------------------------------------- /art/transition_fixed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_fixed.gif -------------------------------------------------------------------------------- /art/transition_wrong0_go_different_page.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong0_go_different_page.gif -------------------------------------------------------------------------------- /art/transition_wrong1_go_previous_image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong1_go_previous_image.gif -------------------------------------------------------------------------------- /art/transition_wrong2_go_more_previuos_image.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong2_go_more_previuos_image.gif -------------------------------------------------------------------------------- /art/transition_wrong3_update_position.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong3_update_position.gif -------------------------------------------------------------------------------- /art/transition_wrong4_update_shared_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong4_update_shared_preview.gif -------------------------------------------------------------------------------- /art/transition_wrong5_update_shared_gallery.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/art/transition_wrong5_update_shared_gallery.gif -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlinVersion = "1.4.10" 3 | repositories { 4 | jcenter() 5 | google() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:4.1.0' 9 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" 11 | } 12 | } 13 | 14 | plugins { 15 | id "com.jfrog.bintray" version "1.7.3" 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | jcenter() 23 | maven { url "https://jitpack.io" } 24 | maven { url "https://maven.google.com" } 25 | } 26 | project.ext { 27 | 28 | compileSdkVersion = 30 29 | minSdkVersion = 16 30 | targetSdkVersion = 30 31 | 32 | versionCode = 10 33 | versionName = "1.3.0" 34 | 35 | materialVersion = '1.2.1' 36 | recyclerViewVersion = '1.1.0' 37 | counterFabVersion = '1.2.2' 38 | glideVersion = '4.11.0' 39 | photoViewVersion = '2.0.0' 40 | 41 | junitVersion = '4.13.1' 42 | 43 | name = 'Louvre' 44 | description = 'A small customizable image picker. Useful to handle an gallery image pick action built-in your app.' 45 | url = 'https://play.google.com/store/apps/details?id=com.andremion.louvre.sample' 46 | 47 | licenseName = 'The Apache Software License, Version 2.0' 48 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 49 | allLicenses = ["Apache-2.0"] 50 | 51 | bintrayRepo = 'github' 52 | group = 'com.github.andremion' 53 | artifact = 'louvre' 54 | 55 | gitUrl = 'https://github.com/andremion/' + name 56 | vcsUrl = gitUrl + '.git' 57 | issueTracker = gitUrl + '/issues' 58 | 59 | developerId = 'andremion' 60 | developerName = 'André Mion' 61 | developerEmail = 'andremion@gmail.com' 62 | } 63 | } 64 | 65 | task clean(type: Delete) { 66 | delete rootProject.buildDir 67 | } 68 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536m 2 | 3 | android.enableJetifier=true 4 | android.useAndroidX=true 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Nov 03 09:28:16 WET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /louvre/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /louvre/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'com.github.dcendents.android-maven' 4 | apply plugin: 'com.jfrog.bintray' 5 | 6 | android { 7 | compileSdkVersion project.ext.compileSdkVersion 8 | defaultConfig { 9 | minSdkVersion project.ext.minSdkVersion 10 | targetSdkVersion project.ext.targetSdkVersion 11 | versionCode project.ext.versionCode 12 | versionName project.ext.versionName 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | vectorDrawables.useSupportLibrary = true 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | } 23 | 24 | dependencies { 25 | implementation fileTree(dir: 'libs', include: ['*.jar']) 26 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 27 | implementation "com.google.android.material:material:$materialVersion" 28 | implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion" 29 | implementation "com.github.andremion:counterfab:$counterFabVersion" 30 | implementation "com.github.bumptech.glide:glide:$glideVersion" 31 | implementation "com.github.chrisbanes:PhotoView:$photoViewVersion" 32 | 33 | testImplementation "junit:junit:$junitVersion" 34 | } 35 | 36 | //apply from: 'https://raw.githubusercontent.com/andremion/JCenter/master/deploy.gradle' 37 | -------------------------------------------------------------------------------- /louvre/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /louvre/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/Louvre.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre; 18 | 19 | import android.app.Activity; 20 | import android.content.Intent; 21 | import android.net.Uri; 22 | import androidx.annotation.IntRange; 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.StringDef; 25 | import androidx.fragment.app.Fragment; 26 | import androidx.appcompat.app.AppCompatDelegate; 27 | 28 | import com.andremion.louvre.home.GalleryActivity; 29 | 30 | import java.lang.annotation.Retention; 31 | import java.lang.annotation.RetentionPolicy; 32 | import java.util.List; 33 | 34 | /** 35 | * A small customizable image picker. Useful to handle an image pick action built-in 36 | */ 37 | public class Louvre { 38 | 39 | static { 40 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true); 41 | } 42 | 43 | public static final String IMAGE_TYPE_BMP = "image/bmp"; 44 | public static final String IMAGE_TYPE_JPEG = "image/jpeg"; 45 | public static final String IMAGE_TYPE_PNG = "image/png"; 46 | public static final String[] IMAGE_TYPES = {IMAGE_TYPE_BMP, IMAGE_TYPE_JPEG, IMAGE_TYPE_PNG}; 47 | 48 | @StringDef({IMAGE_TYPE_BMP, IMAGE_TYPE_JPEG, IMAGE_TYPE_PNG}) 49 | @Retention(RetentionPolicy.SOURCE) 50 | @interface MediaType { 51 | } 52 | 53 | private Activity mActivity; 54 | private Fragment mFragment; 55 | private int mRequestCode; 56 | private int mMaxSelection; 57 | private List mSelection; 58 | private String[] mMediaTypeFilter; 59 | 60 | private Louvre(@NonNull Activity activity) { 61 | mActivity = activity; 62 | mRequestCode = -1; 63 | } 64 | 65 | private Louvre(@NonNull Fragment fragment) { 66 | mFragment = fragment; 67 | mRequestCode = -1; 68 | } 69 | 70 | public static Louvre init(@NonNull Activity activity) { 71 | return new Louvre(activity); 72 | } 73 | 74 | public static Louvre init(@NonNull Fragment fragment) { 75 | return new Louvre(fragment); 76 | } 77 | 78 | /** 79 | * Set the request code to return on {@link Activity#onActivityResult(int, int, Intent)} 80 | */ 81 | public Louvre setRequestCode(int requestCode) { 82 | mRequestCode = requestCode; 83 | return this; 84 | } 85 | 86 | /** 87 | * Set the max images allowed to pick 88 | */ 89 | public Louvre setMaxSelection(@IntRange(from = 0) int maxSelection) { 90 | mMaxSelection = maxSelection; 91 | return this; 92 | } 93 | 94 | /** 95 | * Set the current selected items 96 | */ 97 | public Louvre setSelection(@NonNull List selection) { 98 | mSelection = selection; 99 | return this; 100 | } 101 | 102 | /** 103 | * Set the media type to filter the query with a combination of one of these types: {@link #IMAGE_TYPE_BMP}, {@link #IMAGE_TYPE_JPEG}, {@link #IMAGE_TYPE_PNG} 104 | */ 105 | public Louvre setMediaTypeFilter(@MediaType @NonNull String... mediaTypeFilter) { 106 | mMediaTypeFilter = mediaTypeFilter; 107 | return this; 108 | } 109 | 110 | public void open() { 111 | if (mRequestCode == -1) { 112 | throw new IllegalArgumentException("You need to define a request code in setRequestCode(int) method"); 113 | } 114 | if (mActivity != null) { 115 | GalleryActivity.startActivity(mActivity, mRequestCode, mMaxSelection, mSelection, mMediaTypeFilter); 116 | } else { 117 | GalleryActivity.startActivity(mFragment, mRequestCode, mMaxSelection, mSelection, mMediaTypeFilter); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/StoragePermissionActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre; 18 | 19 | import android.Manifest; 20 | import android.content.Intent; 21 | import android.content.pm.PackageManager; 22 | import android.net.Uri; 23 | import android.provider.Settings; 24 | import androidx.annotation.NonNull; 25 | import com.google.android.material.snackbar.Snackbar; 26 | import androidx.core.app.ActivityCompat; 27 | import androidx.core.content.ContextCompat; 28 | import androidx.appcompat.app.AppCompatActivity; 29 | import android.view.View; 30 | 31 | /** 32 | * {@link AppCompatActivity} that manages request for {@link Manifest.permission#READ_EXTERNAL_STORAGE} Permission 33 | */ 34 | public abstract class StoragePermissionActivity extends AppCompatActivity { 35 | 36 | private static final int REQUEST_READ_EXTERNAL_STORAGE = 4321; 37 | private static final int REQUEST_APP_SETTINGS = 1234; 38 | 39 | public void askForPermission() { 40 | 41 | if (ContextCompat.checkSelfPermission(this, 42 | Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 43 | 44 | // Should we show an explanation? 45 | if (ActivityCompat.shouldShowRequestPermissionRationale(this, 46 | Manifest.permission.READ_EXTERNAL_STORAGE)) { 47 | 48 | // Show an explanation to the user *asynchronously* 49 | showExplanation(); 50 | 51 | } else { 52 | 53 | // No explanation needed, we can request the permission. 54 | ActivityCompat.requestPermissions(this, 55 | new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 56 | REQUEST_READ_EXTERNAL_STORAGE); 57 | } 58 | 59 | } else { 60 | 61 | onPermissionGranted(); 62 | } 63 | 64 | } 65 | 66 | @Override 67 | public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { 68 | switch (requestCode) { 69 | case REQUEST_READ_EXTERNAL_STORAGE: { 70 | // If request is cancelled, the result arrays are empty. 71 | if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 72 | onPermissionGranted(); 73 | } else { 74 | showExplanation(); 75 | } 76 | break; 77 | } 78 | default: 79 | } 80 | } 81 | 82 | @Override 83 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 84 | if (requestCode == REQUEST_APP_SETTINGS) { 85 | askForPermission(); 86 | } else { 87 | super.onActivityResult(requestCode, resultCode, data); 88 | } 89 | } 90 | 91 | /** 92 | * Callback when permission is granted 93 | */ 94 | public abstract void onPermissionGranted(); 95 | 96 | /** 97 | * Show UI with rationale for requesting a storage permission. 98 | *

99 | * "You should do this only if you do not have the permission and the context in 100 | * which the permission is requested does not clearly communicate to the user 101 | * what would be the benefit from granting this permission." 102 | *

103 | */ 104 | private void showExplanation() { 105 | Snackbar.make(findSuitableView(), getString(R.string.activity_gallery_permission_request_explanation), Snackbar.LENGTH_INDEFINITE) 106 | .setAction(R.string.activity_gallery_permission_request_settings, new View.OnClickListener() { 107 | @Override 108 | public void onClick(View v) { 109 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, 110 | Uri.fromParts("package", getPackageName(), null)); 111 | intent.addCategory(Intent.CATEGORY_DEFAULT); 112 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 113 | startActivityForResult(intent, REQUEST_APP_SETTINGS); 114 | } 115 | }) 116 | .show(); 117 | } 118 | 119 | private View findSuitableView() { 120 | View view = findViewById(R.id.coordinator_layout); 121 | if (view == null) { 122 | view = getWindow().getDecorView(); 123 | } 124 | return view; 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/data/MediaLoader.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.andremion.louvre.data 17 | 18 | import android.database.Cursor 19 | import android.database.MatrixCursor 20 | import android.database.MergeCursor 21 | import android.os.Build 22 | import android.os.Bundle 23 | import android.provider.MediaStore 24 | import androidx.annotation.IntRange 25 | import androidx.fragment.app.FragmentActivity 26 | import androidx.loader.app.LoaderManager 27 | import androidx.loader.content.CursorLoader 28 | import androidx.loader.content.Loader 29 | import com.andremion.louvre.R 30 | 31 | private const val TIME_LOADER = 0 32 | private const val BUCKET_LOADER = 1 33 | private const val MEDIA_LOADER = 2 34 | private const val ARG_BUCKET_ID = MediaStore.Images.Media.BUCKET_ID 35 | 36 | /** 37 | * [Loader] for media and bucket data 38 | */ 39 | class MediaLoader : LoaderManager.LoaderCallbacks { 40 | 41 | interface Callbacks { 42 | fun onBucketLoadFinished(data: Cursor?) 43 | fun onMediaLoadFinished(data: Cursor?) 44 | } 45 | 46 | private var activity: FragmentActivity? = null 47 | private var callbacks: Callbacks? = null 48 | private var typeFilter = "1" // Means all media type. 49 | 50 | override fun onCreateLoader(id: Int, args: Bundle?): Loader = 51 | ensureActivityAttached().let { activity -> 52 | when (id) { 53 | TIME_LOADER -> CursorLoader( 54 | activity, 55 | GALLERY_URI, 56 | IMAGE_PROJECTION, 57 | typeFilter, 58 | null, 59 | MEDIA_SORT_ORDER 60 | ) 61 | BUCKET_LOADER -> CursorLoader( 62 | activity, 63 | GALLERY_URI, 64 | BUCKET_PROJECTION, 65 | "$typeFilter AND $BUCKET_SELECTION", 66 | null, 67 | BUCKET_SORT_ORDER 68 | ) 69 | // id == MEDIA_LOADER 70 | else -> CursorLoader( 71 | activity, 72 | GALLERY_URI, 73 | IMAGE_PROJECTION, 74 | "${MediaStore.Images.Media.BUCKET_ID}=${args?.getLong(ARG_BUCKET_ID) ?: 0} AND $typeFilter", 75 | null, 76 | MEDIA_SORT_ORDER 77 | ) 78 | } 79 | } 80 | 81 | override fun onLoadFinished(loader: Loader, data: Cursor?) { 82 | callbacks?.let { callbacks -> 83 | if (loader.id == BUCKET_LOADER) { 84 | callbacks.onBucketLoadFinished(finishUpBuckets(data)) 85 | } else { 86 | callbacks.onMediaLoadFinished(data) 87 | } 88 | } 89 | } 90 | 91 | override fun onLoaderReset(loader: Loader) { 92 | // no-op 93 | } 94 | 95 | fun onAttach(activity: FragmentActivity, callbacks: Callbacks) { 96 | this.activity = activity 97 | this.callbacks = callbacks 98 | } 99 | 100 | fun onDetach() { 101 | activity = null 102 | callbacks = null 103 | } 104 | 105 | fun setMediaTypes(mediaTypes: Array) { 106 | val filter = mediaTypes.joinToString { "'$it'" } 107 | if (filter.isNotEmpty()) { 108 | typeFilter = "${MediaStore.Images.Media.MIME_TYPE} IN ($filter)" 109 | } 110 | } 111 | 112 | fun loadBuckets() { 113 | LoaderManager.getInstance(ensureActivityAttached()) 114 | .restartLoader(BUCKET_LOADER, null, this) 115 | } 116 | 117 | fun loadByBucket(@IntRange(from = 0) bucketId: Long) { 118 | ensureActivityAttached().let { activity -> 119 | if (ALL_MEDIA_BUCKET_ID == bucketId) { 120 | LoaderManager.getInstance(activity).restartLoader(TIME_LOADER, null, this) 121 | } else { 122 | val args = Bundle() 123 | args.putLong(ARG_BUCKET_ID, bucketId) 124 | LoaderManager.getInstance(activity).restartLoader(MEDIA_LOADER, args, this) 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Ensure that a FragmentActivity is attached to this loader. 131 | */ 132 | private fun ensureActivityAttached(): FragmentActivity = 133 | requireNotNull(activity) { "The FragmentActivity was not attached!" } 134 | 135 | private fun finishUpBuckets(cursor: Cursor?): Cursor? = 136 | MergeCursor( 137 | arrayOf( 138 | addAllMediaBucketItem(cursor), 139 | if (isAllowedAggregatedFunctions) cursor 140 | else aggregateBuckets(cursor) 141 | ) 142 | ) 143 | 144 | /** 145 | * Add "All Media" item as the first row of bucket items. 146 | * 147 | * @param cursor The original data of all bucket items 148 | * @return The data with "All Media" item added 149 | */ 150 | private fun addAllMediaBucketItem(cursor: Cursor?): Cursor? = 151 | cursor?.run { 152 | if (!moveToPosition(0)) return null 153 | val id = ALL_MEDIA_BUCKET_ID 154 | val label = ensureActivityAttached().getString(R.string.activity_gallery_bucket_all_media) 155 | val data = getString(getColumnIndex(MediaStore.Images.Media.DATA)) 156 | MatrixCursor(BUCKET_PROJECTION).apply { 157 | newRow() 158 | .add(id) 159 | .add(label) 160 | .add(data) 161 | } 162 | } 163 | 164 | /** 165 | * Since we are not allowed to use SQL aggregation functions we need to do that on code 166 | * 167 | * @param cursor The original data of all bucket items 168 | * @return The data aggregated by buckets 169 | */ 170 | private fun aggregateBuckets(cursor: Cursor?): Cursor? = 171 | cursor?.run { 172 | val idIndex = getColumnIndex(MediaStore.Images.Media.BUCKET_ID) 173 | val labelIndex = getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME) 174 | val dataIndex = getColumnIndex(MediaStore.Images.Media.DATA) 175 | 176 | val aggregatedBucket = MatrixCursor(BUCKET_PROJECTION) 177 | var previousId = 0L 178 | 179 | for (position in 0 until cursor.count) { 180 | moveToPosition(position) 181 | val id = getLong(idIndex) 182 | val label = getString(labelIndex) 183 | val data = getString(dataIndex) 184 | 185 | if (id != previousId) { 186 | aggregatedBucket.newRow() 187 | .add(id) 188 | .add(label) 189 | .add(data) 190 | } 191 | previousId = id 192 | } 193 | 194 | aggregatedBucket 195 | } 196 | } 197 | 198 | internal val isAllowedAggregatedFunctions = Build.VERSION.SDK_INT < Build.VERSION_CODES.Q 199 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/data/MediaQuery.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.andremion.louvre.data 17 | 18 | import android.net.Uri 19 | import android.provider.MediaStore 20 | 21 | /** 22 | * Helper properties used by [MediaLoader] 23 | */ 24 | 25 | internal val GALLERY_URI: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI 26 | internal val IMAGE_PROJECTION: Array = arrayOf( 27 | MediaStore.Images.Media._ID, 28 | MediaStore.Images.Media.BUCKET_ID, 29 | MediaStore.Images.Media.DISPLAY_NAME, 30 | MediaStore.Images.Media.DATA 31 | ) 32 | internal const val ALL_MEDIA_BUCKET_ID: Long = 0L 33 | internal const val MEDIA_SORT_ORDER: String = "${MediaStore.Images.Media.DATE_TAKEN} DESC" 34 | internal val BUCKET_PROJECTION: Array = arrayOf( 35 | MediaStore.Images.Media.BUCKET_ID, 36 | MediaStore.Images.Media.BUCKET_DISPLAY_NAME, 37 | MediaStore.Images.Media.DATA 38 | ) 39 | 40 | // The template for "WHERE" parameter is like: 41 | // SELECT ... FROM ... WHERE (%s) 42 | // and we make it look like: 43 | // SELECT ... FROM ... WHERE (1) GROUP BY (1) 44 | // The "WHERE (1)" means true. 45 | // The "GROUP BY (1)" means the first column specified after SELECT. 46 | // Note that because there is a "(" and )" in the template, we use "1)" and "(1" to match it. 47 | // 48 | // *Hack pulled from https://android.googlesource.com/platform/packages/apps/Gallery2/+/android-4.4.2_r2/src/com/android/gallery3d/data/BucketHelper.java 49 | // 50 | // *Aggregation functions are not allowed from API 29 on 51 | internal val BUCKET_SELECTION: String = if (isAllowedAggregatedFunctions) "1) GROUP BY (1" else "1" 52 | internal val BUCKET_SORT_ORDER: String = 53 | if (isAllowedAggregatedFunctions) "MAX(${MediaStore.Images.Media.DATE_TAKEN}) DESC" 54 | else "${MediaStore.Images.Media.BUCKET_ID}, ${MediaStore.Images.Media.DATE_TAKEN} DESC" 55 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/home/GalleryActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.home; 18 | 19 | import android.app.Activity; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.net.Uri; 23 | import android.os.Build; 24 | import android.os.Bundle; 25 | import androidx.annotation.IntRange; 26 | import androidx.annotation.NonNull; 27 | import androidx.annotation.Nullable; 28 | import com.google.android.material.snackbar.Snackbar; 29 | import androidx.fragment.app.Fragment; 30 | import androidx.appcompat.widget.Toolbar; 31 | import android.transition.Transition; 32 | import android.transition.TransitionInflater; 33 | import android.view.View; 34 | import android.view.ViewGroup; 35 | 36 | import com.andremion.counterfab.CounterFab; 37 | import com.andremion.louvre.R; 38 | import com.andremion.louvre.StoragePermissionActivity; 39 | import com.andremion.louvre.preview.PreviewActivity; 40 | import com.andremion.louvre.util.transition.TransitionCallback; 41 | 42 | import java.util.ArrayList; 43 | import java.util.LinkedList; 44 | import java.util.List; 45 | 46 | public class GalleryActivity extends StoragePermissionActivity implements GalleryFragment.Callbacks, View.OnClickListener { 47 | 48 | private static final String EXTRA_MAX_SELECTION = GalleryActivity.class.getPackage().getName() + ".extra.MAX_SELECTION"; 49 | private static final String EXTRA_MEDIA_TYPE_FILTER = GalleryActivity.class.getPackage().getName() + ".extra.MEDIA_TYPE_FILTER"; 50 | private static final String EXTRA_SELECTION = GalleryActivity.class.getPackage().getName() + ".extra.SELECTION"; 51 | private static final int DEFAULT_MAX_SELECTION = 1; 52 | private static final String TITLE_STATE = "title_state"; 53 | private static final int PREVIEW_REQUEST_CODE = 0; 54 | 55 | /** 56 | * Start the Gallery Activity with additional launch information. 57 | * 58 | * @param activity Context to launch activity from. 59 | * @param requestCode If >= 0, this code will be returned in onActivityResult() when the activity exits. 60 | * @param maxSelection The max count of image selection 61 | * @param selection The current image selection 62 | * @param mediaTypeFilter The media types that will display 63 | */ 64 | public static void startActivity(@NonNull Activity activity, int requestCode, 65 | @IntRange(from = 0) int maxSelection, 66 | List selection, 67 | String... mediaTypeFilter) { 68 | Intent intent = buildIntent(activity, maxSelection, selection, mediaTypeFilter); 69 | activity.startActivityForResult(intent, requestCode); 70 | } 71 | 72 | /** 73 | * Start the Gallery Activity with additional launch information. 74 | * 75 | * @param fragment Context to launch fragment from. 76 | * @param requestCode If >= 0, this code will be returned in onActivityResult() when the fragment exits. 77 | * @param maxSelection The max count of image selection 78 | * @param selection The current image selection 79 | * @param mediaTypeFilter The media types that will display 80 | */ 81 | public static void startActivity(@NonNull Fragment fragment, int requestCode, 82 | @IntRange(from = 0) int maxSelection, 83 | List selection, 84 | String... mediaTypeFilter) { 85 | Intent intent = buildIntent(fragment.getContext(), maxSelection, selection, mediaTypeFilter); 86 | fragment.startActivityForResult(intent, requestCode); 87 | } 88 | 89 | @NonNull 90 | private static Intent buildIntent(@NonNull Context context, @IntRange(from = 0) int maxSelection, List selection, String[] mediaTypeFilter) { 91 | Intent intent = new Intent(context, GalleryActivity.class); 92 | if (maxSelection > 0) { 93 | intent.putExtra(EXTRA_MAX_SELECTION, maxSelection); 94 | } 95 | if (selection != null) { 96 | intent.putExtra(EXTRA_SELECTION, new LinkedList<>(selection)); 97 | } 98 | if (mediaTypeFilter != null && mediaTypeFilter.length > 0) { 99 | intent.putExtra(EXTRA_MEDIA_TYPE_FILTER, mediaTypeFilter); 100 | } 101 | return intent; 102 | } 103 | 104 | public static List getSelection(Intent data) { 105 | return data.getParcelableArrayListExtra(EXTRA_SELECTION); 106 | } 107 | 108 | private GalleryFragment mFragment; 109 | private ViewGroup mContentView; 110 | private CounterFab mFab; 111 | 112 | @Override 113 | protected void onCreate(@Nullable Bundle savedInstanceState) { 114 | super.onCreate(savedInstanceState); 115 | setContentView(R.layout.activity_gallery); 116 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 117 | setSupportActionBar(toolbar); 118 | setupTransition(); 119 | 120 | mContentView = (ViewGroup) findViewById(R.id.coordinator_layout); 121 | 122 | mFab = (CounterFab) findViewById(R.id.fab_done); 123 | mFab.setOnClickListener(this); 124 | 125 | mFragment = (GalleryFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_gallery); 126 | mFragment.setMaxSelection(getIntent().getIntExtra(EXTRA_MAX_SELECTION, DEFAULT_MAX_SELECTION)); 127 | if (getIntent().hasExtra(EXTRA_SELECTION)) { 128 | //noinspection unchecked 129 | mFragment.setSelection((List) getIntent().getSerializableExtra(EXTRA_SELECTION)); 130 | } 131 | if (getIntent().hasExtra(EXTRA_MEDIA_TYPE_FILTER)) { 132 | mFragment.setMediaTypeFilter(getIntent().getStringArrayExtra(EXTRA_MEDIA_TYPE_FILTER)); 133 | } 134 | 135 | if (savedInstanceState == null) { 136 | setResult(RESULT_CANCELED); 137 | askForPermission(); 138 | } else { 139 | setActionBarTitle(savedInstanceState.getString(TITLE_STATE)); 140 | } 141 | } 142 | 143 | private void setupTransition() { 144 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 145 | TransitionInflater inflater = TransitionInflater.from(this); 146 | Transition exitTransition = inflater.inflateTransition(R.transition.gallery_exit); 147 | exitTransition.addListener(new TransitionCallback() { 148 | @Override 149 | public void onTransitionStart(Transition transition) { 150 | mFab.hide(); 151 | } 152 | }); 153 | getWindow().setExitTransition(exitTransition); 154 | Transition reenterTransition = inflater.inflateTransition(R.transition.gallery_reenter); 155 | reenterTransition.addListener(new TransitionCallback() { 156 | @Override 157 | public void onTransitionEnd(Transition transition) { 158 | mFab.show(); 159 | } 160 | 161 | @Override 162 | public void onTransitionCancel(Transition transition) { 163 | mFab.show(); 164 | } 165 | }); 166 | getWindow().setReenterTransition(reenterTransition); 167 | } 168 | } 169 | 170 | @Override 171 | public void onPermissionGranted() { 172 | mFragment.loadBuckets(); 173 | } 174 | 175 | @SuppressWarnings("ConstantConditions") 176 | @Override 177 | protected void onSaveInstanceState(Bundle outState) { 178 | super.onSaveInstanceState(outState); 179 | outState.putCharSequence(TITLE_STATE, getSupportActionBar().getTitle()); 180 | } 181 | 182 | @Override 183 | public void onBackPressed() { 184 | if (mFragment.onBackPressed()) { 185 | resetActionBarTitle(); 186 | } else { 187 | super.onBackPressed(); 188 | } 189 | } 190 | 191 | @Override 192 | public void onActivityReenter(int resultCode, Intent data) { 193 | mFragment.onActivityReenter(resultCode, data); 194 | } 195 | 196 | @Override 197 | public void onClick(View v) { 198 | Intent data = new Intent(); 199 | data.putExtra(EXTRA_SELECTION, (ArrayList) mFragment.getSelection()); 200 | setResult(RESULT_OK, data); 201 | finish(); 202 | } 203 | 204 | @Override 205 | public void onBucketClick(String label) { 206 | setActionBarTitle(label); 207 | } 208 | 209 | @Override 210 | public void onMediaClick(@NonNull View imageView, @NonNull View checkView, long bucketId, int position) { 211 | if (getIntent().hasExtra(EXTRA_MEDIA_TYPE_FILTER)) { 212 | PreviewActivity.startActivity(this, PREVIEW_REQUEST_CODE, imageView, checkView, bucketId, position, mFragment.getSelection(), 213 | getIntent().getIntExtra(EXTRA_MAX_SELECTION, DEFAULT_MAX_SELECTION), 214 | getIntent().getStringArrayExtra(EXTRA_MEDIA_TYPE_FILTER)); 215 | } else { 216 | PreviewActivity.startActivity(this, PREVIEW_REQUEST_CODE, imageView, checkView, bucketId, position, mFragment.getSelection(), 217 | getIntent().getIntExtra(EXTRA_MAX_SELECTION, DEFAULT_MAX_SELECTION)); 218 | } 219 | } 220 | 221 | @Override 222 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 223 | // Before Lollipop we don't have Activity.onActivityReenter() callback, 224 | // so we have to call GalleryFragment.onActivityReenter() here. 225 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 226 | mFragment.onActivityReenter(resultCode, data); 227 | } 228 | 229 | if (requestCode == PREVIEW_REQUEST_CODE) { 230 | mFragment.setSelection(PreviewActivity.getSelection(data)); 231 | } else { 232 | super.onActivityResult(requestCode, resultCode, data); 233 | } 234 | } 235 | 236 | @Override 237 | public void onSelectionUpdated(int count) { 238 | mFab.setCount(count); 239 | } 240 | 241 | @Override 242 | public void onMaxSelectionReached() { 243 | Snackbar.make(mContentView, R.string.activity_gallery_max_selection_reached, Snackbar.LENGTH_SHORT).show(); 244 | } 245 | 246 | @Override 247 | public void onWillExceedMaxSelection() { 248 | Snackbar.make(mContentView, R.string.activity_gallery_will_exceed_max_selection, Snackbar.LENGTH_SHORT).show(); 249 | } 250 | 251 | @SuppressWarnings("ConstantConditions") 252 | private void setActionBarTitle(@Nullable CharSequence title) { 253 | getSupportActionBar().setTitle(title); 254 | } 255 | 256 | private void resetActionBarTitle() { 257 | setActionBarTitle(getTitle()); 258 | } 259 | 260 | } 261 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/home/GalleryAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.home; 18 | 19 | import android.database.Cursor; 20 | import android.net.Uri; 21 | import android.provider.MediaStore; 22 | import androidx.annotation.IntDef; 23 | import androidx.annotation.IntRange; 24 | import androidx.annotation.NonNull; 25 | import androidx.annotation.Nullable; 26 | import androidx.core.view.ViewCompat; 27 | import androidx.recyclerview.widget.LinearLayoutManager; 28 | import androidx.recyclerview.widget.RecyclerView; 29 | import android.view.LayoutInflater; 30 | import android.view.View; 31 | import android.view.ViewGroup; 32 | import android.widget.CheckedTextView; 33 | import android.widget.ImageView; 34 | import android.widget.TextView; 35 | 36 | import com.andremion.louvre.R; 37 | import com.andremion.louvre.util.AnimationHelper; 38 | import com.bumptech.glide.Glide; 39 | import com.bumptech.glide.request.RequestOptions; 40 | 41 | import java.io.File; 42 | import java.lang.annotation.Retention; 43 | import java.lang.annotation.RetentionPolicy; 44 | import java.util.LinkedList; 45 | import java.util.List; 46 | 47 | /** 48 | * {@link RecyclerView.Adapter} subclass used to bind {@link Cursor} items from {@link MediaStore} into {@link RecyclerView} 49 | *

50 | * We can have two types of {@link View} items: {@link #VIEW_TYPE_BUCKET} or {@link #VIEW_TYPE_MEDIA} 51 | */ 52 | class GalleryAdapter extends RecyclerView.Adapter { 53 | 54 | static final int VIEW_TYPE_BUCKET = 0; 55 | static final int VIEW_TYPE_MEDIA = 1; 56 | 57 | private static final String SELECTION_PAYLOAD = "selection"; 58 | private static final float SELECTED_SCALE = .8f; 59 | private static final float UNSELECTED_SCALE = 1f; 60 | 61 | @IntDef({VIEW_TYPE_BUCKET, VIEW_TYPE_MEDIA}) 62 | @Retention(RetentionPolicy.SOURCE) 63 | @interface ViewType { 64 | } 65 | 66 | interface Callbacks { 67 | 68 | void onBucketClick(long bucketId, String label); 69 | 70 | void onMediaClick(View imageView, View checkView, long bucketId, int position); 71 | 72 | void onSelectionUpdated(int count); 73 | 74 | void onMaxSelectionReached(); 75 | 76 | void onWillExceedMaxSelection(); 77 | } 78 | 79 | private final List mSelection; 80 | 81 | @Nullable 82 | private Callbacks mCallbacks; 83 | private int mMaxSelection; 84 | @Nullable 85 | private LinearLayoutManager mLayoutManager; 86 | private int mViewType = VIEW_TYPE_BUCKET; 87 | @Nullable 88 | private Cursor mData; 89 | 90 | GalleryAdapter() { 91 | mSelection = new LinkedList<>(); 92 | setHasStableIds(true); 93 | } 94 | 95 | void setCallbacks(@Nullable Callbacks callbacks) { 96 | mCallbacks = callbacks; 97 | } 98 | 99 | void setMaxSelection(@IntRange(from = 0) int maxSelection) { 100 | mMaxSelection = maxSelection; 101 | } 102 | 103 | public void setLayoutManager(@NonNull LinearLayoutManager layoutManager) { 104 | mLayoutManager = layoutManager; 105 | } 106 | 107 | void swapData(@ViewType int viewType, @Nullable Cursor data) { 108 | if (viewType != mViewType) { 109 | mViewType = viewType; 110 | } 111 | if (data != mData) { 112 | mData = data; 113 | notifyDataSetChanged(); 114 | } 115 | } 116 | 117 | @Override 118 | public long getItemId(int position) { 119 | if (mData != null && !mData.isClosed()) { 120 | mData.moveToPosition(position); 121 | if (VIEW_TYPE_MEDIA == mViewType) { 122 | return mData.getLong(mData.getColumnIndex(MediaStore.Images.Media._ID)); 123 | } else { 124 | return mData.getLong(mData.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)); 125 | } 126 | } 127 | return super.getItemId(position); 128 | } 129 | 130 | @Override 131 | public int getItemCount() { 132 | if (mData != null && !mData.isClosed()) { 133 | return mData.getCount(); 134 | } 135 | return 0; 136 | } 137 | 138 | @Override 139 | public int getItemViewType(int position) { 140 | return mViewType; 141 | } 142 | 143 | @Override 144 | public GalleryAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, @ViewType int viewType) { 145 | if (VIEW_TYPE_MEDIA == viewType) { 146 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_gallery_media, parent, false); 147 | return new MediaViewHolder(view); 148 | } else { 149 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_gallery_bucket, parent, false); 150 | return new BucketViewHolder(view); 151 | } 152 | } 153 | 154 | @Override 155 | public void onBindViewHolder(@NonNull GalleryAdapter.ViewHolder holder, int position) { 156 | Uri data = getData(position); 157 | String imageTransitionName = holder.itemView.getContext().getString(R.string.activity_gallery_image_transition, data.toString()); 158 | String checkboxTransitionName = holder.itemView.getContext().getString(R.string.activity_gallery_checkbox_transition, data.toString()); 159 | ViewCompat.setTransitionName(holder.mImageView, imageTransitionName); 160 | Glide.with(holder.mImageView.getContext()) 161 | .load(data) 162 | .apply(RequestOptions.skipMemoryCacheOf(true) 163 | .centerCrop() 164 | .placeholder(R.color.gallery_item_background)) 165 | .into(holder.mImageView); 166 | 167 | boolean selected = isSelected(position); 168 | if (selected) { 169 | holder.mImageView.setScaleX(SELECTED_SCALE); 170 | holder.mImageView.setScaleY(SELECTED_SCALE); 171 | } else { 172 | holder.mImageView.setScaleX(UNSELECTED_SCALE); 173 | holder.mImageView.setScaleY(UNSELECTED_SCALE); 174 | } 175 | 176 | if (VIEW_TYPE_MEDIA == getItemViewType(position)) { 177 | MediaViewHolder viewHolder = (MediaViewHolder) holder; 178 | ViewCompat.setTransitionName(viewHolder.mCheckView, checkboxTransitionName); 179 | viewHolder.mCheckView.setChecked(selected); 180 | holder.mImageView.setContentDescription(getLabel(position)); 181 | } else { 182 | BucketViewHolder viewHolder = (BucketViewHolder) holder; 183 | viewHolder.mTextView.setText(getLabel(position)); 184 | } 185 | } 186 | 187 | /** 188 | * Binding view holder with payloads is used to handle partial changes in item. 189 | */ 190 | @Override 191 | public void onBindViewHolder(GalleryAdapter.ViewHolder holder, int position, List payloads) { 192 | if (payloads.isEmpty()) { // If doesn't have any payload then bind the fully item 193 | super.onBindViewHolder(holder, position, payloads); 194 | } else { 195 | for (Object payload : payloads) { 196 | boolean selected = isSelected(position); 197 | if (SELECTION_PAYLOAD.equals(payload)) { 198 | if (VIEW_TYPE_MEDIA == getItemViewType(position)) { 199 | MediaViewHolder viewHolder = (MediaViewHolder) holder; 200 | viewHolder.mCheckView.setChecked(selected); 201 | if (selected) { 202 | AnimationHelper.scaleView(holder.mImageView, SELECTED_SCALE); 203 | } else { 204 | AnimationHelper.scaleView(holder.mImageView, UNSELECTED_SCALE); 205 | } 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | List getSelection() { 213 | return new LinkedList<>(mSelection); 214 | } 215 | 216 | void setSelection(@NonNull List selection) { 217 | if (!mSelection.equals(selection)) { 218 | mSelection.clear(); 219 | mSelection.addAll(selection); 220 | notifySelectionChanged(); 221 | } 222 | } 223 | 224 | void selectAll() { 225 | if (mData == null) { 226 | return; 227 | } 228 | List selectionToAdd = new LinkedList<>(); 229 | int count = mData.getCount(); 230 | for (int position = 0; position < count; position++) { 231 | if (!isSelected(position)) { 232 | Uri data = getData(position); 233 | selectionToAdd.add(data); 234 | } 235 | } 236 | if (mSelection.size() + selectionToAdd.size() > mMaxSelection) { 237 | if (mCallbacks != null) { 238 | mCallbacks.onWillExceedMaxSelection(); 239 | } 240 | } else { 241 | mSelection.addAll(selectionToAdd); 242 | notifySelectionChanged(); 243 | } 244 | } 245 | 246 | void clearSelection() { 247 | if (!mSelection.isEmpty()) { 248 | mSelection.clear(); 249 | notifySelectionChanged(); 250 | } 251 | } 252 | 253 | private void notifySelectionChanged() { 254 | if (mCallbacks != null) { 255 | mCallbacks.onSelectionUpdated(mSelection.size()); 256 | } 257 | int from = 0, count = getItemCount(); 258 | // If we have LinearLayoutManager we should just rebind the visible items 259 | if (mLayoutManager != null) { 260 | from = mLayoutManager.findFirstVisibleItemPosition(); 261 | count = mLayoutManager.findLastVisibleItemPosition() - from + 1; 262 | } 263 | notifyItemRangeChanged(from, count, SELECTION_PAYLOAD); 264 | } 265 | 266 | private boolean isSelected(int position) { 267 | Uri data = getData(position); 268 | return mSelection.contains(data); 269 | } 270 | 271 | private String getLabel(int position) { 272 | assert mData != null; // It is supposed not be null here 273 | mData.moveToPosition(position); 274 | if (mViewType == VIEW_TYPE_MEDIA) { 275 | return mData.getString(mData.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)); 276 | } else { 277 | return mData.getString(mData.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)); 278 | } 279 | } 280 | 281 | private Uri getData(int position) { 282 | assert mData != null; // It is supposed not be null here 283 | mData.moveToPosition(position); 284 | return Uri.fromFile(new File(mData.getString(mData.getColumnIndex(MediaStore.Images.Media.DATA)))); 285 | } 286 | 287 | private long getBucketId(int position) { 288 | assert mData != null; // It is supposed not be null here 289 | mData.moveToPosition(position); 290 | return mData.getLong(mData.getColumnIndex(MediaStore.Images.Media.BUCKET_ID)); 291 | } 292 | 293 | abstract class ViewHolder extends RecyclerView.ViewHolder { 294 | 295 | final ImageView mImageView; 296 | 297 | private ViewHolder(View itemView) { 298 | super(itemView); 299 | mImageView = itemView.findViewById(R.id.image); 300 | } 301 | } 302 | 303 | private class BucketViewHolder extends ViewHolder implements View.OnClickListener { 304 | 305 | private final TextView mTextView; 306 | 307 | private BucketViewHolder(View itemView) { 308 | super(itemView); 309 | mTextView = itemView.findViewById(R.id.text); 310 | itemView.setOnClickListener(this); 311 | } 312 | 313 | @Override 314 | public void onClick(View v) { 315 | int position = getAdapterPosition(); 316 | // getAdapterPosition() returns RecyclerView.NO_POSITION if item has been removed from the adapter, 317 | // RecyclerView.Adapter.notifyDataSetChanged() has been called after the last layout pass 318 | // or the ViewHolder has already been recycled. 319 | if (position == RecyclerView.NO_POSITION) { 320 | return; 321 | } 322 | 323 | if (mCallbacks != null) { 324 | mCallbacks.onBucketClick(getItemId(), getLabel(position)); 325 | } 326 | } 327 | 328 | } 329 | 330 | class MediaViewHolder extends ViewHolder implements View.OnClickListener { 331 | 332 | final CheckedTextView mCheckView; 333 | 334 | private MediaViewHolder(View itemView) { 335 | super(itemView); 336 | mCheckView = itemView.findViewById(R.id.check); 337 | mCheckView.setOnClickListener(this); 338 | itemView.setOnClickListener(this); 339 | } 340 | 341 | @Override 342 | public void onClick(View v) { 343 | int position = getAdapterPosition(); 344 | // getAdapterPosition() returns RecyclerView.NO_POSITION if item has been removed from the adapter, 345 | // RecyclerView.Adapter.notifyDataSetChanged() has been called after the last layout pass 346 | // or the ViewHolder has already been recycled. 347 | if (position == RecyclerView.NO_POSITION) { 348 | return; 349 | } 350 | 351 | if (v == mCheckView) { 352 | boolean selectionChanged = handleChangeSelection(position); 353 | if (selectionChanged) { 354 | notifyItemChanged(position, SELECTION_PAYLOAD); 355 | } 356 | if (mCallbacks != null) { 357 | if (selectionChanged) { 358 | mCallbacks.onSelectionUpdated(mSelection.size()); 359 | } else { 360 | mCallbacks.onMaxSelectionReached(); 361 | } 362 | } 363 | } else { 364 | if (mCallbacks != null) { 365 | mCallbacks.onMediaClick(mImageView, mCheckView, getBucketId(position), position); 366 | } 367 | } 368 | } 369 | 370 | } 371 | 372 | private boolean handleChangeSelection(int position) { 373 | Uri data = getData(position); 374 | if (!isSelected(position)) { 375 | if (mSelection.size() == mMaxSelection) { 376 | return false; 377 | } 378 | mSelection.add(data); 379 | } else { 380 | mSelection.remove(data); 381 | } 382 | return true; 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/home/GalleryFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.home; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.database.Cursor; 23 | import android.net.Uri; 24 | import android.os.Build; 25 | import android.os.Bundle; 26 | import androidx.annotation.IntRange; 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import androidx.fragment.app.Fragment; 30 | import androidx.fragment.app.FragmentActivity; 31 | import androidx.core.app.SharedElementCallback; 32 | import androidx.recyclerview.widget.GridLayoutManager; 33 | import androidx.recyclerview.widget.RecyclerView; 34 | import android.transition.Transition; 35 | import android.view.LayoutInflater; 36 | import android.view.Menu; 37 | import android.view.MenuInflater; 38 | import android.view.MenuItem; 39 | import android.view.View; 40 | import android.view.ViewGroup; 41 | import android.view.ViewTreeObserver; 42 | 43 | import com.andremion.louvre.R; 44 | import com.andremion.louvre.data.MediaLoader; 45 | import com.andremion.louvre.preview.PreviewActivity; 46 | import com.andremion.louvre.util.ItemOffsetDecoration; 47 | import com.andremion.louvre.util.transition.MediaSharedElementCallback; 48 | import com.andremion.louvre.util.transition.TransitionCallback; 49 | 50 | import java.util.ArrayList; 51 | import java.util.List; 52 | 53 | public class GalleryFragment extends Fragment implements MediaLoader.Callbacks, GalleryAdapter.Callbacks { 54 | 55 | public interface Callbacks { 56 | 57 | void onBucketClick(String label); 58 | 59 | void onMediaClick(@NonNull View imageView, View checkView, long bucketId, int position); 60 | 61 | void onSelectionUpdated(int count); 62 | 63 | void onMaxSelectionReached(); 64 | 65 | void onWillExceedMaxSelection(); 66 | } 67 | 68 | private final MediaLoader mMediaLoader; 69 | private final GalleryAdapter mAdapter; 70 | private View mEmptyView; 71 | private GridLayoutManager mLayoutManager; 72 | private RecyclerView mRecyclerView; 73 | private Callbacks mCallbacks; 74 | private boolean mShouldHandleBackPressed; 75 | 76 | public GalleryFragment() { 77 | mMediaLoader = new MediaLoader(); 78 | mAdapter = new GalleryAdapter(); 79 | mAdapter.setCallbacks(this); 80 | setRetainInstance(true); 81 | setHasOptionsMenu(true); 82 | } 83 | 84 | public void setMediaTypeFilter(@NonNull String[] mediaTypes) { 85 | mMediaLoader.setMediaTypes(mediaTypes); 86 | } 87 | 88 | public void setMaxSelection(@IntRange(from = 0) int maxSelection) { 89 | mAdapter.setMaxSelection(maxSelection); 90 | } 91 | 92 | @Override 93 | public void onAttach(@NonNull Context context) { 94 | super.onAttach(context); 95 | if (!(context instanceof Callbacks)) { 96 | throw new IllegalArgumentException(context.getClass().getSimpleName() + " must implement " + Callbacks.class.getName()); 97 | } 98 | mCallbacks = (Callbacks) context; 99 | if (!(context instanceof FragmentActivity)) { 100 | throw new IllegalArgumentException(context.getClass().getSimpleName() + " must inherit from " + FragmentActivity.class.getName()); 101 | } 102 | mMediaLoader.onAttach((FragmentActivity) context, this); 103 | } 104 | 105 | @Override 106 | public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 107 | inflater.inflate(R.menu.gallery_menu, menu); 108 | } 109 | 110 | @Override 111 | public void onPrepareOptionsMenu(Menu menu) { 112 | boolean isMedia = mAdapter.getItemViewType(0) == GalleryAdapter.VIEW_TYPE_MEDIA; 113 | MenuItem selectAll = menu.findItem(R.id.action_select_all); 114 | selectAll.setVisible(isMedia); 115 | MenuItem clear = menu.findItem(R.id.action_clear); 116 | clear.setVisible(isMedia); 117 | } 118 | 119 | @Override 120 | public boolean onOptionsItemSelected(MenuItem item) { 121 | if (item.getItemId() == R.id.action_select_all) { 122 | mAdapter.selectAll(); 123 | return true; 124 | } 125 | if (item.getItemId() == R.id.action_clear) { 126 | mAdapter.clearSelection(); 127 | return true; 128 | } 129 | return super.onOptionsItemSelected(item); 130 | } 131 | 132 | @Override 133 | public void onBucketLoadFinished(@Nullable Cursor data) { 134 | mAdapter.swapData(GalleryAdapter.VIEW_TYPE_BUCKET, data); 135 | getActivity().invalidateOptionsMenu(); 136 | updateEmptyState(); 137 | } 138 | 139 | @Override 140 | public void onMediaLoadFinished(@Nullable Cursor data) { 141 | mAdapter.swapData(GalleryAdapter.VIEW_TYPE_MEDIA, data); 142 | getActivity().invalidateOptionsMenu(); 143 | updateEmptyState(); 144 | } 145 | 146 | private void updateEmptyState() { 147 | mRecyclerView.setVisibility(mAdapter.getItemCount() > 0 ? View.VISIBLE : View.INVISIBLE); 148 | mEmptyView.setVisibility(mAdapter.getItemCount() > 0 ? View.INVISIBLE : View.VISIBLE); 149 | } 150 | 151 | @Override 152 | public void onDetach() { 153 | super.onDetach(); 154 | mCallbacks = null; 155 | mMediaLoader.onDetach(); 156 | } 157 | 158 | @Nullable 159 | @Override 160 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 161 | View view = inflater.inflate(R.layout.fragment_gallery, container, false); 162 | 163 | mEmptyView = view.findViewById(android.R.id.empty); 164 | 165 | mLayoutManager = new GridLayoutManager(getContext(), 1); 166 | mAdapter.setLayoutManager(mLayoutManager); 167 | 168 | final int spacing = getResources().getDimensionPixelSize(R.dimen.gallery_item_offset); 169 | mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 170 | mRecyclerView.setLayoutManager(mLayoutManager); 171 | mRecyclerView.setAdapter(mAdapter); 172 | mRecyclerView.setClipToPadding(false); 173 | mRecyclerView.addItemDecoration(new ItemOffsetDecoration(spacing)); 174 | mRecyclerView.setHasFixedSize(true); 175 | mRecyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 176 | @Override 177 | public boolean onPreDraw() { 178 | mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this); 179 | int size = getResources().getDimensionPixelSize(R.dimen.gallery_item_size); 180 | int width = mRecyclerView.getMeasuredWidth(); 181 | int columnCount = width / (size + spacing); 182 | mLayoutManager.setSpanCount(columnCount); 183 | return false; 184 | } 185 | }); 186 | 187 | if (savedInstanceState != null) { 188 | updateEmptyState(); 189 | } 190 | 191 | return view; 192 | } 193 | 194 | public void onActivityReenter(int resultCode, Intent data) { 195 | 196 | final int position = PreviewActivity.getPosition(resultCode, data); 197 | if (position != RecyclerView.NO_POSITION) { 198 | mRecyclerView.scrollToPosition(position); 199 | } 200 | 201 | final MediaSharedElementCallback sharedElementCallback = new MediaSharedElementCallback(); 202 | getActivity().setExitSharedElementCallback(sharedElementCallback); 203 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 204 | // Listener to reset shared element exit transition callbacks. 205 | getActivity().getWindow().getSharedElementExitTransition().addListener(new TransitionCallback() { 206 | @Override 207 | public void onTransitionEnd(Transition transition) { 208 | removeCallback(); 209 | } 210 | 211 | @Override 212 | public void onTransitionCancel(Transition transition) { 213 | removeCallback(); 214 | } 215 | 216 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 217 | private void removeCallback() { 218 | if (getActivity() != null) { 219 | getActivity().getWindow().getSharedElementExitTransition().removeListener(this); 220 | getActivity().setExitSharedElementCallback((SharedElementCallback) null); 221 | } 222 | } 223 | }); 224 | } 225 | 226 | //noinspection ConstantConditions 227 | getActivity().supportPostponeEnterTransition(); 228 | mRecyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 229 | @Override 230 | public boolean onPreDraw() { 231 | mRecyclerView.getViewTreeObserver().removeOnPreDrawListener(this); 232 | 233 | RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(position); 234 | if (holder instanceof GalleryAdapter.MediaViewHolder) { 235 | GalleryAdapter.MediaViewHolder mediaViewHolder = (GalleryAdapter.MediaViewHolder) holder; 236 | sharedElementCallback.setSharedElementViews(mediaViewHolder.mImageView, mediaViewHolder.mCheckView); 237 | } 238 | 239 | getActivity().supportStartPostponedEnterTransition(); 240 | 241 | return true; 242 | } 243 | }); 244 | } 245 | 246 | @Override 247 | public void onBucketClick(long bucketId, String label) { 248 | mMediaLoader.loadByBucket(bucketId); 249 | mCallbacks.onBucketClick(label); 250 | mShouldHandleBackPressed = true; 251 | } 252 | 253 | @Override 254 | public void onMediaClick(View imageView, View checkView, long bucketId, int position) { 255 | mCallbacks.onMediaClick(imageView, checkView, bucketId, position); 256 | } 257 | 258 | @Override 259 | public void onSelectionUpdated(int count) { 260 | mCallbacks.onSelectionUpdated(count); 261 | } 262 | 263 | @Override 264 | public void onMaxSelectionReached() { 265 | mCallbacks.onMaxSelectionReached(); 266 | } 267 | 268 | @Override 269 | public void onWillExceedMaxSelection() { 270 | mCallbacks.onWillExceedMaxSelection(); 271 | } 272 | 273 | /** 274 | * Load the initial data if it handles the back pressed 275 | * 276 | * @return If this Fragment handled the back pressed callback 277 | */ 278 | public boolean onBackPressed() { 279 | if (mShouldHandleBackPressed) { 280 | loadBuckets(); 281 | return true; 282 | } 283 | return false; 284 | } 285 | 286 | public void loadBuckets() { 287 | mMediaLoader.loadBuckets(); 288 | mShouldHandleBackPressed = false; 289 | } 290 | 291 | public List getSelection() { 292 | return new ArrayList<>(mAdapter.getSelection()); 293 | } 294 | 295 | public void setSelection(@NonNull List selection) { 296 | mAdapter.setSelection(selection); 297 | } 298 | 299 | } 300 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/preview/PreviewActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.preview; 18 | 19 | import android.annotation.TargetApi; 20 | import android.app.Activity; 21 | import android.content.Intent; 22 | import android.database.Cursor; 23 | import android.net.Uri; 24 | import android.os.Build; 25 | import android.os.Bundle; 26 | import androidx.annotation.IntRange; 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import com.google.android.material.snackbar.Snackbar; 30 | import androidx.core.app.ActivityCompat; 31 | import androidx.core.app.ActivityOptionsCompat; 32 | import androidx.core.util.Pair; 33 | import androidx.core.view.ViewCompat; 34 | import androidx.viewpager.widget.ViewPager; 35 | import androidx.appcompat.app.AppCompatActivity; 36 | import androidx.appcompat.widget.Toolbar; 37 | import android.transition.Transition; 38 | import android.transition.TransitionInflater; 39 | import android.view.MenuItem; 40 | import android.view.View; 41 | import android.widget.CheckedTextView; 42 | 43 | import com.andremion.louvre.R; 44 | import com.andremion.louvre.data.MediaLoader; 45 | import com.andremion.louvre.util.transition.MediaSharedElementCallback; 46 | import com.andremion.louvre.util.transition.TransitionCallback; 47 | 48 | import java.util.ArrayList; 49 | import java.util.Arrays; 50 | import java.util.LinkedList; 51 | import java.util.List; 52 | 53 | import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; 54 | 55 | public class PreviewActivity extends AppCompatActivity implements MediaLoader.Callbacks, PreviewAdapter.Callbacks { 56 | 57 | private static final String EXTRA_BUCKET_ID = PreviewActivity.class.getPackage().getName() + ".extra.BUCKET_ID"; 58 | private static final String EXTRA_POSITION = PreviewActivity.class.getPackage().getName() + ".extra.POSITION"; 59 | private static final String EXTRA_SELECTION = PreviewActivity.class.getPackage().getName() + ".extra.SELECTION"; 60 | private static final String EXTRA_MAX_SELECTION = PreviewActivity.class.getPackage().getName() + ".extra.MAX_SELECTION"; 61 | private static final String EXTRA_MEDIA_TYPE_FILTER = PreviewActivity.class.getPackage().getName() + ".extra.MEDIA_TYPE_FILTER"; 62 | 63 | public static void startActivity(@NonNull Activity activity, int requestCode, @NonNull View imageView, @NonNull View checkView, 64 | @IntRange(from = 0) long bucketId, @IntRange(from = 0) int position, 65 | List selection, int maxSelection, String... mediaTypeFilter) { 66 | 67 | Intent intent = new Intent(activity, PreviewActivity.class); 68 | intent.putExtra(EXTRA_BUCKET_ID, bucketId); 69 | intent.putExtra(EXTRA_POSITION, position); 70 | intent.putExtra(EXTRA_SELECTION, new LinkedList<>(selection)); 71 | intent.putExtra(EXTRA_MAX_SELECTION, maxSelection); 72 | intent.putExtra(EXTRA_MEDIA_TYPE_FILTER, mediaTypeFilter); 73 | 74 | Pair[] sharedElements = concatToSystemSharedElements(activity, 75 | Pair.create(imageView, ViewCompat.getTransitionName(imageView)), 76 | Pair.create(checkView, ViewCompat.getTransitionName(checkView))); 77 | 78 | //noinspection unchecked 79 | ActivityOptionsCompat options = ActivityOptionsCompat 80 | .makeSceneTransitionAnimation(activity, sharedElements); 81 | ActivityCompat.startActivityForResult(activity, intent, requestCode, options.toBundle()); 82 | } 83 | 84 | @SafeVarargs 85 | private static Pair[] concatToSystemSharedElements(@NonNull Activity activity, @NonNull Pair... activitySharedElements) { 86 | 87 | List> sharedElements = new ArrayList<>(); 88 | sharedElements.addAll(Arrays.asList(activitySharedElements)); 89 | 90 | View decorView = activity.getWindow().getDecorView(); 91 | View statusBackground = decorView.findViewById(android.R.id.statusBarBackground); 92 | View navigationBarBackground = decorView.findViewById(android.R.id.navigationBarBackground); 93 | 94 | if (statusBackground != null) { 95 | sharedElements.add(Pair.create(statusBackground, ViewCompat.getTransitionName(statusBackground))); 96 | } 97 | if (navigationBarBackground != null) { 98 | sharedElements.add(Pair.create(navigationBarBackground, ViewCompat.getTransitionName(navigationBarBackground))); 99 | } 100 | 101 | Pair[] result = new Pair[sharedElements.size()]; 102 | sharedElements.toArray(result); 103 | return result; 104 | } 105 | 106 | public static int getPosition(int resultCode, Intent data) { 107 | if (resultCode == RESULT_OK && data != null && data.hasExtra(EXTRA_POSITION)) { 108 | return data.getIntExtra(EXTRA_POSITION, NO_POSITION); 109 | } 110 | return NO_POSITION; 111 | } 112 | 113 | public static List getSelection(Intent data) { 114 | //noinspection unchecked 115 | return (List) data.getExtras().get(EXTRA_SELECTION); 116 | } 117 | 118 | private MediaLoader mMediaLoader; 119 | private PreviewAdapter mAdapter; 120 | private ViewPager mViewPager; 121 | private CheckedTextView mCheckbox; 122 | 123 | @Override 124 | protected void onCreate(Bundle savedInstanceState) { 125 | super.onCreate(savedInstanceState); 126 | setContentView(R.layout.activity_preview); 127 | setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); 128 | //noinspection ConstantConditions 129 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 130 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 131 | setupTransition(); 132 | } 133 | 134 | setTitle(null); 135 | 136 | // Postpone transition until the image of ViewPager's initial item is loaded 137 | supportPostponeEnterTransition(); 138 | 139 | MediaSharedElementCallback sharedElementCallback = new MediaSharedElementCallback(); 140 | setEnterSharedElementCallback(sharedElementCallback); 141 | 142 | //noinspection unchecked 143 | List selection = (List) getIntent().getExtras().get(EXTRA_SELECTION); 144 | assert selection != null; 145 | int maxSelection = getIntent().getExtras().getInt(EXTRA_MAX_SELECTION); 146 | 147 | mCheckbox = (CheckedTextView) findViewById(R.id.check); 148 | mCheckbox.setOnClickListener(new View.OnClickListener() { 149 | @Override 150 | public void onClick(View v) { 151 | mAdapter.selectCurrentItem(); 152 | } 153 | }); 154 | 155 | mAdapter = new PreviewAdapter(this, mCheckbox, sharedElementCallback, selection); 156 | mAdapter.setCallbacks(this); 157 | mAdapter.setMaxSelection(maxSelection); 158 | 159 | mViewPager = (ViewPager) findViewById(R.id.view_pager); 160 | mViewPager.setAdapter(mAdapter); 161 | 162 | mMediaLoader = new MediaLoader(); 163 | mMediaLoader.onAttach(this, this); 164 | if (getIntent().hasExtra(EXTRA_MEDIA_TYPE_FILTER)) { 165 | mMediaLoader.setMediaTypes(getIntent().getStringArrayExtra(EXTRA_MEDIA_TYPE_FILTER)); 166 | } 167 | 168 | long bucketId = getIntent().getExtras().getLong(EXTRA_BUCKET_ID); 169 | mMediaLoader.loadByBucket(bucketId); 170 | } 171 | 172 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 173 | private void setupTransition() { 174 | TransitionInflater inflater = TransitionInflater.from(this); 175 | Transition sharedElementEnterTransition = inflater.inflateTransition(R.transition.shared_element); 176 | sharedElementEnterTransition.addListener(new TransitionCallback() { 177 | @Override 178 | public void onTransitionEnd(Transition transition) { 179 | mAdapter.setDontAnimate(false); 180 | } 181 | 182 | @Override 183 | public void onTransitionCancel(Transition transition) { 184 | mAdapter.setDontAnimate(false); 185 | } 186 | }); 187 | getWindow().setSharedElementEnterTransition(sharedElementEnterTransition); 188 | } 189 | 190 | @Override 191 | public boolean onOptionsItemSelected(MenuItem item) { 192 | if (item.getItemId() == android.R.id.home) { 193 | onBackPressed(); 194 | return true; 195 | } 196 | return super.onOptionsItemSelected(item); 197 | } 198 | 199 | @Nullable 200 | @Override 201 | public Intent getSupportParentActivityIntent() { 202 | //noinspection ConstantConditions 203 | return super.getSupportParentActivityIntent().addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 204 | } 205 | 206 | @Override 207 | public void onBucketLoadFinished(@Nullable Cursor data) { 208 | swapData(data); 209 | } 210 | 211 | @Override 212 | public void onMediaLoadFinished(@Nullable Cursor data) { 213 | swapData(data); 214 | } 215 | 216 | @Override 217 | public void onCheckedUpdated(boolean checked) { 218 | mCheckbox.setChecked(checked); 219 | } 220 | 221 | @Override 222 | public void onMaxSelectionReached() { 223 | Snackbar.make(mViewPager, R.string.activity_gallery_max_selection_reached, Snackbar.LENGTH_SHORT).show(); 224 | } 225 | 226 | @Override 227 | public void finish() { 228 | setResult(); 229 | super.finish(); 230 | } 231 | 232 | @Override 233 | public void finishAfterTransition() { 234 | setResult(); 235 | super.finishAfterTransition(); 236 | } 237 | 238 | @Override 239 | protected void onDestroy() { 240 | super.onDestroy(); 241 | mMediaLoader.onDetach(); 242 | } 243 | 244 | private void swapData(@Nullable Cursor data) { 245 | int position = getIntent().getExtras().getInt(EXTRA_POSITION); 246 | 247 | mAdapter.swapData(data); 248 | mAdapter.setInitialPosition(position); 249 | mViewPager.setCurrentItem(position, false); 250 | 251 | setCheckboxTransitionName(position); 252 | } 253 | 254 | private void setResult() { 255 | int position = mViewPager.getCurrentItem(); 256 | 257 | Intent data = new Intent(); 258 | data.putExtra(EXTRA_POSITION, position); 259 | data.putExtra(EXTRA_SELECTION, new LinkedList<>(mAdapter.getSelection())); 260 | setResult(RESULT_OK, data); 261 | 262 | setCheckboxTransitionName(position); 263 | } 264 | 265 | private void setCheckboxTransitionName(int position) { 266 | Uri uri = mAdapter.getData(position); 267 | if (uri != null) { 268 | String checkboxTransitionName = getString(R.string.activity_gallery_checkbox_transition, uri.toString()); 269 | ViewCompat.setTransitionName(mCheckbox, checkboxTransitionName); 270 | } 271 | } 272 | 273 | } 274 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/preview/PreviewAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.preview; 18 | 19 | import android.database.Cursor; 20 | import android.graphics.drawable.Drawable; 21 | import android.net.Uri; 22 | import android.provider.MediaStore; 23 | import android.view.LayoutInflater; 24 | import android.view.View; 25 | import android.view.ViewGroup; 26 | import android.widget.CheckedTextView; 27 | import android.widget.ImageView; 28 | 29 | import androidx.annotation.IntRange; 30 | import androidx.annotation.NonNull; 31 | import androidx.annotation.Nullable; 32 | import androidx.core.view.ViewCompat; 33 | import androidx.fragment.app.FragmentActivity; 34 | import androidx.recyclerview.widget.RecyclerView; 35 | import androidx.viewpager.widget.PagerAdapter; 36 | 37 | import com.andremion.louvre.R; 38 | import com.andremion.louvre.util.transition.MediaSharedElementCallback; 39 | import com.bumptech.glide.Glide; 40 | import com.bumptech.glide.load.DataSource; 41 | import com.bumptech.glide.load.engine.GlideException; 42 | import com.bumptech.glide.request.RequestListener; 43 | import com.bumptech.glide.request.RequestOptions; 44 | import com.bumptech.glide.request.target.Target; 45 | 46 | import java.io.File; 47 | import java.util.LinkedList; 48 | import java.util.List; 49 | 50 | import static android.view.View.NO_ID; 51 | 52 | class PreviewAdapter extends PagerAdapter { 53 | 54 | interface Callbacks { 55 | 56 | void onCheckedUpdated(boolean checked); 57 | 58 | void onMaxSelectionReached(); 59 | } 60 | 61 | private final FragmentActivity mActivity; 62 | private final LayoutInflater mInflater; 63 | private final CheckedTextView mCheckbox; 64 | private final MediaSharedElementCallback mSharedElementCallback; 65 | private final List mSelection; 66 | @Nullable 67 | private PreviewAdapter.Callbacks mCallbacks; 68 | private int mMaxSelection; 69 | private int mInitialPosition; 70 | @Nullable 71 | private Cursor mData; 72 | private boolean mDontAnimate; 73 | private int mCurrentPosition = RecyclerView.NO_POSITION; 74 | 75 | PreviewAdapter(@NonNull FragmentActivity activity, @NonNull CheckedTextView checkbox, @NonNull MediaSharedElementCallback sharedElementCallback, @NonNull List selection) { 76 | mActivity = activity; 77 | mInflater = LayoutInflater.from(activity); 78 | mCheckbox = checkbox; 79 | mSharedElementCallback = sharedElementCallback; 80 | mSelection = selection; 81 | mDontAnimate = true; 82 | } 83 | 84 | void setCallbacks(@Nullable PreviewAdapter.Callbacks callbacks) { 85 | mCallbacks = callbacks; 86 | } 87 | 88 | void setMaxSelection(@IntRange(from = 0) int maxSelection) { 89 | mMaxSelection = maxSelection; 90 | } 91 | 92 | void setInitialPosition(int position) { 93 | mInitialPosition = position; 94 | } 95 | 96 | void swapData(Cursor data) { 97 | if (data != mData) { 98 | mData = data; 99 | notifyDataSetChanged(); 100 | } 101 | } 102 | 103 | void setDontAnimate(boolean dontAnimate) { 104 | mDontAnimate = dontAnimate; 105 | } 106 | 107 | @Override 108 | public int getCount() { 109 | if (mData != null && !mData.isClosed()) { 110 | return mData.getCount(); 111 | } 112 | return 0; 113 | } 114 | 115 | @Override 116 | public Object instantiateItem(@NonNull ViewGroup container, int position) { 117 | View view = mInflater.inflate(R.layout.page_item_preview, container, false); 118 | ViewHolder holder = new ViewHolder(view); 119 | Uri data = getData(position); 120 | onViewBound(holder, position, data); 121 | container.addView(holder.itemView); 122 | return holder; 123 | } 124 | 125 | @Nullable 126 | Uri getData(int position) { 127 | if (mData != null && !mData.isClosed()) { 128 | mData.moveToPosition(position); 129 | return Uri.fromFile(new File(mData.getString(mData.getColumnIndex(MediaStore.Images.Media.DATA)))); 130 | } 131 | return null; 132 | } 133 | 134 | private long getItemId(int position) { 135 | if (mData != null && !mData.isClosed()) { 136 | mData.moveToPosition(position); 137 | return mData.getLong(mData.getColumnIndex(MediaStore.Images.Media._ID)); 138 | } 139 | return NO_ID; 140 | } 141 | 142 | private void onViewBound(ViewHolder holder, int position, Uri data) { 143 | String imageTransitionName = holder.imageView.getContext().getString(R.string.activity_gallery_image_transition, data.toString()); 144 | ViewCompat.setTransitionName(holder.imageView, imageTransitionName); 145 | 146 | RequestOptions options = new RequestOptions() 147 | .skipMemoryCache(true) 148 | .fitCenter(); 149 | if (mDontAnimate) { 150 | options.dontAnimate(); 151 | } 152 | Glide.with(mActivity) 153 | .load(data) 154 | .apply(options) 155 | .listener(new ImageLoadingCallback(position)) 156 | .into(holder.imageView); 157 | } 158 | 159 | private boolean isSelected(int position) { 160 | Uri data = getData(position); 161 | return mSelection.contains(data); 162 | } 163 | 164 | private void startPostponedEnterTransition(int position) { 165 | if (position == mInitialPosition) { 166 | mActivity.supportStartPostponedEnterTransition(); 167 | } 168 | } 169 | 170 | @Override 171 | public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { 172 | if (object instanceof ViewHolder) { 173 | mCurrentPosition = position; 174 | mSharedElementCallback.setSharedElementViews(((ViewHolder) object).imageView, mCheckbox); 175 | if (mCallbacks != null) { 176 | mCallbacks.onCheckedUpdated(isSelected(position)); 177 | } 178 | } 179 | } 180 | 181 | @Override 182 | public boolean isViewFromObject(@NonNull View view, @NonNull Object object) { 183 | return object instanceof ViewHolder 184 | && view.equals(((ViewHolder) object).itemView); 185 | } 186 | 187 | @Override 188 | public void destroyItem(ViewGroup container, int position, Object object) { 189 | View view = ((ViewHolder) object).itemView; 190 | container.removeView(view); 191 | } 192 | 193 | void selectCurrentItem() { 194 | boolean selectionChanged = handleChangeSelection(mCurrentPosition); 195 | if (selectionChanged) { 196 | notifyDataSetChanged(); 197 | } 198 | if (mCallbacks != null) { 199 | if (selectionChanged) { 200 | mCallbacks.onCheckedUpdated(isSelected(mCurrentPosition)); 201 | } else { 202 | mCallbacks.onMaxSelectionReached(); 203 | } 204 | } 205 | } 206 | 207 | List getSelection() { 208 | return new LinkedList<>(mSelection); 209 | } 210 | 211 | private boolean handleChangeSelection(int position) { 212 | Uri data = getData(position); 213 | if (!isSelected(position)) { 214 | if (mSelection.size() == mMaxSelection) { 215 | return false; 216 | } 217 | mSelection.add(data); 218 | } else { 219 | mSelection.remove(data); 220 | } 221 | return true; 222 | } 223 | 224 | private static class ViewHolder { 225 | 226 | final View itemView; 227 | final ImageView imageView; 228 | 229 | ViewHolder(View view) { 230 | itemView = view; 231 | imageView = view.findViewById(R.id.image); 232 | } 233 | 234 | } 235 | 236 | private class ImageLoadingCallback implements RequestListener { 237 | 238 | final int mPosition; 239 | 240 | ImageLoadingCallback(int position) { 241 | mPosition = position; 242 | } 243 | 244 | @Override 245 | public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { 246 | startPostponedEnterTransition(mPosition); 247 | return false; 248 | } 249 | 250 | @Override 251 | public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { 252 | startPostponedEnterTransition(mPosition); 253 | return false; 254 | } 255 | } 256 | 257 | } 258 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/AnimationHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.util; 18 | 19 | import androidx.annotation.FloatRange; 20 | import androidx.annotation.NonNull; 21 | import android.view.View; 22 | 23 | public class AnimationHelper { 24 | 25 | public static void scaleView(@NonNull View view, @FloatRange(from = 0, to = 1) float scale) { 26 | if (view.getScaleX() != scale || view.getScaleY() != scale) { 27 | long duration = view.getContext().getResources().getInteger(android.R.integer.config_shortAnimTime); 28 | view.animate().scaleX(scale).scaleY(scale).setDuration(duration).start(); 29 | } 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/FabBehavior.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.util; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | 23 | import androidx.annotation.NonNull; 24 | import androidx.coordinatorlayout.widget.CoordinatorLayout; 25 | import androidx.core.view.ViewCompat; 26 | import androidx.recyclerview.widget.RecyclerView; 27 | 28 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 29 | 30 | /** 31 | * Custom {@link FloatingActionButton.Behavior} to hide {@link FloatingActionButton} when user is scrolling 32 | */ 33 | public class FabBehavior extends FloatingActionButton.Behavior { 34 | 35 | public FabBehavior() { 36 | } 37 | 38 | public FabBehavior(Context context, AttributeSet attrs) { 39 | super(context, attrs); 40 | } 41 | 42 | @Override 43 | public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, 44 | @NonNull View directTargetChild, @NonNull View target, int axes, int type) { 45 | return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type) 46 | || (type == ViewCompat.TYPE_TOUCH && (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 47 | && target instanceof RecyclerView); 48 | } 49 | 50 | @Override 51 | public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, 52 | @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, 53 | int type, @NonNull int[] consumed) { 54 | super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed); 55 | if (type == ViewCompat.TYPE_TOUCH && Math.abs(dyConsumed) > 0 && child.getVisibility() == View.VISIBLE) { 56 | child.hide(); 57 | } 58 | } 59 | 60 | @Override 61 | public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull FloatingActionButton child, 62 | @NonNull View target, int type) { 63 | super.onStopNestedScroll(coordinatorLayout, child, target, type); 64 | if (type == ViewCompat.TYPE_TOUCH) child.show(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/HackyViewPager.java: -------------------------------------------------------------------------------- 1 | package com.andremion.louvre.util; 2 | 3 | import android.content.Context; 4 | import androidx.viewpager.widget.ViewPager; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | 8 | /** 9 | * Hacky fix for Issue #4 and 10 | * http://code.google.com/p/android/issues/detail?id=18990 11 | *

12 | * ScaleGestureDetector seems to mess up the touch events, which means that 13 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of 14 | * IllegalArgumentException: pointerIndex out of range. 15 | *

16 | * There's not much I can do in my code for now, but we can mask the result by 17 | * just catching the problem and ignoring it. 18 | * 19 | * @author Chris Banes 20 | */ 21 | public class HackyViewPager extends ViewPager { 22 | 23 | public HackyViewPager(Context context) { 24 | super(context); 25 | } 26 | 27 | public HackyViewPager(Context context, AttributeSet attrs) { 28 | super(context, attrs); 29 | } 30 | 31 | @Override 32 | public boolean onInterceptTouchEvent(MotionEvent ev) { 33 | try { 34 | return super.onInterceptTouchEvent(ev); 35 | } catch (IllegalArgumentException e) { 36 | return false; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/ItemOffsetDecoration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.util; 18 | 19 | import android.graphics.Rect; 20 | import androidx.recyclerview.widget.RecyclerView; 21 | import android.view.View; 22 | 23 | public class ItemOffsetDecoration extends RecyclerView.ItemDecoration { 24 | 25 | private final int mItemOffset; 26 | 27 | public ItemOffsetDecoration(int itemOffset) { 28 | mItemOffset = itemOffset; 29 | } 30 | 31 | @Override 32 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 33 | outRect.set(mItemOffset, mItemOffset, mItemOffset, mItemOffset); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/transition/MediaSharedElementCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.util.transition; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.core.app.SharedElementCallback; 21 | import androidx.core.view.ViewCompat; 22 | import android.view.View; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.List; 27 | import java.util.Map; 28 | 29 | /** 30 | * Some hacks pulled from https://github.com/googlesamples/android-unsplash 31 | */ 32 | public class MediaSharedElementCallback extends SharedElementCallback { 33 | 34 | private final List mSharedElementViews; 35 | 36 | public MediaSharedElementCallback() { 37 | mSharedElementViews = new ArrayList<>(); 38 | } 39 | 40 | public void setSharedElementViews(@NonNull View... sharedElementViews) { 41 | mSharedElementViews.clear(); 42 | mSharedElementViews.addAll(Arrays.asList(sharedElementViews)); 43 | } 44 | 45 | @Override 46 | public void onMapSharedElements(List names, Map sharedElements) { 47 | if (!mSharedElementViews.isEmpty()) { 48 | removeObsoleteElements(names, sharedElements, mapObsoleteElements(names)); 49 | for (View sharedElementView : mSharedElementViews) { 50 | String transitionName = ViewCompat.getTransitionName(sharedElementView); 51 | names.add(transitionName); 52 | sharedElements.put(transitionName, sharedElementView); 53 | } 54 | } 55 | } 56 | 57 | @Override 58 | public void onSharedElementEnd(List sharedElementNames, 59 | List sharedElements, 60 | List sharedElementSnapshots) { 61 | for (View sharedElementView : mSharedElementViews) { 62 | forceSharedElementLayout(sharedElementView); 63 | } 64 | } 65 | 66 | /** 67 | * Maps all views that don't start with "android" namespace. 68 | * 69 | * @param names All shared element names. 70 | * @return The obsolete shared element names. 71 | */ 72 | @NonNull 73 | private List mapObsoleteElements(List names) { 74 | List elementsToRemove = new ArrayList<>(names.size()); 75 | for (String name : names) { 76 | if (name.startsWith("android")) continue; 77 | elementsToRemove.add(name); 78 | } 79 | return elementsToRemove; 80 | } 81 | 82 | /** 83 | * Removes obsolete elements from names and shared elements. 84 | * 85 | * @param names Shared element names. 86 | * @param sharedElements Shared elements. 87 | * @param elementsToRemove The elements that should be removed. 88 | */ 89 | private void removeObsoleteElements(List names, 90 | Map sharedElements, 91 | List elementsToRemove) { 92 | if (elementsToRemove.size() > 0) { 93 | names.removeAll(elementsToRemove); 94 | for (String elementToRemove : elementsToRemove) { 95 | sharedElements.remove(elementToRemove); 96 | } 97 | } 98 | } 99 | 100 | private void forceSharedElementLayout(View view) { 101 | int widthSpec = View.MeasureSpec.makeMeasureSpec(view.getWidth(), 102 | View.MeasureSpec.EXACTLY); 103 | int heightSpec = View.MeasureSpec.makeMeasureSpec(view.getHeight(), 104 | View.MeasureSpec.EXACTLY); 105 | view.measure(widthSpec, heightSpec); 106 | view.layout(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /louvre/src/main/java/com/andremion/louvre/util/transition/TransitionCallback.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.util.transition; 18 | 19 | import android.transition.Transition; 20 | 21 | public abstract class TransitionCallback implements Transition.TransitionListener { 22 | 23 | @Override 24 | public void onTransitionStart(Transition transition) { 25 | // no-op 26 | } 27 | 28 | @Override 29 | public void onTransitionEnd(Transition transition) { 30 | // no-op 31 | } 32 | 33 | @Override 34 | public void onTransitionCancel(Transition transition) { 35 | // no-op 36 | } 37 | 38 | @Override 39 | public void onTransitionPause(Transition transition) { 40 | // no-op 41 | } 42 | 43 | @Override 44 | public void onTransitionResume(Transition transition) { 45 | // no-op 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_checked_mtrl_animation_interpolator_0.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_checked_mtrl_animation_interpolator_1.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_checked_box_inner_merged_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 22 | 23 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_checked_box_outer_merged_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 23 | 30 | 31 | 32 | 38 | 44 | 45 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_checked_icon_null_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 36 | 42 | 43 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_unchecked_box_inner_merged_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 23 | 30 | 31 | 32 | 38 | 44 | 45 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_unchecked_check_path_merged_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 22 | 23 | 29 | 35 | 36 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_to_unchecked_icon_null_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 22 | 28 | 29 | 30 | 36 | 42 | 43 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_unchecked_mtrl_animation_interpolator_0.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /louvre/src/main/res/anim/btn_checkbox_unchecked_mtrl_animation_interpolator_1.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-hdpi/ic_clear_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-hdpi/ic_clear_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-hdpi/ic_no_images_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-hdpi/ic_no_images_black_48dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-mdpi/ic_clear_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-mdpi/ic_clear_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-mdpi/ic_no_images_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-mdpi/ic_no_images_black_48dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-v21/btn_check_material_anim.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 19 | 22 | 26 | 30 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-v21/btn_checkbox_checked_to_unchecked_mtrl_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 16 | 19 | 22 | 25 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-v21/btn_checkbox_unchecked_to_checked_mtrl_animation.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 16 | 19 | 22 | 25 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xhdpi/ic_clear_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xhdpi/ic_clear_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xhdpi/ic_no_images_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xhdpi/ic_no_images_black_48dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxhdpi/ic_clear_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxhdpi/ic_clear_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxhdpi/ic_no_images_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxhdpi/ic_no_images_black_48dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxxhdpi/ic_clear_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxxhdpi/ic_clear_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxxhdpi/ic_no_images_black_48dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxxhdpi/ic_no_images_black_48dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/louvre/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/btn_check_material_anim.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/btn_checkbox_checked_mtrl.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | 27 | 31 | 35 | 36 | 40 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/btn_checkbox_unchecked_mtrl.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 21 | 27 | 31 | 36 | 37 | 41 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/ic_done_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 25 | 26 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/ic_no_images.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /louvre/src/main/res/drawable/ic_select_all.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/activity_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 25 | 26 | 30 | 31 | 32 | 33 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/activity_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 15 | 16 | 21 | 22 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/checkbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/fragment_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | 16 | 26 | 27 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/list_item_gallery_bucket.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 33 | 34 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/list_item_gallery_media.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /louvre/src/main/res/layout/page_item_preview.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /louvre/src/main/res/menu/gallery_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

4 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /louvre/src/main/res/transition-v21/gallery_exit.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /louvre/src/main/res/transition-v21/gallery_reenter.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /louvre/src/main/res/transition-v21/shared_element.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /louvre/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Permiso de almacenamiento necesario. Por favor habilitar en los ajustes del teléfono. 4 | Ajustes 5 | Seleccionar todo 6 | Limpiar 7 | No hay fotos. 8 | Has alcanzado el número máximo de fotos. 9 | Usted excederá el número máximo de fotos. 10 | Todas las fotos 11 | 12 | %d seleccionado 13 | %d seleccionados 14 | 15 | 16 | -------------------------------------------------------------------------------- /louvre/src/main/res/values-v19/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /louvre/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /louvre/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #66000000 4 | #ddd 5 | -------------------------------------------------------------------------------- /louvre/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 128dp 5 | 1dp 6 | 16dp 7 | 8dp 8 | -------------------------------------------------------------------------------- /louvre/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | %s[image] 4 | %s[checkbox] 5 | To fetch your Media, allow storage permission in App Permission Settings. 6 | Settings 7 | Select All 8 | Clear 9 | No images. 10 | You have reached the max number of photos. 11 | You will exceed the max number of photos. 12 | All Media 13 | 14 | %d selected 15 | %d selected 16 | 17 | 18 | -------------------------------------------------------------------------------- /louvre/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 15 | 16 | 19 | 20 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion project.ext.compileSdkVersion 6 | defaultConfig { 7 | applicationId "com.andremion.louvre.sample" 8 | minSdkVersion project.ext.minSdkVersion 9 | targetSdkVersion project.ext.targetSdkVersion 10 | versionCode project.ext.versionCode 11 | versionName project.ext.versionName 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | vectorDrawables.useSupportLibrary = true 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | } 21 | } 22 | 23 | dependencies { 24 | implementation fileTree(dir: 'libs', include: ['*.jar']) 25 | implementation project(':louvre') 26 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" 27 | implementation "com.google.android.material:material:$materialVersion" 28 | implementation "com.github.bumptech.glide:glide:$glideVersion" 29 | 30 | testImplementation "junit:junit:$junitVersion" 31 | } 32 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 26 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /sample/src/main/java/com/andremion/louvre/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.sample; 18 | 19 | import android.content.Intent; 20 | import android.net.Uri; 21 | import android.os.Bundle; 22 | import com.google.android.material.floatingactionbutton.FloatingActionButton; 23 | import androidx.appcompat.app.AppCompatActivity; 24 | import androidx.recyclerview.widget.GridLayoutManager; 25 | import androidx.recyclerview.widget.RecyclerView; 26 | import androidx.appcompat.widget.Toolbar; 27 | import android.view.View; 28 | import android.view.ViewTreeObserver; 29 | 30 | import com.andremion.louvre.home.GalleryActivity; 31 | import com.andremion.louvre.util.ItemOffsetDecoration; 32 | 33 | import java.util.List; 34 | 35 | public class MainActivity extends AppCompatActivity { 36 | 37 | private static final int LOUVRE_REQUEST_CODE = 0; 38 | 39 | private MainAdapter mAdapter; 40 | private List mSelection; 41 | 42 | @Override 43 | protected void onCreate(Bundle savedInstanceState) { 44 | super.onCreate(savedInstanceState); 45 | setContentView(R.layout.activity_main); 46 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 47 | setSupportActionBar(toolbar); 48 | 49 | final int spacing = getResources().getDimensionPixelSize(R.dimen.gallery_item_offset); 50 | final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recycler_view); 51 | recyclerView.addItemDecoration(new ItemOffsetDecoration(spacing)); 52 | recyclerView.setHasFixedSize(true); 53 | recyclerView.setAdapter(mAdapter = new MainAdapter()); 54 | recyclerView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 55 | @Override 56 | public boolean onPreDraw() { 57 | recyclerView.getViewTreeObserver().removeOnPreDrawListener(this); 58 | int size = getResources().getDimensionPixelSize(R.dimen.gallery_item_size); 59 | int width = recyclerView.getMeasuredWidth(); 60 | int columnCount = width / (size + spacing); 61 | recyclerView.setLayoutManager(new GridLayoutManager(MainActivity.this, columnCount)); 62 | return false; 63 | } 64 | }); 65 | 66 | FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 67 | fab.setOnClickListener(new View.OnClickListener() { 68 | @Override 69 | public void onClick(View view) { 70 | NumberPickerDialog.show(getSupportFragmentManager(), LOUVRE_REQUEST_CODE, mSelection); 71 | } 72 | }); 73 | } 74 | 75 | @Override 76 | protected void onRestoreInstanceState(Bundle savedInstanceState) { 77 | super.onRestoreInstanceState(savedInstanceState); 78 | //noinspection unchecked 79 | mSelection = (List) getLastCustomNonConfigurationInstance(); 80 | } 81 | 82 | @Override 83 | public Object onRetainCustomNonConfigurationInstance() { 84 | return mSelection; 85 | } 86 | 87 | @Override 88 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 89 | if (requestCode == LOUVRE_REQUEST_CODE && resultCode == RESULT_OK) { 90 | mAdapter.swapData(mSelection = GalleryActivity.getSelection(data)); 91 | return; 92 | } 93 | super.onActivityResult(requestCode, resultCode, data); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /sample/src/main/java/com/andremion/louvre/sample/MainAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.sample; 18 | 19 | import android.net.Uri; 20 | import androidx.annotation.Nullable; 21 | import androidx.recyclerview.widget.RecyclerView; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.ImageView; 26 | 27 | import com.bumptech.glide.Glide; 28 | import com.bumptech.glide.request.RequestOptions; 29 | 30 | import java.util.ArrayList; 31 | import java.util.List; 32 | 33 | class MainAdapter extends RecyclerView.Adapter { 34 | 35 | private final List mData; 36 | 37 | MainAdapter() { 38 | mData = new ArrayList<>(); 39 | } 40 | 41 | @Override 42 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 43 | View view = LayoutInflater.from(parent.getContext()) 44 | .inflate(R.layout.list_item_main, parent, false); 45 | return new ViewHolder(view); 46 | } 47 | 48 | @Override 49 | public void onBindViewHolder(ViewHolder holder, int position) { 50 | Uri data = mData.get(position); 51 | Glide.with(holder.mImageView.getContext()) 52 | .load(data) 53 | .apply(RequestOptions.skipMemoryCacheOf(true) 54 | .centerCrop() 55 | .placeholder(com.andremion.louvre.R.color.gallery_item_background)) 56 | .into(holder.mImageView); 57 | } 58 | 59 | @Override 60 | public int getItemCount() { 61 | return mData.size(); 62 | } 63 | 64 | void swapData(@Nullable List data) { 65 | if (!mData.equals(data)) { 66 | mData.clear(); 67 | if (data != null) { 68 | mData.addAll(data); 69 | } 70 | notifyDataSetChanged(); 71 | } 72 | } 73 | 74 | class ViewHolder extends RecyclerView.ViewHolder { 75 | 76 | private final ImageView mImageView; 77 | 78 | private ViewHolder(View itemView) { 79 | super(itemView); 80 | mImageView = (ImageView) itemView; 81 | } 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /sample/src/main/java/com/andremion/louvre/sample/MediaTypeFilterDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.sample; 18 | 19 | import android.app.Dialog; 20 | import android.content.DialogInterface; 21 | import android.net.Uri; 22 | import android.os.Bundle; 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.Nullable; 25 | import androidx.fragment.app.DialogFragment; 26 | import androidx.fragment.app.FragmentManager; 27 | import androidx.appcompat.app.AlertDialog; 28 | import android.util.SparseBooleanArray; 29 | 30 | import com.andremion.louvre.Louvre; 31 | 32 | import java.util.ArrayList; 33 | import java.util.LinkedList; 34 | import java.util.List; 35 | 36 | import static android.content.ContentValues.TAG; 37 | 38 | public class MediaTypeFilterDialog extends DialogFragment implements DialogInterface.OnMultiChoiceClickListener, DialogInterface.OnClickListener { 39 | 40 | private static final String ARG_REQUEST_CODE = "request_code_arg"; 41 | private static final String ARG_MAX_SELECTION = "max_selection_arg"; 42 | private static final String ARG_SELECTION = "selection_arg"; 43 | 44 | public static void show(@NonNull FragmentManager fragmentManager, int requestCode, int maxSelection, @Nullable List selection) { 45 | MediaTypeFilterDialog dialog = new MediaTypeFilterDialog(); 46 | Bundle args = new Bundle(); 47 | args.putInt(ARG_REQUEST_CODE, requestCode); 48 | args.putInt(ARG_MAX_SELECTION, maxSelection); 49 | if (selection != null) { 50 | args.putSerializable(ARG_SELECTION, new LinkedList<>(selection)); 51 | } 52 | dialog.setArguments(args); 53 | dialog.show(fragmentManager, TAG); 54 | } 55 | 56 | private final SparseBooleanArray mSelectedTypes; 57 | 58 | public MediaTypeFilterDialog() { 59 | mSelectedTypes = new SparseBooleanArray(); 60 | } 61 | 62 | @NonNull 63 | @Override 64 | public Dialog onCreateDialog(Bundle savedInstanceState) { 65 | return new AlertDialog.Builder(getContext()) 66 | .setTitle(R.string.media_type_prompt) 67 | .setMultiChoiceItems(Louvre.IMAGE_TYPES, null, this) 68 | .setPositiveButton(android.R.string.ok, this) 69 | .setNegativeButton(android.R.string.cancel, null) 70 | .show(); 71 | } 72 | 73 | @Override 74 | public void onClick(DialogInterface dialog, int which, boolean isChecked) { 75 | if (isChecked) { 76 | mSelectedTypes.put(which, true); 77 | } else { 78 | mSelectedTypes.put(which, false); 79 | } 80 | } 81 | 82 | @Override 83 | public void onClick(DialogInterface dialog, int which) { 84 | //noinspection WrongConstant,ConstantConditions,unchecked 85 | Louvre.init(getActivity()) 86 | .setRequestCode(getArguments().getInt(ARG_REQUEST_CODE)) 87 | .setMaxSelection(getArguments().getInt(ARG_MAX_SELECTION)) 88 | .setSelection((List) getArguments().get(ARG_SELECTION)) 89 | .setMediaTypeFilter(parseToArray(mSelectedTypes)) 90 | .open(); 91 | } 92 | 93 | @NonNull 94 | private String[] parseToArray(@NonNull SparseBooleanArray selectedTypes) { 95 | List selectedTypeList = new ArrayList<>(); 96 | for (int i = 0; i < selectedTypes.size(); i++) { 97 | int key = selectedTypes.keyAt(i); 98 | if (selectedTypes.get(key, false)) { 99 | selectedTypeList.add(Louvre.IMAGE_TYPES[key]); 100 | } 101 | } 102 | String[] array = new String[selectedTypeList.size()]; 103 | selectedTypeList.toArray(array); 104 | return array; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /sample/src/main/java/com/andremion/louvre/sample/NumberPickerDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020. André Mion 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.andremion.louvre.sample; 18 | 19 | import android.app.Dialog; 20 | import android.content.DialogInterface; 21 | import android.net.Uri; 22 | import android.os.Bundle; 23 | import androidx.annotation.NonNull; 24 | import androidx.annotation.Nullable; 25 | import androidx.fragment.app.DialogFragment; 26 | import androidx.fragment.app.FragmentManager; 27 | import androidx.appcompat.app.AlertDialog; 28 | import android.widget.NumberPicker; 29 | 30 | import java.util.LinkedList; 31 | import java.util.List; 32 | 33 | public class NumberPickerDialog extends DialogFragment implements DialogInterface.OnClickListener { 34 | 35 | private static final String TAG = NumberPickerDialog.class.getSimpleName(); 36 | private static final String ARG_REQUEST_CODE = "request_code_arg"; 37 | private static final String ARG_SELECTION = "selection_arg"; 38 | private static final int MIN_VALUE = 1; 39 | private static final int MAX_VALUE = 100; 40 | private static final int INITIAL_VALUE = 10; 41 | 42 | public static void show(@NonNull FragmentManager fragmentManager, int requestCode, @Nullable List selection) { 43 | NumberPickerDialog dialog = new NumberPickerDialog(); 44 | Bundle args = new Bundle(); 45 | args.putInt(ARG_REQUEST_CODE, requestCode); 46 | if (selection != null) { 47 | args.putSerializable(ARG_SELECTION, new LinkedList<>(selection)); 48 | } 49 | dialog.setArguments(args); 50 | dialog.show(fragmentManager, TAG); 51 | } 52 | 53 | private NumberPicker mNumberPicker; 54 | 55 | @NonNull 56 | @Override 57 | public Dialog onCreateDialog(Bundle savedInstanceState) { 58 | mNumberPicker = new NumberPicker(getContext()); 59 | mNumberPicker.setMinValue(MIN_VALUE); 60 | mNumberPicker.setMaxValue(MAX_VALUE); 61 | mNumberPicker.setValue(INITIAL_VALUE); 62 | mNumberPicker.setWrapSelectorWheel(true); 63 | return new AlertDialog.Builder(getContext()) 64 | .setTitle(R.string.max_select_prompt) 65 | .setView(mNumberPicker) 66 | .setPositiveButton(android.R.string.ok, this) 67 | .setNegativeButton(android.R.string.cancel, null) 68 | .create(); 69 | } 70 | 71 | @Override 72 | public void onClick(DialogInterface dialog, int which) { 73 | //noinspection unchecked 74 | MediaTypeFilterDialog.show(getFragmentManager(), 75 | getArguments().getInt(ARG_REQUEST_CODE), 76 | mNumberPicker.getValue(), 77 | (List) getArguments().getSerializable(ARG_SELECTION)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_add_to_photos_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/drawable-hdpi/ic_add_to_photos_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_add_to_photos_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/drawable-mdpi/ic_add_to_photos_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_add_to_photos_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/drawable-xhdpi/ic_add_to_photos_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_add_to_photos_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/drawable-xxhdpi/ic_add_to_photos_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_add_to_photos_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/drawable-xxxhdpi/ic_add_to_photos_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/content_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/list_item_main.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andremion/Louvre/7ae451ac8aa740219a1c68114e75a6f4eb078fd8/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values-es/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Seleccione la selección máxima 3 | Seleccione el filtro de tipo de medio 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #ff6f00 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Louvre 3 | Select the maximum selection 4 | Select the media type filter 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 13 | 14 | 25 | 26 | 31 | 32 | 37 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':sample', ':louvre' 2 | --------------------------------------------------------------------------------