Repository: zhihu/Matisse Branch: master Commit: 21591aebe73a Files: 123 Total size: 334.1 KB Directory structure: gitextract_8rhgqmr3/ ├── .github/ │ └── ISSUE_TEMPLATE/ │ └── bug_report.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build.gradle ├── checkstyle.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── matisse/ │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── zhihu/ │ │ └── matisse/ │ │ ├── Matisse.java │ │ ├── MimeType.java │ │ ├── SelectionCreator.java │ │ ├── engine/ │ │ │ ├── ImageEngine.java │ │ │ └── impl/ │ │ │ ├── GlideEngine.java │ │ │ └── PicassoEngine.java │ │ ├── filter/ │ │ │ └── Filter.java │ │ ├── internal/ │ │ │ ├── entity/ │ │ │ │ ├── Album.java │ │ │ │ ├── CaptureStrategy.java │ │ │ │ ├── IncapableCause.java │ │ │ │ ├── Item.java │ │ │ │ └── SelectionSpec.java │ │ │ ├── loader/ │ │ │ │ ├── AlbumLoader.java │ │ │ │ └── AlbumMediaLoader.java │ │ │ ├── model/ │ │ │ │ ├── AlbumCollection.java │ │ │ │ ├── AlbumMediaCollection.java │ │ │ │ └── SelectedItemCollection.java │ │ │ ├── ui/ │ │ │ │ ├── AlbumPreviewActivity.java │ │ │ │ ├── BasePreviewActivity.java │ │ │ │ ├── MediaSelectionFragment.java │ │ │ │ ├── PreviewItemFragment.java │ │ │ │ ├── SelectedPreviewActivity.java │ │ │ │ ├── adapter/ │ │ │ │ │ ├── AlbumMediaAdapter.java │ │ │ │ │ ├── AlbumsAdapter.java │ │ │ │ │ ├── PreviewPagerAdapter.java │ │ │ │ │ └── RecyclerViewCursorAdapter.java │ │ │ │ └── widget/ │ │ │ │ ├── AlbumsSpinner.java │ │ │ │ ├── CheckRadioView.java │ │ │ │ ├── CheckView.java │ │ │ │ ├── IncapableDialog.java │ │ │ │ ├── MediaGrid.java │ │ │ │ ├── MediaGridInset.java │ │ │ │ ├── PreviewViewPager.java │ │ │ │ ├── RoundedRectangleImageView.java │ │ │ │ └── SquareFrameLayout.java │ │ │ └── utils/ │ │ │ ├── ExifInterfaceCompat.java │ │ │ ├── MediaStoreCompat.java │ │ │ ├── PathUtils.java │ │ │ ├── PhotoMetadataUtils.java │ │ │ ├── Platform.java │ │ │ ├── SingleMediaScanner.java │ │ │ └── UIUtils.java │ │ ├── listener/ │ │ │ ├── OnCheckedListener.java │ │ │ ├── OnFragmentInteractionListener.java │ │ │ └── OnSelectedListener.java │ │ └── ui/ │ │ └── MatisseActivity.java │ └── res/ │ ├── color/ │ │ ├── dracula_bottom_toolbar_apply.xml │ │ ├── dracula_bottom_toolbar_preview.xml │ │ ├── dracula_preview_bottom_toolbar_apply.xml │ │ ├── zhihu_bottom_toolbar_apply.xml │ │ ├── zhihu_bottom_toolbar_preview.xml │ │ └── zhihu_preview_bottom_toolbar_apply.xml │ ├── layout/ │ │ ├── activity_matisse.xml │ │ ├── activity_media_preview.xml │ │ ├── album_list_item.xml │ │ ├── fragment_media_selection.xml │ │ ├── fragment_preview_item.xml │ │ ├── media_grid_content.xml │ │ ├── media_grid_item.xml │ │ └── photo_capture_item.xml │ ├── values/ │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── colors_dracula.xml │ │ ├── colors_zhihu.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-tr-rTR/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh/ │ │ └── strings.xml │ └── values-zh-rTW/ │ └── strings.xml ├── sample/ │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── zhihu/ │ │ └── matisse/ │ │ └── sample/ │ │ ├── GifSizeFilter.java │ │ └── SampleActivity.java │ └── res/ │ ├── layout/ │ │ ├── activity_main.xml │ │ └── uri_item.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-ca/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-ko/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-tr-rTR/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-zh/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── file_paths_private.xml │ └── file_paths_public.xml └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties .DS_Store /build /captures .idea/ # .gitignore template from https://github.com/github/gitignore/blob/master/Android.gitignore # Built application files *.apk *.ap_ # Files for the Dalvik VM *.dex # Java class files *.class # Generated files bin/ gen/ out/ # Gradle build files matisse/build sample/build # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # Keystore files *.jks ================================================ FILE: .travis.yml ================================================ language: android android: components: - tools - platform-tools - build-tools-28.0.3 - android-28 - extra-android-m2repository jdk: - oraclejdk8 notifications: email: false before_install: - chmod +x gradlew - mkdir "$ANDROID_HOME/licenses" || true - echo -e "\d56f5187479451eabf01fb78af6dfcb131a6481e" "\n24333f8a63b6825ea9c5514f83c2829b004d1fee"> "$ANDROID_HOME/licenses/android-sdk-license" - echo -e "\84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" script: - ./gradlew assemble check - ./gradlew checkstyle ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at gejiaheng@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Matisse is an Open Source Project ## You Should Know - To contribute with a small fix, simply create a pull request. - Better to open an issue to discuss with the team and the community if you're intended to work on something BIG. - Check out our [roadmap](https://github.com/zhihu/Matisse/wiki/Roadmap) to see if some features you want is on the way. - Better to use English to open issues and pull requests. ## Code Style Please follow [Code Style for Contributors](https://source.android.com/source/code-style) of AOSP except - Right margin is 120 characters instead of the default 100 value. And also run `./gradlew checkstyle` to check if there is any style issues before sending a PR. ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 Zhihu Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ ![Image](/image/banner.png) # Matisse [![Build Status](https://travis-ci.org/zhihu/Matisse.svg)](https://travis-ci.org/zhihu/Matisse) [ ![Download](https://api.bintray.com/packages/zhihu/maven/matisse/images/download.svg) ](https://bintray.com/zhihu/maven/matisse/_latestVersion) Matisse is a well-designed local image and video selector for Android. You can - Use it in Activity or Fragment - Select images including JPEG, PNG, GIF and videos including MPEG, MP4 - Apply different themes, including two built-in themes and custom themes - Different image loaders - Define custom filter rules - More to find out yourself | Zhihu Style | Dracula Style | Preview | |:------------------------------:|:---------------------------------:|:--------------------------------:| |![](image/screenshot_zhihu.png) | ![](image/screenshot_dracula.png) | ![](image/screenshot_preview.png)| ## Download Gradle: ```groovy repositories { jcenter() } dependencies { implementation 'com.zhihu.android:matisse:$latest_version' } ``` Check out [Matisse releases](https://github.com/zhihu/Matisse/releases) to see more unstable versions. ## ProGuard If you use [Glide](https://github.com/bumptech/glide) as your image engine, add rules as Glide's README says. And add extra rule: ```pro -dontwarn com.squareup.picasso.** ``` If you use [Picasso](https://github.com/square/picasso) as your image engine, add rules as Picasso's README says. And add extra rule: ```pro -dontwarn com.bumptech.glide.** ``` **Attention**: The above progurad rules are correct. ## How do I use Matisse? #### Permission The library requires two permissions: - `android.permission.READ_EXTERNAL_STORAGE` - `android.permission.WRITE_EXTERNAL_STORAGE` So if you are targeting Android 6.0+, you need to handle runtime permission request before next step. #### Simple usage snippet ------ Start `MatisseActivity` from current `Activity` or `Fragment`: ```java Matisse.from(MainActivity.this) .choose(MimeType.allOf()) .countable(true) .maxSelectable(9) .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) .gridExpectedSize(getResources().getDimensionPixelSize(R.dimen.grid_expected_size)) .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) .thumbnailScale(0.85f) .imageEngine(new GlideEngine()) .showPreview(false) // Default is `true` .forResult(REQUEST_CODE_CHOOSE); ``` #### Themes There are two built-in themes you can use to start `MatisseActivity`: - `R.style.Matisse_Zhihu` (light mode) - `R.style.Matisse_Dracula` (dark mode) And Also you can define your own theme as you wish. #### Receive Result In `onActivityResult()` callback of the starting `Activity` or `Fragment`: ```java List mSelected; @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) { mSelected = Matisse.obtainResult(data); Log.d("Matisse", "mSelected: " + mSelected); } } ``` #### More Find more details about Matisse in [wiki](https://github.com/zhihu/Matisse/wiki). ## Contributing [Matisse is an Open Source Project](https://github.com/zhihu/Matisse/blob/master/CONTRIBUTING.md) ## Thanks This library is inspired by [Laevatein](https://github.com/nohana/Laevatein) and uses some of its source code. ## License Copyright 2017 Zhihu Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: build.gradle ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() google() } dependencies { classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.novoda:bintray-release:0.9.1' } } allprojects { repositories { jcenter() google() } } task clean(type: Delete) { delete rootProject.buildDir } ================================================ FILE: checkstyle.xml ================================================ ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Thu Aug 22 11:37:43 CST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: matisse/build.gradle ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'com.android.library' apply plugin: 'com.novoda.bintray-release' apply plugin: 'checkstyle' android { compileSdkVersion 29 buildToolsVersion '29.0.2' defaultConfig { minSdkVersion 14 targetSdkVersion 29 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } lintOptions { abortOnError false } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.annotation:annotation:1.1.0" implementation "androidx.recyclerview:recyclerview:1.0.0" implementation 'it.sephiroth.android.library.imagezoom:library:1.0.4' compileOnly 'com.github.bumptech.glide:glide:4.9.0' compileOnly 'com.squareup.picasso:picasso:2.5.2' } // jcenter configuration for novoda's bintray-release // $ ./gradlew clean build bintrayUpload -PbintrayUser=BINTRAY_USERNAME -PbintrayKey=BINTRAY_KEY -PdryRun=false publish { userOrg = 'zhihu' groupId = 'com.zhihu.android' artifactId = 'matisse' publishVersion = '0.5.3-beta3' desc = 'A well-designed local image selector for Android' website = 'https://www.zhihu.com/' } task javadoc(type: Javadoc) { options.encoding = "utf-8" } checkstyle { toolVersion = '7.6.1' } tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') options.addStringOption('encoding', 'UTF-8') } task checkstyle(type:Checkstyle) { description 'Runs Checkstyle inspection against matisse sourcesets.' group = 'Code Quality' configFile rootProject.file('checkstyle.xml') ignoreFailures = false showViolations true classpath = files() source 'src/main/java' } ================================================ FILE: matisse/gradle.properties ================================================ ================================================ FILE: matisse/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Library/android-sdk-macosx/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -dontwarn com.squareup.okhttp.** ================================================ FILE: matisse/src/main/AndroidManifest.xml ================================================ ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/Matisse.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse; import android.app.Activity; import android.content.Intent; import android.net.Uri; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.zhihu.matisse.ui.MatisseActivity; import java.lang.ref.WeakReference; import java.util.List; import java.util.Set; /** * Entry for Matisse's media selection. */ public final class Matisse { private final WeakReference mContext; private final WeakReference mFragment; private Matisse(Activity activity) { this(activity, null); } private Matisse(Fragment fragment) { this(fragment.getActivity(), fragment); } private Matisse(Activity activity, Fragment fragment) { mContext = new WeakReference<>(activity); mFragment = new WeakReference<>(fragment); } /** * Start Matisse from an Activity. *

* This Activity's {@link Activity#onActivityResult(int, int, Intent)} will be called when user * finishes selecting. * * @param activity Activity instance. * @return Matisse instance. */ public static Matisse from(Activity activity) { return new Matisse(activity); } /** * Start Matisse from a Fragment. *

* This Fragment's {@link Fragment#onActivityResult(int, int, Intent)} will be called when user * finishes selecting. * * @param fragment Fragment instance. * @return Matisse instance. */ public static Matisse from(Fragment fragment) { return new Matisse(fragment); } /** * Obtain user selected media' {@link Uri} list in the starting Activity or Fragment. * * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or * {@link Fragment#onActivityResult(int, int, Intent)}. * @return User selected media' {@link Uri} list. */ public static List obtainResult(Intent data) { return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION); } /** * Obtain user selected media path list in the starting Activity or Fragment. * * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or * {@link Fragment#onActivityResult(int, int, Intent)}. * @return User selected media path list. */ public static List obtainPathResult(Intent data) { return data.getStringArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH); } /** * Obtain state whether user decide to use selected media in original * * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or * {@link Fragment#onActivityResult(int, int, Intent)}. * @return Whether use original photo */ public static boolean obtainOriginalState(Intent data) { return data.getBooleanExtra(MatisseActivity.EXTRA_RESULT_ORIGINAL_ENABLE, false); } /** * MIME types the selection constrains on. *

* Types not included in the set will still be shown in the grid but can't be chosen. * * @param mimeTypes MIME types set user can choose from. * @return {@link SelectionCreator} to build select specifications. * @see MimeType * @see SelectionCreator */ public SelectionCreator choose(Set mimeTypes) { return this.choose(mimeTypes, true); } /** * MIME types the selection constrains on. *

* Types not included in the set will still be shown in the grid but can't be chosen. * * @param mimeTypes MIME types set user can choose from. * @param mediaTypeExclusive Whether can choose images and videos at the same time during one single choosing * process. true corresponds to not being able to choose images and videos at the same * time, and false corresponds to being able to do this. * @return {@link SelectionCreator} to build select specifications. * @see MimeType * @see SelectionCreator */ public SelectionCreator choose(Set mimeTypes, boolean mediaTypeExclusive) { return new SelectionCreator(this, mimeTypes, mediaTypeExclusive); } @Nullable Activity getActivity() { return mContext.get(); } @Nullable Fragment getFragment() { return mFragment != null ? mFragment.get() : null; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/MimeType.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse; import android.content.ContentResolver; import android.net.Uri; import android.text.TextUtils; import androidx.collection.ArraySet; import android.webkit.MimeTypeMap; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import java.util.Arrays; import java.util.EnumSet; import java.util.Locale; import java.util.Set; /** * MIME Type enumeration to restrict selectable media on the selection activity. Matisse only supports images and * videos. *

* Good example of mime types Android supports: * https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/MediaFile.java */ @SuppressWarnings("unused") public enum MimeType { // ============== images ============== JPEG("image/jpeg", arraySetOf( "jpg", "jpeg" )), PNG("image/png", arraySetOf( "png" )), GIF("image/gif", arraySetOf( "gif" )), BMP("image/x-ms-bmp", arraySetOf( "bmp" )), WEBP("image/webp", arraySetOf( "webp" )), // ============== videos ============== MPEG("video/mpeg", arraySetOf( "mpeg", "mpg" )), MP4("video/mp4", arraySetOf( "mp4", "m4v" )), QUICKTIME("video/quicktime", arraySetOf( "mov" )), THREEGPP("video/3gpp", arraySetOf( "3gp", "3gpp" )), THREEGPP2("video/3gpp2", arraySetOf( "3g2", "3gpp2" )), MKV("video/x-matroska", arraySetOf( "mkv" )), WEBM("video/webm", arraySetOf( "webm" )), TS("video/mp2ts", arraySetOf( "ts" )), AVI("video/avi", arraySetOf( "avi" )); private final String mMimeTypeName; private final Set mExtensions; MimeType(String mimeTypeName, Set extensions) { mMimeTypeName = mimeTypeName; mExtensions = extensions; } public static Set ofAll() { return EnumSet.allOf(MimeType.class); } public static Set of(MimeType type, MimeType... rest) { return EnumSet.of(type, rest); } public static Set ofImage() { return EnumSet.of(JPEG, PNG, GIF, BMP, WEBP); } public static Set ofImage(boolean onlyGif) { return EnumSet.of(GIF); } public static Set ofGif() { return ofImage(true); } public static Set ofVideo() { return EnumSet.of(MPEG, MP4, QUICKTIME, THREEGPP, THREEGPP2, MKV, WEBM, TS, AVI); } public static boolean isImage(String mimeType) { if (mimeType == null) return false; return mimeType.startsWith("image"); } public static boolean isVideo(String mimeType) { if (mimeType == null) return false; return mimeType.startsWith("video"); } public static boolean isGif(String mimeType) { if (mimeType == null) return false; return mimeType.equals(MimeType.GIF.toString()); } private static Set arraySetOf(String... suffixes) { return new ArraySet<>(Arrays.asList(suffixes)); } @Override public String toString() { return mMimeTypeName; } public boolean checkType(ContentResolver resolver, Uri uri) { MimeTypeMap map = MimeTypeMap.getSingleton(); if (uri == null) { return false; } String type = map.getExtensionFromMimeType(resolver.getType(uri)); String path = null; // lazy load the path and prevent resolve for multiple times boolean pathParsed = false; for (String extension : mExtensions) { if (extension.equals(type)) { return true; } if (!pathParsed) { // we only resolve the path for one time path = PhotoMetadataUtils.getPath(resolver, uri); if (!TextUtils.isEmpty(path)) { path = path.toLowerCase(Locale.US); } pathParsed = true; } if (path != null && path.endsWith(extension)) { return true; } } return false; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse; import android.app.Activity; import android.content.Intent; import android.os.Build; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StyleRes; import androidx.fragment.app.Fragment; import com.zhihu.matisse.engine.ImageEngine; import com.zhihu.matisse.filter.Filter; import com.zhihu.matisse.internal.entity.CaptureStrategy; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.listener.OnCheckedListener; import com.zhihu.matisse.listener.OnSelectedListener; import com.zhihu.matisse.ui.MatisseActivity; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Set; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT; /** * Fluent API for building media select specification. */ @SuppressWarnings("unused") public final class SelectionCreator { private final Matisse mMatisse; private final SelectionSpec mSelectionSpec; @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) @IntDef({ SCREEN_ORIENTATION_UNSPECIFIED, SCREEN_ORIENTATION_LANDSCAPE, SCREEN_ORIENTATION_PORTRAIT, SCREEN_ORIENTATION_USER, SCREEN_ORIENTATION_BEHIND, SCREEN_ORIENTATION_SENSOR, SCREEN_ORIENTATION_NOSENSOR, SCREEN_ORIENTATION_SENSOR_LANDSCAPE, SCREEN_ORIENTATION_SENSOR_PORTRAIT, SCREEN_ORIENTATION_REVERSE_LANDSCAPE, SCREEN_ORIENTATION_REVERSE_PORTRAIT, SCREEN_ORIENTATION_FULL_SENSOR, SCREEN_ORIENTATION_USER_LANDSCAPE, SCREEN_ORIENTATION_USER_PORTRAIT, SCREEN_ORIENTATION_FULL_USER, SCREEN_ORIENTATION_LOCKED }) @Retention(RetentionPolicy.SOURCE) @interface ScreenOrientation { } /** * Constructs a new specification builder on the context. * * @param matisse a requester context wrapper. * @param mimeTypes MIME type set to select. */ SelectionCreator(Matisse matisse, @NonNull Set mimeTypes, boolean mediaTypeExclusive) { mMatisse = matisse; mSelectionSpec = SelectionSpec.getCleanInstance(); mSelectionSpec.mimeTypeSet = mimeTypes; mSelectionSpec.mediaTypeExclusive = mediaTypeExclusive; mSelectionSpec.orientation = SCREEN_ORIENTATION_UNSPECIFIED; } /** * Whether to show only one media type if choosing medias are only images or videos. * * @param showSingleMediaType whether to show only one media type, either images or videos. * @return {@link SelectionCreator} for fluent API. * @see SelectionSpec#onlyShowImages() * @see SelectionSpec#onlyShowVideos() */ public SelectionCreator showSingleMediaType(boolean showSingleMediaType) { mSelectionSpec.showSingleMediaType = showSingleMediaType; return this; } /** * Theme for media selecting Activity. *

* There are two built-in themes: * 1. com.zhihu.matisse.R.style.Matisse_Zhihu; * 2. com.zhihu.matisse.R.style.Matisse_Dracula * you can define a custom theme derived from the above ones or other themes. * * @param themeId theme resource id. Default value is com.zhihu.matisse.R.style.Matisse_Zhihu. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator theme(@StyleRes int themeId) { mSelectionSpec.themeId = themeId; return this; } /** * Show a auto-increased number or a check mark when user select media. * * @param countable true for a auto-increased number from 1, false for a check mark. Default * value is false. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator countable(boolean countable) { mSelectionSpec.countable = countable; return this; } /** * Maximum selectable count. * * @param maxSelectable Maximum selectable count. Default value is 1. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator maxSelectable(int maxSelectable) { if (maxSelectable < 1) throw new IllegalArgumentException("maxSelectable must be greater than or equal to one"); if (mSelectionSpec.maxImageSelectable > 0 || mSelectionSpec.maxVideoSelectable > 0) throw new IllegalStateException("already set maxImageSelectable and maxVideoSelectable"); mSelectionSpec.maxSelectable = maxSelectable; return this; } /** * Only useful when {@link SelectionSpec#mediaTypeExclusive} set true and you want to set different maximum * selectable files for image and video media types. * * @param maxImageSelectable Maximum selectable count for image. * @param maxVideoSelectable Maximum selectable count for video. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator maxSelectablePerMediaType(int maxImageSelectable, int maxVideoSelectable) { if (maxImageSelectable < 1 || maxVideoSelectable < 1) throw new IllegalArgumentException(("max selectable must be greater than or equal to one")); mSelectionSpec.maxSelectable = -1; mSelectionSpec.maxImageSelectable = maxImageSelectable; mSelectionSpec.maxVideoSelectable = maxVideoSelectable; return this; } /** * Add filter to filter each selecting item. * * @param filter {@link Filter} * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator addFilter(@NonNull Filter filter) { if (mSelectionSpec.filters == null) { mSelectionSpec.filters = new ArrayList<>(); } if (filter == null) throw new IllegalArgumentException("filter cannot be null"); mSelectionSpec.filters.add(filter); return this; } /** * Determines whether the photo capturing is enabled or not on the media grid view. *

* If this value is set true, photo capturing entry will appear only on All Media's page. * * @param enable Whether to enable capturing or not. Default value is false; * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator capture(boolean enable) { mSelectionSpec.capture = enable; return this; } /** * Show a original photo check options.Let users decide whether use original photo after select * * @param enable Whether to enable original photo or not * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator originalEnable(boolean enable) { mSelectionSpec.originalable = enable; return this; } /** * Determines Whether to hide top and bottom toolbar in PreView mode ,when user tap the picture * @param enable * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator autoHideToolbarOnSingleTap(boolean enable) { mSelectionSpec.autoHideToobar = enable; return this; } /** * Maximum original size,the unit is MB. Only useful when {link@originalEnable} set true * * @param size Maximum original size. Default value is Integer.MAX_VALUE * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator maxOriginalSize(int size) { mSelectionSpec.originalMaxSize = size; return this; } /** * Capture strategy provided for the location to save photos including internal and external * storage and also a authority for {@link androidx.core.content.FileProvider}. * * @param captureStrategy {@link CaptureStrategy}, needed only when capturing is enabled. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator captureStrategy(CaptureStrategy captureStrategy) { mSelectionSpec.captureStrategy = captureStrategy; return this; } /** * Set the desired orientation of this activity. * * @param orientation An orientation constant as used in {@link ScreenOrientation}. * Default value is {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_PORTRAIT}. * @return {@link SelectionCreator} for fluent API. * @see Activity#setRequestedOrientation(int) */ public SelectionCreator restrictOrientation(@ScreenOrientation int orientation) { mSelectionSpec.orientation = orientation; return this; } /** * Set a fixed span count for the media grid. Same for different screen orientations. *

* This will be ignored when {@link #gridExpectedSize(int)} is set. * * @param spanCount Requested span count. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator spanCount(int spanCount) { if (spanCount < 1) throw new IllegalArgumentException("spanCount cannot be less than 1"); mSelectionSpec.spanCount = spanCount; return this; } /** * Set expected size for media grid to adapt to different screen sizes. This won't necessarily * be applied cause the media grid should fill the view container. The measured media grid's * size will be as close to this value as possible. * * @param size Expected media grid size in pixel. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator gridExpectedSize(int size) { mSelectionSpec.gridExpectedSize = size; return this; } /** * Photo thumbnail's scale compared to the View's size. It should be a float value in (0.0, * 1.0]. * * @param scale Thumbnail's scale in (0.0, 1.0]. Default value is 0.5. * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator thumbnailScale(float scale) { if (scale <= 0f || scale > 1f) throw new IllegalArgumentException("Thumbnail scale must be between (0.0, 1.0]"); mSelectionSpec.thumbnailScale = scale; return this; } /** * Provide an image engine. *

* There are two built-in image engines: * 1. {@link com.zhihu.matisse.engine.impl.GlideEngine} * 2. {@link com.zhihu.matisse.engine.impl.PicassoEngine} * And you can implement your own image engine. * * @param imageEngine {@link ImageEngine} * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator imageEngine(ImageEngine imageEngine) { mSelectionSpec.imageEngine = imageEngine; return this; } /** * Set listener for callback immediately when user select or unselect something. *

* It's a redundant API with {@link Matisse#obtainResult(Intent)}, * we only suggest you to use this API when you need to do something immediately. * * @param listener {@link OnSelectedListener} * @return {@link SelectionCreator} for fluent API. */ @NonNull public SelectionCreator setOnSelectedListener(@Nullable OnSelectedListener listener) { mSelectionSpec.onSelectedListener = listener; return this; } /** * Set listener for callback immediately when user check or uncheck original. * * @param listener {@link OnSelectedListener} * @return {@link SelectionCreator} for fluent API. */ public SelectionCreator setOnCheckedListener(@Nullable OnCheckedListener listener) { mSelectionSpec.onCheckedListener = listener; return this; } /** * Start to select media and wait for result. * * @param requestCode Identity of the request Activity or Fragment. */ public void forResult(int requestCode) { Activity activity = mMatisse.getActivity(); if (activity == null) { return; } Intent intent = new Intent(activity, MatisseActivity.class); Fragment fragment = mMatisse.getFragment(); if (fragment != null) { fragment.startActivityForResult(intent, requestCode); } else { activity.startActivityForResult(intent, requestCode); } } public SelectionCreator showPreview(boolean showPreview) { mSelectionSpec.showPreview = showPreview; return this; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/engine/ImageEngine.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.engine; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.widget.ImageView; /** * Image loader interface. There are predefined {@link com.zhihu.matisse.engine.impl.GlideEngine} * and {@link com.zhihu.matisse.engine.impl.PicassoEngine}. */ @SuppressWarnings("unused") public interface ImageEngine { /** * Load thumbnail of a static image resource. * * @param context Context * @param resize Desired size of the origin image * @param placeholder Placeholder drawable when image is not loaded yet * @param imageView ImageView widget * @param uri Uri of the loaded image */ void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri); /** * Load thumbnail of a gif image resource. You don't have to load an animated gif when it's only * a thumbnail tile. * * @param context Context * @param resize Desired size of the origin image * @param placeholder Placeholder drawable when image is not loaded yet * @param imageView ImageView widget * @param uri Uri of the loaded image */ void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri); /** * Load a static image resource. * * @param context Context * @param resizeX Desired x-size of the origin image * @param resizeY Desired y-size of the origin image * @param imageView ImageView widget * @param uri Uri of the loaded image */ void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri); /** * Load a gif image resource. * * @param context Context * @param resizeX Desired x-size of the origin image * @param resizeY Desired y-size of the origin image * @param imageView ImageView widget * @param uri Uri of the loaded image */ void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri); /** * Whether this implementation supports animated gif. * Just knowledge of it, convenient for users. * * @return true support animated gif, false do not support animated gif. */ boolean supportAnimatedGif(); } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/engine/impl/GlideEngine.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.engine.impl; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.widget.ImageView; import com.bumptech.glide.Glide; import com.bumptech.glide.Priority; import com.bumptech.glide.request.RequestOptions; import com.zhihu.matisse.engine.ImageEngine; /** * {@link ImageEngine} implementation using Glide. */ public class GlideEngine implements ImageEngine { @Override public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) { Glide.with(context) .asBitmap() // some .jpeg files are actually gif .load(uri) .apply(new RequestOptions() .override(resize, resize) .placeholder(placeholder) .centerCrop()) .into(imageView); } @Override public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) { Glide.with(context) .asBitmap() // some .jpeg files are actually gif .load(uri) .apply(new RequestOptions() .override(resize, resize) .placeholder(placeholder) .centerCrop()) .into(imageView); } @Override public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { Glide.with(context) .load(uri) .apply(new RequestOptions() .override(resizeX, resizeY) .priority(Priority.HIGH) .fitCenter()) .into(imageView); } @Override public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { Glide.with(context) .asGif() .load(uri) .apply(new RequestOptions() .override(resizeX, resizeY) .priority(Priority.HIGH) .fitCenter()) .into(imageView); } @Override public boolean supportAnimatedGif() { return true; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/engine/impl/PicassoEngine.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.engine.impl; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.widget.ImageView; import com.squareup.picasso.Picasso; import com.zhihu.matisse.engine.ImageEngine; /** * {@link ImageEngine} implementation using Picasso. */ public class PicassoEngine implements ImageEngine { @Override public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) { Picasso.with(context).load(uri).placeholder(placeholder) .resize(resize, resize) .centerCrop() .into(imageView); } @Override public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) { loadThumbnail(context, resize, placeholder, imageView, uri); } @Override public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { Picasso.with(context).load(uri).resize(resizeX, resizeY).priority(Picasso.Priority.HIGH) .centerInside().into(imageView); } @Override public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { loadImage(context, resizeX, resizeY, imageView, uri); } @Override public boolean supportAnimatedGif() { return false; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/filter/Filter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.filter; import android.content.Context; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.SelectionCreator; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.IncapableCause; import java.util.Set; /** * Filter for choosing a {@link Item}. You can add multiple Filters through * {@link SelectionCreator#addFilter(Filter)}. */ @SuppressWarnings("unused") public abstract class Filter { /** * Convenient constant for a minimum value. */ public static final int MIN = 0; /** * Convenient constant for a maximum value. */ public static final int MAX = Integer.MAX_VALUE; /** * Convenient constant for 1024. */ public static final int K = 1024; /** * Against what mime types this filter applies. */ protected abstract Set constraintTypes(); /** * Invoked for filtering each item. * * @return null if selectable, {@link IncapableCause} if not selectable. */ public abstract IncapableCause filter(Context context, Item item); /** * Whether an {@link Item} need filtering. */ protected boolean needFiltering(Context context, Item item) { for (MimeType type : constraintTypes()) { if (type.checkType(context.getContentResolver(), item.getContentUri())) { return true; } } return false; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.entity; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.loader.AlbumLoader; public class Album implements Parcelable { public static final Creator CREATOR = new Creator() { @Nullable @Override public Album createFromParcel(Parcel source) { return new Album(source); } @Override public Album[] newArray(int size) { return new Album[size]; } }; public static final String ALBUM_ID_ALL = String.valueOf(-1); public static final String ALBUM_NAME_ALL = "All"; private final String mId; private final Uri mCoverUri; private final String mDisplayName; private long mCount; public Album(String id, Uri coverUri, String albumName, long count) { mId = id; mCoverUri = coverUri; mDisplayName = albumName; mCount = count; } private Album(Parcel source) { mId = source.readString(); mCoverUri = source.readParcelable(Uri.class.getClassLoader()); mDisplayName = source.readString(); mCount = source.readLong(); } /** * Constructs a new {@link Album} entity from the {@link Cursor}. * This method is not responsible for managing cursor resource, such as close, iterate, and so on. */ public static Album valueOf(Cursor cursor) { String clumn = cursor.getString(cursor.getColumnIndex(AlbumLoader.COLUMN_URI)); return new Album( cursor.getString(cursor.getColumnIndex("bucket_id")), Uri.parse(clumn != null ? clumn : ""), cursor.getString(cursor.getColumnIndex("bucket_display_name")), cursor.getLong(cursor.getColumnIndex(AlbumLoader.COLUMN_COUNT))); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(mId); dest.writeParcelable(mCoverUri, 0); dest.writeString(mDisplayName); dest.writeLong(mCount); } public String getId() { return mId; } public Uri getCoverUri() { return mCoverUri; } public long getCount() { return mCount; } public void addCaptureCount() { mCount++; } public String getDisplayName(Context context) { if (isAll()) { return context.getString(R.string.album_name_all); } return mDisplayName; } public boolean isAll() { return ALBUM_ID_ALL.equals(mId); } public boolean isEmpty() { return mCount == 0; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/entity/CaptureStrategy.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.entity; public class CaptureStrategy { public final boolean isPublic; public final String authority; public final String directory; public CaptureStrategy(boolean isPublic, String authority) { this(isPublic, authority, null); } public CaptureStrategy(boolean isPublic, String authority, String directory) { this.isPublic = isPublic; this.authority = authority; this.directory = directory; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.entity; import android.content.Context; import androidx.annotation.IntDef; import androidx.fragment.app.FragmentActivity; import android.widget.Toast; import com.zhihu.matisse.internal.ui.widget.IncapableDialog; import java.lang.annotation.Retention; import static java.lang.annotation.RetentionPolicy.SOURCE; @SuppressWarnings("unused") public class IncapableCause { public static final int TOAST = 0x00; public static final int DIALOG = 0x01; public static final int NONE = 0x02; @Retention(SOURCE) @IntDef({TOAST, DIALOG, NONE}) public @interface Form { } private int mForm = TOAST; private String mTitle; private String mMessage; public IncapableCause(String message) { mMessage = message; } public IncapableCause(String title, String message) { mTitle = title; mMessage = message; } public IncapableCause(@Form int form, String message) { mForm = form; mMessage = message; } public IncapableCause(@Form int form, String title, String message) { mForm = form; mTitle = title; mMessage = message; } public static void handleCause(Context context, IncapableCause cause) { if (cause == null) return; switch (cause.mForm) { case NONE: // do nothing. break; case DIALOG: IncapableDialog incapableDialog = IncapableDialog.newInstance(cause.mTitle, cause.mMessage); incapableDialog.show(((FragmentActivity) context).getSupportFragmentManager(), IncapableDialog.class.getName()); break; case TOAST: default: Toast.makeText(context, cause.mMessage, Toast.LENGTH_SHORT).show(); break; } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.entity; import android.content.ContentUris; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.provider.MediaStore; import androidx.annotation.Nullable; import com.zhihu.matisse.MimeType; public class Item implements Parcelable { public static final Creator CREATOR = new Creator() { @Override @Nullable public Item createFromParcel(Parcel source) { return new Item(source); } @Override public Item[] newArray(int size) { return new Item[size]; } }; public static final long ITEM_ID_CAPTURE = -1; public static final String ITEM_DISPLAY_NAME_CAPTURE = "Capture"; public final long id; public final String mimeType; public final Uri uri; public final long size; public final long duration; // only for video, in ms private Item(long id, String mimeType, long size, long duration) { this.id = id; this.mimeType = mimeType; Uri contentUri; if (isImage()) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if (isVideo()) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else { // ? contentUri = MediaStore.Files.getContentUri("external"); } this.uri = ContentUris.withAppendedId(contentUri, id); this.size = size; this.duration = duration; } private Item(Parcel source) { id = source.readLong(); mimeType = source.readString(); uri = source.readParcelable(Uri.class.getClassLoader()); size = source.readLong(); duration = source.readLong(); } public static Item valueOf(Cursor cursor) { return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)), cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)), cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)), cursor.getLong(cursor.getColumnIndex("duration"))); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeString(mimeType); dest.writeParcelable(uri, 0); dest.writeLong(size); dest.writeLong(duration); } public Uri getContentUri() { return uri; } public boolean isCapture() { return id == ITEM_ID_CAPTURE; } public boolean isImage() { return MimeType.isImage(mimeType); } public boolean isGif() { return MimeType.isGif(mimeType); } public boolean isVideo() { return MimeType.isVideo(mimeType); } @Override public boolean equals(Object obj) { if (!(obj instanceof Item)) { return false; } Item other = (Item) obj; return id == other.id && (mimeType != null && mimeType.equals(other.mimeType) || (mimeType == null && other.mimeType == null)) && (uri != null && uri.equals(other.uri) || (uri == null && other.uri == null)) && size == other.size && duration == other.duration; } @Override public int hashCode() { int result = 1; result = 31 * result + Long.valueOf(id).hashCode(); if (mimeType != null) { result = 31 * result + mimeType.hashCode(); } result = 31 * result + uri.hashCode(); result = 31 * result + Long.valueOf(size).hashCode(); result = 31 * result + Long.valueOf(duration).hashCode(); return result; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.entity; import android.content.pm.ActivityInfo; import androidx.annotation.StyleRes; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.R; import com.zhihu.matisse.engine.ImageEngine; import com.zhihu.matisse.engine.impl.GlideEngine; import com.zhihu.matisse.filter.Filter; import com.zhihu.matisse.listener.OnCheckedListener; import com.zhihu.matisse.listener.OnSelectedListener; import java.util.List; import java.util.Set; public final class SelectionSpec { public Set mimeTypeSet; public boolean mediaTypeExclusive; public boolean showSingleMediaType; @StyleRes public int themeId; public int orientation; public boolean countable; public int maxSelectable; public int maxImageSelectable; public int maxVideoSelectable; public List filters; public boolean capture; public CaptureStrategy captureStrategy; public int spanCount; public int gridExpectedSize; public float thumbnailScale; public ImageEngine imageEngine; public boolean hasInited; public OnSelectedListener onSelectedListener; public boolean originalable; public boolean autoHideToobar; public int originalMaxSize; public OnCheckedListener onCheckedListener; public boolean showPreview; private SelectionSpec() { } public static SelectionSpec getInstance() { return InstanceHolder.INSTANCE; } public static SelectionSpec getCleanInstance() { SelectionSpec selectionSpec = getInstance(); selectionSpec.reset(); return selectionSpec; } private void reset() { mimeTypeSet = null; mediaTypeExclusive = true; showSingleMediaType = false; themeId = R.style.Matisse_Zhihu; orientation = 0; countable = false; maxSelectable = 1; maxImageSelectable = 0; maxVideoSelectable = 0; filters = null; capture = false; captureStrategy = null; spanCount = 3; gridExpectedSize = 0; thumbnailScale = 0.5f; imageEngine = new GlideEngine(); hasInited = true; originalable = false; autoHideToobar = false; originalMaxSize = Integer.MAX_VALUE; showPreview = true; } public boolean singleSelectionModeEnabled() { return !countable && (maxSelectable == 1 || (maxImageSelectable == 1 && maxVideoSelectable == 1)); } public boolean needOrientationRestriction() { return orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } public boolean onlyShowImages() { return showSingleMediaType && MimeType.ofImage().containsAll(mimeTypeSet); } public boolean onlyShowVideos() { return showSingleMediaType && MimeType.ofVideo().containsAll(mimeTypeSet); } public boolean onlyShowGif() { return showSingleMediaType && MimeType.ofGif().equals(mimeTypeSet); } private static final class InstanceHolder { private static final SelectionSpec INSTANCE = new SelectionSpec(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.loader; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; import androidx.loader.content.CursorLoader; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.SelectionSpec; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Load all albums (grouped by bucket_id) into a single cursor. */ public class AlbumLoader extends CursorLoader { private static final String COLUMN_BUCKET_ID = "bucket_id"; private static final String COLUMN_BUCKET_DISPLAY_NAME = "bucket_display_name"; public static final String COLUMN_URI = "uri"; public static final String COLUMN_COUNT = "count"; private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); private static final String[] COLUMNS = { MediaStore.Files.FileColumns._ID, COLUMN_BUCKET_ID, COLUMN_BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, COLUMN_URI, COLUMN_COUNT}; private static final String[] PROJECTION = { MediaStore.Files.FileColumns._ID, COLUMN_BUCKET_ID, COLUMN_BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, "COUNT(*) AS " + COLUMN_COUNT}; private static final String[] PROJECTION_29 = { MediaStore.Files.FileColumns._ID, COLUMN_BUCKET_ID, COLUMN_BUCKET_DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE}; // === params for showSingleMediaType: false === private static final String SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)" + " AND " + MediaStore.MediaColumns.SIZE + ">0" + ") GROUP BY (bucket_id"; private static final String SELECTION_29 = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static final String[] SELECTION_ARGS = { String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), }; // ============================================= // === params for showSingleMediaType: true === private static final String SELECTION_FOR_SINGLE_MEDIA_TYPE = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0" + ") GROUP BY (bucket_id"; private static final String SELECTION_FOR_SINGLE_MEDIA_TYPE_29 = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionArgsForSingleMediaType(int mediaType) { return new String[]{String.valueOf(mediaType)}; } // ============================================= // === params for showSingleMediaType: true === private static final String SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0" + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?" + ") GROUP BY (bucket_id"; private static final String SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE_29 = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0" + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?"; private static String[] getSelectionArgsForSingleMediaGifType(int mediaType) { return new String[]{String.valueOf(mediaType), "image/gif"}; } // ============================================= private static final String BUCKET_ORDER_BY = "datetaken DESC"; private AlbumLoader(Context context, String selection, String[] selectionArgs) { super( context, QUERY_URI, beforeAndroidTen() ? PROJECTION : PROJECTION_29, selection, selectionArgs, BUCKET_ORDER_BY ); } public static CursorLoader newInstance(Context context) { String selection; String[] selectionArgs; if (SelectionSpec.getInstance().onlyShowGif()) { selection = beforeAndroidTen() ? SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE : SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE_29; selectionArgs = getSelectionArgsForSingleMediaGifType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); } else if (SelectionSpec.getInstance().onlyShowImages()) { selection = beforeAndroidTen() ? SELECTION_FOR_SINGLE_MEDIA_TYPE : SELECTION_FOR_SINGLE_MEDIA_TYPE_29; selectionArgs = getSelectionArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); } else if (SelectionSpec.getInstance().onlyShowVideos()) { selection = beforeAndroidTen() ? SELECTION_FOR_SINGLE_MEDIA_TYPE : SELECTION_FOR_SINGLE_MEDIA_TYPE_29; selectionArgs = getSelectionArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO); } else { selection = beforeAndroidTen() ? SELECTION : SELECTION_29; selectionArgs = SELECTION_ARGS; } return new AlbumLoader(context, selection, selectionArgs); } @Override public Cursor loadInBackground() { Cursor albums = super.loadInBackground(); MatrixCursor allAlbum = new MatrixCursor(COLUMNS); if (beforeAndroidTen()) { int totalCount = 0; Uri allAlbumCoverUri = null; MatrixCursor otherAlbums = new MatrixCursor(COLUMNS); if (albums != null) { while (albums.moveToNext()) { long fileId = albums.getLong( albums.getColumnIndex(MediaStore.Files.FileColumns._ID)); long bucketId = albums.getLong( albums.getColumnIndex(COLUMN_BUCKET_ID)); String bucketDisplayName = albums.getString( albums.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME)); String mimeType = albums.getString( albums.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)); Uri uri = getUri(albums); int count = albums.getInt(albums.getColumnIndex(COLUMN_COUNT)); otherAlbums.addRow(new String[]{ Long.toString(fileId), Long.toString(bucketId), bucketDisplayName, mimeType, uri.toString(), String.valueOf(count)}); totalCount += count; } if (albums.moveToFirst()) { allAlbumCoverUri = getUri(albums); } } allAlbum.addRow(new String[]{ Album.ALBUM_ID_ALL, Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL, null, allAlbumCoverUri == null ? null : allAlbumCoverUri.toString(), String.valueOf(totalCount)}); return new MergeCursor(new Cursor[]{allAlbum, otherAlbums}); } else { int totalCount = 0; Uri allAlbumCoverUri = null; // Pseudo GROUP BY Map countMap = new HashMap<>(); if (albums != null) { while (albums.moveToNext()) { long bucketId = albums.getLong(albums.getColumnIndex(COLUMN_BUCKET_ID)); Long count = countMap.get(bucketId); if (count == null) { count = 1L; } else { count++; } countMap.put(bucketId, count); } } MatrixCursor otherAlbums = new MatrixCursor(COLUMNS); if (albums != null) { if (albums.moveToFirst()) { allAlbumCoverUri = getUri(albums); Set done = new HashSet<>(); do { long bucketId = albums.getLong(albums.getColumnIndex(COLUMN_BUCKET_ID)); if (done.contains(bucketId)) { continue; } long fileId = albums.getLong( albums.getColumnIndex(MediaStore.Files.FileColumns._ID)); String bucketDisplayName = albums.getString( albums.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME)); String mimeType = albums.getString( albums.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)); Uri uri = getUri(albums); long count = countMap.get(bucketId); otherAlbums.addRow(new String[]{ Long.toString(fileId), Long.toString(bucketId), bucketDisplayName, mimeType, uri.toString(), String.valueOf(count)}); done.add(bucketId); totalCount += count; } while (albums.moveToNext()); } } allAlbum.addRow(new String[]{ Album.ALBUM_ID_ALL, Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL, null, allAlbumCoverUri == null ? null : allAlbumCoverUri.toString(), String.valueOf(totalCount)}); return new MergeCursor(new Cursor[]{allAlbum, otherAlbums}); } } private static Uri getUri(Cursor cursor) { long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)); String mimeType = cursor.getString( cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)); Uri contentUri; if (MimeType.isImage(mimeType)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if (MimeType.isVideo(mimeType)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else { // ? contentUri = MediaStore.Files.getContentUri("external"); } Uri uri = ContentUris.withAppendedId(contentUri, id); return uri; } @Override public void onContentChanged() { // FIXME a dirty way to fix loading multiple times } /** * @return 是否是 Android 10 (Q) 之前的版本 */ private static boolean beforeAndroidTen() { return android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.loader; import android.content.Context; import android.database.Cursor; import android.database.MatrixCursor; import android.database.MergeCursor; import android.net.Uri; import android.provider.MediaStore; import androidx.loader.content.CursorLoader; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.utils.MediaStoreCompat; /** * Load images and videos into a single cursor. */ public class AlbumMediaLoader extends CursorLoader { private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); private static final String[] PROJECTION = { MediaStore.Files.FileColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.SIZE, "duration"}; // === params for album ALL && showSingleMediaType: false === private static final String SELECTION_ALL = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static final String[] SELECTION_ALL_ARGS = { String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), }; // =========================================================== // === params for album ALL && showSingleMediaType: true === private static final String SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionArgsForSingleMediaType(int mediaType) { return new String[]{String.valueOf(mediaType)}; } // ========================================================= // === params for ordinary album && showSingleMediaType: false === private static final String SELECTION_ALBUM = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)" + " AND " + " bucket_id=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionAlbumArgs(String albumId) { return new String[]{ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), albumId }; } // =============================================================== // === params for ordinary album && showSingleMediaType: true === private static final String SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + " bucket_id=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionAlbumArgsForSingleMediaType(int mediaType, String albumId) { return new String[]{String.valueOf(mediaType), albumId}; } // =============================================================== // === params for album ALL && showSingleMediaType: true && MineType=="image/gif" private static final String SELECTION_ALL_FOR_GIF = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionArgsForGifType(int mediaType) { return new String[]{String.valueOf(mediaType), "image/gif"}; } // =============================================================== // === params for ordinary album && showSingleMediaType: true && MineType=="image/gif" === private static final String SELECTION_ALBUM_FOR_GIF = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + " AND " + " bucket_id=?" + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?" + " AND " + MediaStore.MediaColumns.SIZE + ">0"; private static String[] getSelectionAlbumArgsForGifType(int mediaType, String albumId) { return new String[]{String.valueOf(mediaType), albumId, "image/gif"}; } // =============================================================== private static final String ORDER_BY = MediaStore.Images.Media.DATE_TAKEN + " DESC"; private final boolean mEnableCapture; private AlbumMediaLoader(Context context, String selection, String[] selectionArgs, boolean capture) { super(context, QUERY_URI, PROJECTION, selection, selectionArgs, ORDER_BY); mEnableCapture = capture; } public static CursorLoader newInstance(Context context, Album album, boolean capture) { String selection; String[] selectionArgs; boolean enableCapture; if (album.isAll()) { if (SelectionSpec.getInstance().onlyShowGif()) { selection = SELECTION_ALL_FOR_GIF; selectionArgs = getSelectionArgsForGifType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); } else if (SelectionSpec.getInstance().onlyShowImages()) { selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE; selectionArgs = getSelectionArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); } else if (SelectionSpec.getInstance().onlyShowVideos()) { selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE; selectionArgs = getSelectionArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO); } else { selection = SELECTION_ALL; selectionArgs = SELECTION_ALL_ARGS; } enableCapture = capture; } else { if (SelectionSpec.getInstance().onlyShowGif()) { selection = SELECTION_ALBUM_FOR_GIF; selectionArgs = getSelectionAlbumArgsForGifType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE, album.getId()); } else if (SelectionSpec.getInstance().onlyShowImages()) { selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE; selectionArgs = getSelectionAlbumArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE, album.getId()); } else if (SelectionSpec.getInstance().onlyShowVideos()) { selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE; selectionArgs = getSelectionAlbumArgsForSingleMediaType( MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO, album.getId()); } else { selection = SELECTION_ALBUM; selectionArgs = getSelectionAlbumArgs(album.getId()); } enableCapture = false; } return new AlbumMediaLoader(context, selection, selectionArgs, enableCapture); } @Override public Cursor loadInBackground() { Cursor result = super.loadInBackground(); if (!mEnableCapture || !MediaStoreCompat.hasCameraFeature(getContext())) { return result; } MatrixCursor dummy = new MatrixCursor(PROJECTION); dummy.addRow(new Object[]{Item.ITEM_ID_CAPTURE, Item.ITEM_DISPLAY_NAME_CAPTURE, "", 0, 0}); return new MergeCursor(new Cursor[]{dummy, result}); } @Override public void onContentChanged() { // FIXME a dirty way to fix loading multiple times } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.model; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import com.zhihu.matisse.internal.loader.AlbumLoader; import java.lang.ref.WeakReference; public class AlbumCollection implements LoaderManager.LoaderCallbacks { private static final int LOADER_ID = 1; private static final String STATE_CURRENT_SELECTION = "state_current_selection"; private WeakReference mContext; private LoaderManager mLoaderManager; private AlbumCallbacks mCallbacks; private int mCurrentSelection; private boolean mLoadFinished; @Override public Loader onCreateLoader(int id, Bundle args) { Context context = mContext.get(); if (context == null) { return null; } mLoadFinished = false; return AlbumLoader.newInstance(context); } @Override public void onLoadFinished(Loader loader, Cursor data) { Context context = mContext.get(); if (context == null) { return; } if (!mLoadFinished) { mLoadFinished = true; mCallbacks.onAlbumLoad(data); } } @Override public void onLoaderReset(Loader loader) { Context context = mContext.get(); if (context == null) { return; } mCallbacks.onAlbumReset(); } public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) { mContext = new WeakReference(activity); mLoaderManager = activity.getSupportLoaderManager(); mCallbacks = callbacks; } public void onRestoreInstanceState(Bundle savedInstanceState) { if (savedInstanceState == null) { return; } mCurrentSelection = savedInstanceState.getInt(STATE_CURRENT_SELECTION); } public void onSaveInstanceState(Bundle outState) { outState.putInt(STATE_CURRENT_SELECTION, mCurrentSelection); } public void onDestroy() { if (mLoaderManager != null) { mLoaderManager.destroyLoader(LOADER_ID); } mCallbacks = null; } public void loadAlbums() { mLoaderManager.initLoader(LOADER_ID, null, this); } public int getCurrentSelection() { return mCurrentSelection; } public void setStateCurrentSelection(int currentSelection) { mCurrentSelection = currentSelection; } public interface AlbumCallbacks { void onAlbumLoad(Cursor cursor); void onAlbumReset(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.model; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager; import androidx.loader.content.Loader; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.loader.AlbumMediaLoader; import java.lang.ref.WeakReference; public class AlbumMediaCollection implements LoaderManager.LoaderCallbacks { private static final int LOADER_ID = 2; private static final String ARGS_ALBUM = "args_album"; private static final String ARGS_ENABLE_CAPTURE = "args_enable_capture"; private WeakReference mContext; private LoaderManager mLoaderManager; private AlbumMediaCallbacks mCallbacks; @Override public Loader onCreateLoader(int id, Bundle args) { Context context = mContext.get(); if (context == null) { return null; } Album album = args.getParcelable(ARGS_ALBUM); if (album == null) { return null; } return AlbumMediaLoader.newInstance(context, album, album.isAll() && args.getBoolean(ARGS_ENABLE_CAPTURE, false)); } @Override public void onLoadFinished(Loader loader, Cursor data) { Context context = mContext.get(); if (context == null) { return; } mCallbacks.onAlbumMediaLoad(data); } @Override public void onLoaderReset(Loader loader) { Context context = mContext.get(); if (context == null) { return; } mCallbacks.onAlbumMediaReset(); } public void onCreate(@NonNull FragmentActivity context, @NonNull AlbumMediaCallbacks callbacks) { mContext = new WeakReference(context); mLoaderManager = context.getSupportLoaderManager(); mCallbacks = callbacks; } public void onDestroy() { if (mLoaderManager != null) { mLoaderManager.destroyLoader(LOADER_ID); } mCallbacks = null; } public void load(@Nullable Album target) { load(target, false); } public void load(@Nullable Album target, boolean enableCapture) { Bundle args = new Bundle(); args.putParcelable(ARGS_ALBUM, target); args.putBoolean(ARGS_ENABLE_CAPTURE, enableCapture); mLoaderManager.initLoader(LOADER_ID, args, this); } public interface AlbumMediaCallbacks { void onAlbumMediaLoad(Cursor cursor); void onAlbumMediaReset(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.model; import android.content.Context; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.IncapableCause; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.ui.widget.CheckView; import com.zhihu.matisse.internal.utils.PathUtils; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @SuppressWarnings("unused") public class SelectedItemCollection { public static final String STATE_SELECTION = "state_selection"; public static final String STATE_COLLECTION_TYPE = "state_collection_type"; /** * Empty collection */ public static final int COLLECTION_UNDEFINED = 0x00; /** * Collection only with images */ public static final int COLLECTION_IMAGE = 0x01; /** * Collection only with videos */ public static final int COLLECTION_VIDEO = 0x01 << 1; /** * Collection with images and videos. */ public static final int COLLECTION_MIXED = COLLECTION_IMAGE | COLLECTION_VIDEO; private final Context mContext; private Set mItems; private int mCollectionType = COLLECTION_UNDEFINED; public SelectedItemCollection(Context context) { mContext = context; } public void onCreate(Bundle bundle) { if (bundle == null) { mItems = new LinkedHashSet<>(); } else { List saved = bundle.getParcelableArrayList(STATE_SELECTION); mItems = new LinkedHashSet<>(saved); mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED); } } public void setDefaultSelection(List uris) { mItems.addAll(uris); } public void onSaveInstanceState(Bundle outState) { outState.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems)); outState.putInt(STATE_COLLECTION_TYPE, mCollectionType); } public Bundle getDataWithBundle() { Bundle bundle = new Bundle(); bundle.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems)); bundle.putInt(STATE_COLLECTION_TYPE, mCollectionType); return bundle; } public boolean add(Item item) { if (typeConflict(item)) { throw new IllegalArgumentException("Can't select images and videos at the same time."); } boolean added = mItems.add(item); if (added) { if (mCollectionType == COLLECTION_UNDEFINED) { if (item.isImage()) { mCollectionType = COLLECTION_IMAGE; } else if (item.isVideo()) { mCollectionType = COLLECTION_VIDEO; } } else if (mCollectionType == COLLECTION_IMAGE) { if (item.isVideo()) { mCollectionType = COLLECTION_MIXED; } } else if (mCollectionType == COLLECTION_VIDEO) { if (item.isImage()) { mCollectionType = COLLECTION_MIXED; } } } return added; } public boolean remove(Item item) { boolean removed = mItems.remove(item); if (removed) { if (mItems.size() == 0) { mCollectionType = COLLECTION_UNDEFINED; } else { if (mCollectionType == COLLECTION_MIXED) { refineCollectionType(); } } } return removed; } public void overwrite(ArrayList items, int collectionType) { if (items.size() == 0) { mCollectionType = COLLECTION_UNDEFINED; } else { mCollectionType = collectionType; } mItems.clear(); mItems.addAll(items); } public List asList() { return new ArrayList<>(mItems); } public List asListOfUri() { List uris = new ArrayList<>(); for (Item item : mItems) { uris.add(item.getContentUri()); } return uris; } public List asListOfString() { List paths = new ArrayList<>(); for (Item item : mItems) { paths.add(PathUtils.getPath(mContext, item.getContentUri())); } return paths; } public boolean isEmpty() { return mItems == null || mItems.isEmpty(); } public boolean isSelected(Item item) { return mItems.contains(item); } public IncapableCause isAcceptable(Item item) { if (maxSelectableReached()) { int maxSelectable = currentMaxSelectable(); String cause; try { cause = mContext.getResources().getQuantityString( R.plurals.error_over_count, maxSelectable, maxSelectable ); } catch (Resources.NotFoundException e) { cause = mContext.getString( R.string.error_over_count, maxSelectable ); } catch (NoClassDefFoundError e) { cause = mContext.getString( R.string.error_over_count, maxSelectable ); } return new IncapableCause(cause); } else if (typeConflict(item)) { return new IncapableCause(mContext.getString(R.string.error_type_conflict)); } return PhotoMetadataUtils.isAcceptable(mContext, item); } public boolean maxSelectableReached() { return mItems.size() == currentMaxSelectable(); } // depends private int currentMaxSelectable() { SelectionSpec spec = SelectionSpec.getInstance(); if (spec.maxSelectable > 0) { return spec.maxSelectable; } else if (mCollectionType == COLLECTION_IMAGE) { return spec.maxImageSelectable; } else if (mCollectionType == COLLECTION_VIDEO) { return spec.maxVideoSelectable; } else { return spec.maxSelectable; } } public int getCollectionType() { return mCollectionType; } private void refineCollectionType() { boolean hasImage = false; boolean hasVideo = false; for (Item i : mItems) { if (i.isImage() && !hasImage) hasImage = true; if (i.isVideo() && !hasVideo) hasVideo = true; } if (hasImage && hasVideo) { mCollectionType = COLLECTION_MIXED; } else if (hasImage) { mCollectionType = COLLECTION_IMAGE; } else if (hasVideo) { mCollectionType = COLLECTION_VIDEO; } } /** * Determine whether there will be conflict media types. A user can only select images and videos at the same time * while {@link SelectionSpec#mediaTypeExclusive} is set to false. */ public boolean typeConflict(Item item) { return SelectionSpec.getInstance().mediaTypeExclusive && ((item.isImage() && (mCollectionType == COLLECTION_VIDEO || mCollectionType == COLLECTION_MIXED)) || (item.isVideo() && (mCollectionType == COLLECTION_IMAGE || mCollectionType == COLLECTION_MIXED))); } public int count() { return mItems.size(); } public int checkedNumOf(Item item) { int index = new ArrayList<>(mItems).indexOf(item); return index == -1 ? CheckView.UNCHECKED : index + 1; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui; import android.database.Cursor; import android.os.Bundle; import androidx.annotation.Nullable; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.AlbumMediaCollection; import com.zhihu.matisse.internal.ui.adapter.PreviewPagerAdapter; import java.util.ArrayList; import java.util.List; public class AlbumPreviewActivity extends BasePreviewActivity implements AlbumMediaCollection.AlbumMediaCallbacks { public static final String EXTRA_ALBUM = "extra_album"; public static final String EXTRA_ITEM = "extra_item"; private AlbumMediaCollection mCollection = new AlbumMediaCollection(); private boolean mIsAlreadySetPosition; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!SelectionSpec.getInstance().hasInited) { setResult(RESULT_CANCELED); finish(); return; } mCollection.onCreate(this, this); Album album = getIntent().getParcelableExtra(EXTRA_ALBUM); mCollection.load(album); Item item = getIntent().getParcelableExtra(EXTRA_ITEM); if (mSpec.countable) { mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item)); } else { mCheckView.setChecked(mSelectedCollection.isSelected(item)); } updateSize(item); } @Override protected void onDestroy() { super.onDestroy(); mCollection.onDestroy(); } @Override public void onAlbumMediaLoad(Cursor cursor) { List items = new ArrayList<>(); while (cursor.moveToNext()) { items.add(Item.valueOf(cursor)); } // cursor.close(); if (items.isEmpty()) { return; } PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter(); adapter.addAll(items); adapter.notifyDataSetChanged(); if (!mIsAlreadySetPosition) { //onAlbumMediaLoad is called many times.. mIsAlreadySetPosition = true; Item selected = getIntent().getParcelableExtra(EXTRA_ITEM); int selectedIndex = items.indexOf(selected); mPager.setCurrentItem(selectedIndex, false); mPreviousPos = selectedIndex; } } @Override public void onAlbumMediaReset() { } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui; import android.app.Activity; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.viewpager.widget.ViewPager; import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.appcompat.app.AppCompatActivity; import android.view.View; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.IncapableCause; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.SelectedItemCollection; import com.zhihu.matisse.internal.ui.adapter.PreviewPagerAdapter; import com.zhihu.matisse.internal.ui.widget.CheckRadioView; import com.zhihu.matisse.internal.ui.widget.CheckView; import com.zhihu.matisse.internal.ui.widget.IncapableDialog; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import com.zhihu.matisse.internal.utils.Platform; import com.zhihu.matisse.listener.OnFragmentInteractionListener; public abstract class BasePreviewActivity extends AppCompatActivity implements View.OnClickListener, ViewPager.OnPageChangeListener, OnFragmentInteractionListener { public static final String EXTRA_DEFAULT_BUNDLE = "extra_default_bundle"; public static final String EXTRA_RESULT_BUNDLE = "extra_result_bundle"; public static final String EXTRA_RESULT_APPLY = "extra_result_apply"; public static final String EXTRA_RESULT_ORIGINAL_ENABLE = "extra_result_original_enable"; public static final String CHECK_STATE = "checkState"; protected final SelectedItemCollection mSelectedCollection = new SelectedItemCollection(this); protected SelectionSpec mSpec; protected ViewPager mPager; protected PreviewPagerAdapter mAdapter; protected CheckView mCheckView; protected TextView mButtonBack; protected TextView mButtonApply; protected TextView mSize; protected int mPreviousPos = -1; private LinearLayout mOriginalLayout; private CheckRadioView mOriginal; protected boolean mOriginalEnable; private FrameLayout mBottomToolbar; private FrameLayout mTopToolbar; private boolean mIsToolbarHide = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { setTheme(SelectionSpec.getInstance().themeId); super.onCreate(savedInstanceState); if (!SelectionSpec.getInstance().hasInited) { setResult(RESULT_CANCELED); finish(); return; } setContentView(R.layout.activity_media_preview); if (Platform.hasKitKat()) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } mSpec = SelectionSpec.getInstance(); if (mSpec.needOrientationRestriction()) { setRequestedOrientation(mSpec.orientation); } if (savedInstanceState == null) { mSelectedCollection.onCreate(getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE)); mOriginalEnable = getIntent().getBooleanExtra(EXTRA_RESULT_ORIGINAL_ENABLE, false); } else { mSelectedCollection.onCreate(savedInstanceState); mOriginalEnable = savedInstanceState.getBoolean(CHECK_STATE); } mButtonBack = (TextView) findViewById(R.id.button_back); mButtonApply = (TextView) findViewById(R.id.button_apply); mSize = (TextView) findViewById(R.id.size); mButtonBack.setOnClickListener(this); mButtonApply.setOnClickListener(this); mPager = (ViewPager) findViewById(R.id.pager); mPager.addOnPageChangeListener(this); mAdapter = new PreviewPagerAdapter(getSupportFragmentManager(), null); mPager.setAdapter(mAdapter); mCheckView = (CheckView) findViewById(R.id.check_view); mCheckView.setCountable(mSpec.countable); mBottomToolbar = findViewById(R.id.bottom_toolbar); mTopToolbar = findViewById(R.id.top_toolbar); mCheckView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Item item = mAdapter.getMediaItem(mPager.getCurrentItem()); if (mSelectedCollection.isSelected(item)) { mSelectedCollection.remove(item); if (mSpec.countable) { mCheckView.setCheckedNum(CheckView.UNCHECKED); } else { mCheckView.setChecked(false); } } else { if (assertAddSelection(item)) { mSelectedCollection.add(item); if (mSpec.countable) { mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item)); } else { mCheckView.setChecked(true); } } } updateApplyButton(); if (mSpec.onSelectedListener != null) { mSpec.onSelectedListener.onSelected( mSelectedCollection.asListOfUri(), mSelectedCollection.asListOfString()); } } }); mOriginalLayout = findViewById(R.id.originalLayout); mOriginal = findViewById(R.id.original); mOriginalLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int count = countOverMaxSize(); if (count > 0) { IncapableDialog incapableDialog = IncapableDialog.newInstance("", getString(R.string.error_over_original_count, count, mSpec.originalMaxSize)); incapableDialog.show(getSupportFragmentManager(), IncapableDialog.class.getName()); return; } mOriginalEnable = !mOriginalEnable; mOriginal.setChecked(mOriginalEnable); if (!mOriginalEnable) { mOriginal.setColor(Color.WHITE); } if (mSpec.onCheckedListener != null) { mSpec.onCheckedListener.onCheck(mOriginalEnable); } } }); updateApplyButton(); } @Override protected void onSaveInstanceState(Bundle outState) { mSelectedCollection.onSaveInstanceState(outState); outState.putBoolean("checkState", mOriginalEnable); super.onSaveInstanceState(outState); } @Override public void onBackPressed() { sendBackResult(false); super.onBackPressed(); } @Override public void onClick(View v) { if (v.getId() == R.id.button_back) { onBackPressed(); } else if (v.getId() == R.id.button_apply) { sendBackResult(true); finish(); } } @Override public void onClick() { if (!mSpec.autoHideToobar) { return; } if (mIsToolbarHide) { mTopToolbar.animate() .setInterpolator(new FastOutSlowInInterpolator()) .translationYBy(mTopToolbar.getMeasuredHeight()) .start(); mBottomToolbar.animate() .translationYBy(-mBottomToolbar.getMeasuredHeight()) .setInterpolator(new FastOutSlowInInterpolator()) .start(); } else { mTopToolbar.animate() .setInterpolator(new FastOutSlowInInterpolator()) .translationYBy(-mTopToolbar.getMeasuredHeight()) .start(); mBottomToolbar.animate() .setInterpolator(new FastOutSlowInInterpolator()) .translationYBy(mBottomToolbar.getMeasuredHeight()) .start(); } mIsToolbarHide = !mIsToolbarHide; } @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { } @Override public void onPageSelected(int position) { PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter(); if (mPreviousPos != -1 && mPreviousPos != position) { ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView(); Item item = adapter.getMediaItem(position); if (mSpec.countable) { int checkedNum = mSelectedCollection.checkedNumOf(item); mCheckView.setCheckedNum(checkedNum); if (checkedNum > 0) { mCheckView.setEnabled(true); } else { mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached()); } } else { boolean checked = mSelectedCollection.isSelected(item); mCheckView.setChecked(checked); if (checked) { mCheckView.setEnabled(true); } else { mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached()); } } updateSize(item); } mPreviousPos = position; } @Override public void onPageScrollStateChanged(int state) { } private void updateApplyButton() { int selectedCount = mSelectedCollection.count(); if (selectedCount == 0) { mButtonApply.setText(R.string.button_apply_default); mButtonApply.setEnabled(false); } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) { mButtonApply.setText(R.string.button_apply_default); mButtonApply.setEnabled(true); } else { mButtonApply.setEnabled(true); mButtonApply.setText(getString(R.string.button_apply, selectedCount)); } if (mSpec.originalable) { mOriginalLayout.setVisibility(View.VISIBLE); updateOriginalState(); } else { mOriginalLayout.setVisibility(View.GONE); } } private void updateOriginalState() { mOriginal.setChecked(mOriginalEnable); if (!mOriginalEnable) { mOriginal.setColor(Color.WHITE); } if (countOverMaxSize() > 0) { if (mOriginalEnable) { IncapableDialog incapableDialog = IncapableDialog.newInstance("", getString(R.string.error_over_original_size, mSpec.originalMaxSize)); incapableDialog.show(getSupportFragmentManager(), IncapableDialog.class.getName()); mOriginal.setChecked(false); mOriginal.setColor(Color.WHITE); mOriginalEnable = false; } } } private int countOverMaxSize() { int count = 0; int selectedCount = mSelectedCollection.count(); for (int i = 0; i < selectedCount; i++) { Item item = mSelectedCollection.asList().get(i); if (item.isImage()) { float size = PhotoMetadataUtils.getSizeInMB(item.size); if (size > mSpec.originalMaxSize) { count++; } } } return count; } protected void updateSize(Item item) { if (item.isGif()) { mSize.setVisibility(View.VISIBLE); mSize.setText(PhotoMetadataUtils.getSizeInMB(item.size) + "M"); } else { mSize.setVisibility(View.GONE); } if (item.isVideo()) { mOriginalLayout.setVisibility(View.GONE); } else if (mSpec.originalable) { mOriginalLayout.setVisibility(View.VISIBLE); } } protected void sendBackResult(boolean apply) { Intent intent = new Intent(); intent.putExtra(EXTRA_RESULT_BUNDLE, mSelectedCollection.getDataWithBundle()); intent.putExtra(EXTRA_RESULT_APPLY, apply); intent.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); setResult(Activity.RESULT_OK, intent); } private boolean assertAddSelection(Item item) { IncapableCause cause = mSelectedCollection.isAcceptable(item); IncapableCause.handleCause(this, cause); return cause == null; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui; import android.content.Context; import android.database.Cursor; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.AlbumMediaCollection; import com.zhihu.matisse.internal.model.SelectedItemCollection; import com.zhihu.matisse.internal.ui.adapter.AlbumMediaAdapter; import com.zhihu.matisse.internal.ui.widget.MediaGridInset; import com.zhihu.matisse.internal.utils.UIUtils; public class MediaSelectionFragment extends Fragment implements AlbumMediaCollection.AlbumMediaCallbacks, AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener { public static final String EXTRA_ALBUM = "extra_album"; private final AlbumMediaCollection mAlbumMediaCollection = new AlbumMediaCollection(); private RecyclerView mRecyclerView; private AlbumMediaAdapter mAdapter; private SelectionProvider mSelectionProvider; private AlbumMediaAdapter.CheckStateListener mCheckStateListener; private AlbumMediaAdapter.OnMediaClickListener mOnMediaClickListener; public static MediaSelectionFragment newInstance(Album album) { MediaSelectionFragment fragment = new MediaSelectionFragment(); Bundle args = new Bundle(); args.putParcelable(EXTRA_ALBUM, album); fragment.setArguments(args); return fragment; } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof SelectionProvider) { mSelectionProvider = (SelectionProvider) context; } else { throw new IllegalStateException("Context must implement SelectionProvider."); } if (context instanceof AlbumMediaAdapter.CheckStateListener) { mCheckStateListener = (AlbumMediaAdapter.CheckStateListener) context; } if (context instanceof AlbumMediaAdapter.OnMediaClickListener) { mOnMediaClickListener = (AlbumMediaAdapter.OnMediaClickListener) context; } } @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_media_selection, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerview); } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Album album = getArguments().getParcelable(EXTRA_ALBUM); mAdapter = new AlbumMediaAdapter(getContext(), mSelectionProvider.provideSelectedItemCollection(), mRecyclerView); mAdapter.registerCheckStateListener(this); mAdapter.registerOnMediaClickListener(this); mRecyclerView.setHasFixedSize(true); int spanCount; SelectionSpec selectionSpec = SelectionSpec.getInstance(); if (selectionSpec.gridExpectedSize > 0) { spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize); } else { spanCount = selectionSpec.spanCount; } mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount)); int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing); mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false)); mRecyclerView.setAdapter(mAdapter); mAlbumMediaCollection.onCreate(getActivity(), this); mAlbumMediaCollection.load(album, selectionSpec.capture); } @Override public void onDestroyView() { super.onDestroyView(); mAlbumMediaCollection.onDestroy(); } public void refreshMediaGrid() { mAdapter.notifyDataSetChanged(); } public void refreshSelection() { mAdapter.refreshSelection(); } @Override public void onAlbumMediaLoad(Cursor cursor) { mAdapter.swapCursor(cursor); } @Override public void onAlbumMediaReset() { mAdapter.swapCursor(null); } @Override public void onUpdate() { // notify outer Activity that check state changed if (mCheckStateListener != null) { mCheckStateListener.onUpdate(); } } @Override public void onMediaClick(Album album, Item item, int adapterPosition) { if (mOnMediaClickListener != null) { mOnMediaClickListener.onMediaClick((Album) getArguments().getParcelable(EXTRA_ALBUM), item, adapterPosition); } } public interface SelectionProvider { SelectedItemCollection provideSelectedItemCollection(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.Point; import android.os.Bundle; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import com.zhihu.matisse.listener.OnFragmentInteractionListener; import it.sephiroth.android.library.imagezoom.ImageViewTouch; import it.sephiroth.android.library.imagezoom.ImageViewTouchBase; public class PreviewItemFragment extends Fragment { private static final String ARGS_ITEM = "args_item"; private OnFragmentInteractionListener mListener; public static PreviewItemFragment newInstance(Item item) { PreviewItemFragment fragment = new PreviewItemFragment(); Bundle bundle = new Bundle(); bundle.putParcelable(ARGS_ITEM, item); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_preview_item, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); final Item item = getArguments().getParcelable(ARGS_ITEM); if (item == null) { return; } View videoPlayButton = view.findViewById(R.id.video_play_button); if (item.isVideo()) { videoPlayButton.setVisibility(View.VISIBLE); videoPlayButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(item.uri, "video/*"); try { startActivity(intent); } catch (ActivityNotFoundException e) { Toast.makeText(getContext(), R.string.error_no_video_activity, Toast.LENGTH_SHORT).show(); } } }); } else { videoPlayButton.setVisibility(View.GONE); } ImageViewTouch image = (ImageViewTouch) view.findViewById(R.id.image_view); image.setDisplayType(ImageViewTouchBase.DisplayType.FIT_TO_SCREEN); image.setSingleTapListener(new ImageViewTouch.OnImageViewTouchSingleTapListener() { @Override public void onSingleTapConfirmed() { if (mListener != null) { mListener.onClick(); } } }); Point size = PhotoMetadataUtils.getBitmapSize(item.getContentUri(), getActivity()); if (item.isGif()) { SelectionSpec.getInstance().imageEngine.loadGifImage(getContext(), size.x, size.y, image, item.getContentUri()); } else { SelectionSpec.getInstance().imageEngine.loadImage(getContext(), size.x, size.y, image, item.getContentUri()); } } public void resetView() { if (getView() != null) { ((ImageViewTouch) getView().findViewById(R.id.image_view)).resetMatrix(); } } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnFragmentInteractionListener) { mListener = (OnFragmentInteractionListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui; import android.os.Bundle; import androidx.annotation.Nullable; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.SelectedItemCollection; import java.util.List; public class SelectedPreviewActivity extends BasePreviewActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!SelectionSpec.getInstance().hasInited) { setResult(RESULT_CANCELED); finish(); return; } Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE); List selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION); mAdapter.addAll(selected); mAdapter.notifyDataSetChanged(); if (mSpec.countable) { mCheckView.setCheckedNum(1); } else { mCheckView.setChecked(true); } mPreviousPos = 0; updateSize(selected.get(0)); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.adapter; import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.entity.IncapableCause; import com.zhihu.matisse.internal.model.SelectedItemCollection; import com.zhihu.matisse.internal.ui.widget.CheckView; import com.zhihu.matisse.internal.ui.widget.MediaGrid; public class AlbumMediaAdapter extends RecyclerViewCursorAdapter implements MediaGrid.OnMediaGridClickListener { private static final int VIEW_TYPE_CAPTURE = 0x01; private static final int VIEW_TYPE_MEDIA = 0x02; private final SelectedItemCollection mSelectedCollection; private final Drawable mPlaceholder; private SelectionSpec mSelectionSpec; private CheckStateListener mCheckStateListener; private OnMediaClickListener mOnMediaClickListener; private RecyclerView mRecyclerView; private int mImageResize; public AlbumMediaAdapter(Context context, SelectedItemCollection selectedCollection, RecyclerView recyclerView) { super(null); mSelectionSpec = SelectionSpec.getInstance(); mSelectedCollection = selectedCollection; TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{R.attr.item_placeholder}); mPlaceholder = ta.getDrawable(0); ta.recycle(); mRecyclerView = recyclerView; } @Override public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == VIEW_TYPE_CAPTURE) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.photo_capture_item, parent, false); CaptureViewHolder holder = new CaptureViewHolder(v); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (v.getContext() instanceof OnPhotoCapture) { ((OnPhotoCapture) v.getContext()).capture(); } } }); return holder; } else if (viewType == VIEW_TYPE_MEDIA) { View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.media_grid_item, parent, false); return new MediaViewHolder(v); } return null; } @Override protected void onBindViewHolder(final RecyclerView.ViewHolder holder, Cursor cursor) { if (holder instanceof CaptureViewHolder) { CaptureViewHolder captureViewHolder = (CaptureViewHolder) holder; Drawable[] drawables = captureViewHolder.mHint.getCompoundDrawables(); TypedArray ta = holder.itemView.getContext().getTheme().obtainStyledAttributes( new int[]{R.attr.capture_textColor}); int color = ta.getColor(0, 0); ta.recycle(); for (int i = 0; i < drawables.length; i++) { Drawable drawable = drawables[i]; if (drawable != null) { final Drawable.ConstantState state = drawable.getConstantState(); if (state == null) { continue; } Drawable newDrawable = state.newDrawable().mutate(); newDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); newDrawable.setBounds(drawable.getBounds()); drawables[i] = newDrawable; } } captureViewHolder.mHint.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]); } else if (holder instanceof MediaViewHolder) { MediaViewHolder mediaViewHolder = (MediaViewHolder) holder; final Item item = Item.valueOf(cursor); mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo( getImageResize(mediaViewHolder.mMediaGrid.getContext()), mPlaceholder, mSelectionSpec.countable, holder )); mediaViewHolder.mMediaGrid.bindMedia(item); mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this); setCheckStatus(item, mediaViewHolder.mMediaGrid); } } private void setCheckStatus(Item item, MediaGrid mediaGrid) { if (mSelectionSpec.countable) { int checkedNum = mSelectedCollection.checkedNumOf(item); if (checkedNum > 0) { mediaGrid.setCheckEnabled(true); mediaGrid.setCheckedNum(checkedNum); } else { if (mSelectedCollection.maxSelectableReached()) { mediaGrid.setCheckEnabled(false); mediaGrid.setCheckedNum(CheckView.UNCHECKED); } else { mediaGrid.setCheckEnabled(true); mediaGrid.setCheckedNum(checkedNum); } } } else { boolean selected = mSelectedCollection.isSelected(item); if (selected) { mediaGrid.setCheckEnabled(true); mediaGrid.setChecked(true); } else { if (mSelectedCollection.maxSelectableReached()) { mediaGrid.setCheckEnabled(false); mediaGrid.setChecked(false); } else { mediaGrid.setCheckEnabled(true); mediaGrid.setChecked(false); } } } } @Override public void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder) { if (mSelectionSpec.showPreview) { if (mOnMediaClickListener != null) { mOnMediaClickListener.onMediaClick(null, item, holder.getAdapterPosition()); } } else { updateSelectedItem(item, holder); } } @Override public void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder) { updateSelectedItem(item, holder); } private void updateSelectedItem(Item item, RecyclerView.ViewHolder holder) { if (mSelectionSpec.countable) { int checkedNum = mSelectedCollection.checkedNumOf(item); if (checkedNum == CheckView.UNCHECKED) { if (assertAddSelection(holder.itemView.getContext(), item)) { mSelectedCollection.add(item); notifyCheckStateChanged(); } } else { mSelectedCollection.remove(item); notifyCheckStateChanged(); } } else { if (mSelectedCollection.isSelected(item)) { mSelectedCollection.remove(item); notifyCheckStateChanged(); } else { if (assertAddSelection(holder.itemView.getContext(), item)) { mSelectedCollection.add(item); notifyCheckStateChanged(); } } } } private void notifyCheckStateChanged() { notifyDataSetChanged(); if (mCheckStateListener != null) { mCheckStateListener.onUpdate(); } } @Override public int getItemViewType(int position, Cursor cursor) { return Item.valueOf(cursor).isCapture() ? VIEW_TYPE_CAPTURE : VIEW_TYPE_MEDIA; } private boolean assertAddSelection(Context context, Item item) { IncapableCause cause = mSelectedCollection.isAcceptable(item); IncapableCause.handleCause(context, cause); return cause == null; } public void registerCheckStateListener(CheckStateListener listener) { mCheckStateListener = listener; } public void unregisterCheckStateListener() { mCheckStateListener = null; } public void registerOnMediaClickListener(OnMediaClickListener listener) { mOnMediaClickListener = listener; } public void unregisterOnMediaClickListener() { mOnMediaClickListener = null; } public void refreshSelection() { GridLayoutManager layoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager(); int first = layoutManager.findFirstVisibleItemPosition(); int last = layoutManager.findLastVisibleItemPosition(); if (first == -1 || last == -1) { return; } Cursor cursor = getCursor(); for (int i = first; i <= last; i++) { RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(first); if (holder instanceof MediaViewHolder) { if (cursor.moveToPosition(i)) { setCheckStatus(Item.valueOf(cursor), ((MediaViewHolder) holder).mMediaGrid); } } } } private int getImageResize(Context context) { if (mImageResize == 0) { RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager(); int spanCount = ((GridLayoutManager) lm).getSpanCount(); int screenWidth = context.getResources().getDisplayMetrics().widthPixels; int availableWidth = screenWidth - context.getResources().getDimensionPixelSize( R.dimen.media_grid_spacing) * (spanCount - 1); mImageResize = availableWidth / spanCount; mImageResize = (int) (mImageResize * mSelectionSpec.thumbnailScale); } return mImageResize; } public interface CheckStateListener { void onUpdate(); } public interface OnMediaClickListener { void onMediaClick(Album album, Item item, int adapterPosition); } public interface OnPhotoCapture { void capture(); } private static class MediaViewHolder extends RecyclerView.ViewHolder { private MediaGrid mMediaGrid; MediaViewHolder(View itemView) { super(itemView); mMediaGrid = (MediaGrid) itemView; } } private static class CaptureViewHolder extends RecyclerView.ViewHolder { private TextView mHint; CaptureViewHolder(View itemView) { super(itemView); mHint = (TextView) itemView.findViewById(R.id.hint); } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumsAdapter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.adapter; import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.drawable.Drawable; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; import android.widget.ImageView; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.SelectionSpec; public class AlbumsAdapter extends CursorAdapter { private final Drawable mPlaceholder; public AlbumsAdapter(Context context, Cursor c, boolean autoRequery) { super(context, c, autoRequery); TypedArray ta = context.getTheme().obtainStyledAttributes( new int[]{R.attr.album_thumbnail_placeholder}); mPlaceholder = ta.getDrawable(0); ta.recycle(); } public AlbumsAdapter(Context context, Cursor c, int flags) { super(context, c, flags); TypedArray ta = context.getTheme().obtainStyledAttributes( new int[]{R.attr.album_thumbnail_placeholder}); mPlaceholder = ta.getDrawable(0); ta.recycle(); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context).inflate(R.layout.album_list_item, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { Album album = Album.valueOf(cursor); ((TextView) view.findViewById(R.id.album_name)).setText(album.getDisplayName(context)); ((TextView) view.findViewById(R.id.album_media_count)).setText(String.valueOf(album.getCount())); // do not need to load animated Gif SelectionSpec.getInstance().imageEngine.loadThumbnail(context, context.getResources().getDimensionPixelSize(R .dimen.media_grid_size), mPlaceholder, (ImageView) view.findViewById(R.id.album_cover), album.getCoverUri()); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.adapter; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentPagerAdapter; import android.view.ViewGroup; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.ui.PreviewItemFragment; import java.util.ArrayList; import java.util.List; public class PreviewPagerAdapter extends FragmentPagerAdapter { private ArrayList mItems = new ArrayList<>(); private OnPrimaryItemSetListener mListener; public PreviewPagerAdapter(FragmentManager manager, OnPrimaryItemSetListener listener) { super(manager); mListener = listener; } @Override public Fragment getItem(int position) { return PreviewItemFragment.newInstance(mItems.get(position)); } @Override public int getCount() { return mItems.size(); } @Override public void setPrimaryItem(ViewGroup container, int position, Object object) { super.setPrimaryItem(container, position, object); if (mListener != null) { mListener.onPrimaryItemSet(position); } } public Item getMediaItem(int position) { return mItems.get(position); } public void addAll(List items) { mItems.addAll(items); } interface OnPrimaryItemSetListener { void onPrimaryItemSet(int position); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.adapter; import android.database.Cursor; import android.provider.MediaStore; import androidx.recyclerview.widget.RecyclerView; public abstract class RecyclerViewCursorAdapter extends RecyclerView.Adapter { private Cursor mCursor; private int mRowIDColumn; RecyclerViewCursorAdapter(Cursor c) { setHasStableIds(true); swapCursor(c); } protected abstract void onBindViewHolder(VH holder, Cursor cursor); @Override public void onBindViewHolder(VH holder, int position) { if (!isDataValid(mCursor)) { throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state."); } if (!mCursor.moveToPosition(position)) { throw new IllegalStateException("Could not move cursor to position " + position + " when trying to bind view holder"); } onBindViewHolder(holder, mCursor); } @Override public int getItemViewType(int position) { if (!mCursor.moveToPosition(position)) { throw new IllegalStateException("Could not move cursor to position " + position + " when trying to get item view type."); } return getItemViewType(position, mCursor); } protected abstract int getItemViewType(int position, Cursor cursor); @Override public int getItemCount() { if (isDataValid(mCursor)) { return mCursor.getCount(); } else { return 0; } } @Override public long getItemId(int position) { if (!isDataValid(mCursor)) { throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state."); } if (!mCursor.moveToPosition(position)) { throw new IllegalStateException("Could not move cursor to position " + position + " when trying to get an item id"); } return mCursor.getLong(mRowIDColumn); } public void swapCursor(Cursor newCursor) { if (newCursor == mCursor) { return; } if (newCursor != null) { mCursor = newCursor; mRowIDColumn = mCursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID); // notify the observers about the new cursor notifyDataSetChanged(); } else { notifyItemRangeRemoved(0, getItemCount()); mCursor = null; mRowIDColumn = -1; } } public Cursor getCursor() { return mCursor; } private boolean isDataValid(Cursor cursor) { return cursor != null && !cursor.isClosed(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.appcompat.widget.ListPopupWindow; import android.view.View; import android.widget.AdapterView; import android.widget.CursorAdapter; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.utils.Platform; public class AlbumsSpinner { private static final int MAX_SHOWN_COUNT = 6; private CursorAdapter mAdapter; private TextView mSelected; private ListPopupWindow mListPopupWindow; private AdapterView.OnItemSelectedListener mOnItemSelectedListener; public AlbumsSpinner(@NonNull Context context) { mListPopupWindow = new ListPopupWindow(context, null, R.attr.listPopupWindowStyle); mListPopupWindow.setModal(true); float density = context.getResources().getDisplayMetrics().density; mListPopupWindow.setContentWidth((int) (216 * density)); mListPopupWindow.setHorizontalOffset((int) (16 * density)); mListPopupWindow.setVerticalOffset((int) (-48 * density)); mListPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { AlbumsSpinner.this.onItemSelected(parent.getContext(), position); if (mOnItemSelectedListener != null) { mOnItemSelectedListener.onItemSelected(parent, view, position, id); } } }); } public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) { mOnItemSelectedListener = listener; } public void setSelection(Context context, int position) { mListPopupWindow.setSelection(position); onItemSelected(context, position); } private void onItemSelected(Context context, int position) { mListPopupWindow.dismiss(); Cursor cursor = mAdapter.getCursor(); cursor.moveToPosition(position); Album album = Album.valueOf(cursor); String displayName = album.getDisplayName(context); if (mSelected.getVisibility() == View.VISIBLE) { mSelected.setText(displayName); } else { if (Platform.hasICS()) { mSelected.setAlpha(0.0f); mSelected.setVisibility(View.VISIBLE); mSelected.setText(displayName); mSelected.animate().alpha(1.0f).setDuration(context.getResources().getInteger( android.R.integer.config_longAnimTime)).start(); } else { mSelected.setVisibility(View.VISIBLE); mSelected.setText(displayName); } } } public void setAdapter(CursorAdapter adapter) { mListPopupWindow.setAdapter(adapter); mAdapter = adapter; } public void setSelectedTextView(TextView textView) { mSelected = textView; // tint dropdown arrow icon Drawable[] drawables = mSelected.getCompoundDrawables(); Drawable right = drawables[2]; TypedArray ta = mSelected.getContext().getTheme().obtainStyledAttributes( new int[]{R.attr.album_element_color}); int color = ta.getColor(0, 0); ta.recycle(); right.setColorFilter(color, PorterDuff.Mode.SRC_IN); mSelected.setVisibility(View.GONE); mSelected.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { int itemHeight = v.getResources().getDimensionPixelSize(R.dimen.album_item_height); mListPopupWindow.setHeight( mAdapter.getCount() > MAX_SHOWN_COUNT ? itemHeight * MAX_SHOWN_COUNT : itemHeight * mAdapter.getCount()); mListPopupWindow.show(); } }); mSelected.setOnTouchListener(mListPopupWindow.createDragToOpenListener(mSelected)); } public void setPopupAnchorView(View view) { mListPopupWindow.setAnchorView(view); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java ================================================ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import androidx.core.content.res.ResourcesCompat; import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; import com.zhihu.matisse.R; public class CheckRadioView extends AppCompatImageView { private Drawable mDrawable; private int mSelectedColor; private int mUnSelectUdColor; public CheckRadioView(Context context) { super(context); init(); } public CheckRadioView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { mSelectedColor = ResourcesCompat.getColor( getResources(), R.color.zhihu_item_checkCircle_backgroundColor, getContext().getTheme()); mUnSelectUdColor = ResourcesCompat.getColor( getResources(), R.color.zhihu_check_original_radio_disable, getContext().getTheme()); setChecked(false); } public void setChecked(boolean enable) { if (enable) { setImageResource(R.drawable.ic_preview_radio_on); mDrawable = getDrawable(); mDrawable.setColorFilter(mSelectedColor, PorterDuff.Mode.SRC_IN); } else { setImageResource(R.drawable.ic_preview_radio_off); mDrawable = getDrawable(); mDrawable.setColorFilter(mUnSelectUdColor, PorterDuff.Mode.SRC_IN); } } public void setColor(int color) { if (mDrawable == null) { mDrawable = getDrawable(); } mDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; import androidx.core.content.res.ResourcesCompat; import android.text.TextPaint; import android.util.AttributeSet; import android.view.View; import com.zhihu.matisse.R; public class CheckView extends View { public static final int UNCHECKED = Integer.MIN_VALUE; private static final float STROKE_WIDTH = 3.0f; // dp private static final float SHADOW_WIDTH = 6.0f; // dp private static final int SIZE = 48; // dp private static final float STROKE_RADIUS = 11.5f; // dp private static final float BG_RADIUS = 11.0f; // dp private static final int CONTENT_SIZE = 16; // dp private boolean mCountable; private boolean mChecked; private int mCheckedNum; private Paint mStrokePaint; private Paint mBackgroundPaint; private TextPaint mTextPaint; private Paint mShadowPaint; private Drawable mCheckDrawable; private float mDensity; private Rect mCheckRect; private boolean mEnabled = true; public CheckView(Context context) { super(context); init(context); } public CheckView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CheckView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // fixed size 48dp x 48dp int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY); super.onMeasure(sizeSpec, sizeSpec); } private void init(Context context) { mDensity = context.getResources().getDisplayMetrics().density; mStrokePaint = new Paint(); mStrokePaint.setAntiAlias(true); mStrokePaint.setStyle(Paint.Style.STROKE); mStrokePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER)); mStrokePaint.setStrokeWidth(STROKE_WIDTH * mDensity); TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{R.attr.item_checkCircle_borderColor}); int defaultColor = ResourcesCompat.getColor( getResources(), R.color.zhihu_item_checkCircle_borderColor, getContext().getTheme()); int color = ta.getColor(0, defaultColor); ta.recycle(); mStrokePaint.setColor(color); mCheckDrawable = ResourcesCompat.getDrawable(context.getResources(), R.drawable.ic_check_white_18dp, context.getTheme()); } public void setChecked(boolean checked) { if (mCountable) { throw new IllegalStateException("CheckView is countable, call setCheckedNum() instead."); } mChecked = checked; invalidate(); } public void setCountable(boolean countable) { mCountable = countable; } public void setCheckedNum(int checkedNum) { if (!mCountable) { throw new IllegalStateException("CheckView is not countable, call setChecked() instead."); } if (checkedNum != UNCHECKED && checkedNum <= 0) { throw new IllegalArgumentException("checked num can't be negative."); } mCheckedNum = checkedNum; invalidate(); } public void setEnabled(boolean enabled) { if (mEnabled != enabled) { mEnabled = enabled; invalidate(); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // draw outer and inner shadow initShadowPaint(); canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint); // draw white stroke canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, STROKE_RADIUS * mDensity, mStrokePaint); // draw content if (mCountable) { if (mCheckedNum != UNCHECKED) { initBackgroundPaint(); canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, BG_RADIUS * mDensity, mBackgroundPaint); initTextPaint(); String text = String.valueOf(mCheckedNum); int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2; int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2; canvas.drawText(text, baseX, baseY, mTextPaint); } } else { if (mChecked) { initBackgroundPaint(); canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, BG_RADIUS * mDensity, mBackgroundPaint); mCheckDrawable.setBounds(getCheckRect()); mCheckDrawable.draw(canvas); } } // enable hint setAlpha(mEnabled ? 1.0f : 0.5f); } private void initShadowPaint() { if (mShadowPaint == null) { mShadowPaint = new Paint(); mShadowPaint.setAntiAlias(true); // all in dp float outerRadius = STROKE_RADIUS + STROKE_WIDTH / 2; float innerRadius = outerRadius - STROKE_WIDTH; float gradientRadius = outerRadius + SHADOW_WIDTH; float stop0 = (innerRadius - SHADOW_WIDTH) / gradientRadius; float stop1 = innerRadius / gradientRadius; float stop2 = outerRadius / gradientRadius; float stop3 = 1.0f; mShadowPaint.setShader( new RadialGradient((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2, gradientRadius * mDensity, new int[]{Color.parseColor("#00000000"), Color.parseColor("#0D000000"), Color.parseColor("#0D000000"), Color.parseColor("#00000000")}, new float[]{stop0, stop1, stop2, stop3}, Shader.TileMode.CLAMP)); } } private void initBackgroundPaint() { if (mBackgroundPaint == null) { mBackgroundPaint = new Paint(); mBackgroundPaint.setAntiAlias(true); mBackgroundPaint.setStyle(Paint.Style.FILL); TypedArray ta = getContext().getTheme() .obtainStyledAttributes(new int[]{R.attr.item_checkCircle_backgroundColor}); int defaultColor = ResourcesCompat.getColor( getResources(), R.color.zhihu_item_checkCircle_backgroundColor, getContext().getTheme()); int color = ta.getColor(0, defaultColor); ta.recycle(); mBackgroundPaint.setColor(color); } } private void initTextPaint() { if (mTextPaint == null) { mTextPaint = new TextPaint(); mTextPaint.setAntiAlias(true); mTextPaint.setColor(Color.WHITE); mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); mTextPaint.setTextSize(12.0f * mDensity); } } // rect for drawing checked number or mark private Rect getCheckRect() { if (mCheckRect == null) { int rectPadding = (int) (SIZE * mDensity / 2 - CONTENT_SIZE * mDensity / 2); mCheckRect = new Rect(rectPadding, rectPadding, (int) (SIZE * mDensity - rectPadding), (int) (SIZE * mDensity - rectPadding)); } return mCheckRect; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import androidx.appcompat.app.AlertDialog; import android.text.TextUtils; import com.zhihu.matisse.R; public class IncapableDialog extends DialogFragment { public static final String EXTRA_TITLE = "extra_title"; public static final String EXTRA_MESSAGE = "extra_message"; public static IncapableDialog newInstance(String title, String message) { IncapableDialog dialog = new IncapableDialog(); Bundle args = new Bundle(); args.putString(EXTRA_TITLE, title); args.putString(EXTRA_MESSAGE, message); dialog.setArguments(args); return dialog; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { String title = getArguments().getString(EXTRA_TITLE); String message = getArguments().getString(EXTRA_MESSAGE); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); if (!TextUtils.isEmpty(title)) { builder.setTitle(title); } if (!TextUtils.isEmpty(message)) { builder.setMessage(message); } builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); return builder.create(); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.graphics.drawable.Drawable; import androidx.recyclerview.widget.RecyclerView; import android.text.format.DateUtils; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; public class MediaGrid extends SquareFrameLayout implements View.OnClickListener { private ImageView mThumbnail; private CheckView mCheckView; private ImageView mGifTag; private TextView mVideoDuration; private Item mMedia; private PreBindInfo mPreBindInfo; private OnMediaGridClickListener mListener; public MediaGrid(Context context) { super(context); init(context); } public MediaGrid(Context context, AttributeSet attrs) { super(context, attrs); init(context); } private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.media_grid_content, this, true); mThumbnail = (ImageView) findViewById(R.id.media_thumbnail); mCheckView = (CheckView) findViewById(R.id.check_view); mGifTag = (ImageView) findViewById(R.id.gif); mVideoDuration = (TextView) findViewById(R.id.video_duration); mThumbnail.setOnClickListener(this); mCheckView.setOnClickListener(this); } @Override public void onClick(View v) { if (mListener != null) { if (v == mThumbnail) { mListener.onThumbnailClicked(mThumbnail, mMedia, mPreBindInfo.mViewHolder); } else if (v == mCheckView) { mListener.onCheckViewClicked(mCheckView, mMedia, mPreBindInfo.mViewHolder); } } } public void preBindMedia(PreBindInfo info) { mPreBindInfo = info; } public void bindMedia(Item item) { mMedia = item; setGifTag(); initCheckView(); setImage(); setVideoDuration(); } public Item getMedia() { return mMedia; } private void setGifTag() { mGifTag.setVisibility(mMedia.isGif() ? View.VISIBLE : View.GONE); } private void initCheckView() { mCheckView.setCountable(mPreBindInfo.mCheckViewCountable); } public void setCheckEnabled(boolean enabled) { mCheckView.setEnabled(enabled); } public void setCheckedNum(int checkedNum) { mCheckView.setCheckedNum(checkedNum); } public void setChecked(boolean checked) { mCheckView.setChecked(checked); } private void setImage() { if (mMedia.isGif()) { SelectionSpec.getInstance().imageEngine.loadGifThumbnail(getContext(), mPreBindInfo.mResize, mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri()); } else { SelectionSpec.getInstance().imageEngine.loadThumbnail(getContext(), mPreBindInfo.mResize, mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri()); } } private void setVideoDuration() { if (mMedia.isVideo()) { mVideoDuration.setVisibility(VISIBLE); mVideoDuration.setText(DateUtils.formatElapsedTime(mMedia.duration / 1000)); } else { mVideoDuration.setVisibility(GONE); } } public void setOnMediaGridClickListener(OnMediaGridClickListener listener) { mListener = listener; } public void removeOnMediaGridClickListener() { mListener = null; } public interface OnMediaGridClickListener { void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder); void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder); } public static class PreBindInfo { int mResize; Drawable mPlaceholder; boolean mCheckViewCountable; RecyclerView.ViewHolder mViewHolder; public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable, RecyclerView.ViewHolder viewHolder) { mResize = resize; mPlaceholder = placeholder; mCheckViewCountable = checkViewCountable; mViewHolder = viewHolder; } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.graphics.Rect; import androidx.recyclerview.widget.RecyclerView; import android.view.View; public class MediaGridInset extends RecyclerView.ItemDecoration { private int mSpanCount; private int mSpacing; private boolean mIncludeEdge; public MediaGridInset(int spanCount, int spacing, boolean includeEdge) { this.mSpanCount = spanCount; this.mSpacing = spacing; this.mIncludeEdge = includeEdge; } @Override public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { int position = parent.getChildAdapterPosition(view); // item position int column = position % mSpanCount; // item column if (mIncludeEdge) { // spacing - column * ((1f / spanCount) * spacing) outRect.left = mSpacing - column * mSpacing / mSpanCount; // (column + 1) * ((1f / spanCount) * spacing) outRect.right = (column + 1) * mSpacing / mSpanCount; if (position < mSpanCount) { // top edge outRect.top = mSpacing; } outRect.bottom = mSpacing; // item bottom } else { // column * ((1f / spanCount) * spacing) outRect.left = column * mSpacing / mSpanCount; // spacing - (column + 1) * ((1f / spanCount) * spacing) outRect.right = mSpacing - (column + 1) * mSpacing / mSpanCount; if (position >= mSpanCount) { outRect.top = mSpacing; // item top } } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import androidx.viewpager.widget.ViewPager; import android.util.AttributeSet; import android.view.View; import it.sephiroth.android.library.imagezoom.ImageViewTouch; public class PreviewViewPager extends ViewPager { public PreviewViewPager(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { if (v instanceof ImageViewTouch) { return ((ImageViewTouch) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y); } return super.canScroll(v, checkV, dx, x, y); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.graphics.Canvas; import android.graphics.Path; import android.graphics.RectF; import androidx.appcompat.widget.AppCompatImageView; import android.util.AttributeSet; public class RoundedRectangleImageView extends AppCompatImageView { private float mRadius; // dp private Path mRoundedRectPath; private RectF mRectF; public RoundedRectangleImageView(Context context) { super(context); init(context); } public RoundedRectangleImageView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public RoundedRectangleImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { float density = context.getResources().getDisplayMetrics().density; mRadius = 2.0f * density; mRoundedRectPath = new Path(); mRectF = new RectF(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mRectF.set(0.0f, 0.0f, getMeasuredWidth(), getMeasuredHeight()); mRoundedRectPath.addRoundRect(mRectF, mRadius, mRadius, Path.Direction.CW); } @Override protected void onDraw(Canvas canvas) { canvas.clipPath(mRoundedRectPath); super.onDraw(canvas); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareFrameLayout.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; import android.util.AttributeSet; import android.widget.FrameLayout; public class SquareFrameLayout extends FrameLayout { public SquareFrameLayout(Context context) { super(context); } public SquareFrameLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, widthMeasureSpec); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/ExifInterfaceCompat.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.utils; import android.media.ExifInterface; import android.text.TextUtils; import android.util.Log; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; /** * Bug fixture for ExifInterface constructor. */ final class ExifInterfaceCompat { private static final String TAG = ExifInterfaceCompat.class.getSimpleName(); private static final int EXIF_DEGREE_FALLBACK_VALUE = -1; /** * Do not instantiate this class. */ private ExifInterfaceCompat() { } /** * Creates new instance of {@link ExifInterface}. * Original constructor won't check filename value, so if null value has been passed, * the process will be killed because of SIGSEGV. * Google Play crash report system cannot perceive this crash, so this method will throw * {@link NullPointerException} when the filename is null. * * @param filename a JPEG filename. * @return {@link ExifInterface} instance. * @throws IOException something wrong with I/O. */ public static ExifInterface newInstance(String filename) throws IOException { if (filename == null) throw new NullPointerException("filename should not be null"); return new ExifInterface(filename); } private static Date getExifDateTime(String filepath) { ExifInterface exif; try { // ExifInterface does not check whether file path is null or not, // so passing null file path argument to its constructor causing SIGSEGV. // We should avoid such a situation by checking file path string. exif = newInstance(filepath); } catch (IOException ex) { Log.e(TAG, "cannot read exif", ex); return null; } String date = exif.getAttribute(ExifInterface.TAG_DATETIME); if (TextUtils.isEmpty(date)) { return null; } try { SimpleDateFormat formatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss"); formatter.setTimeZone(TimeZone.getTimeZone("UTC")); return formatter.parse(date); } catch (ParseException e) { Log.d(TAG, "failed to parse date taken", e); } return null; } /** * Read exif info and get datetime value of the photo. * * @param filepath to get datetime * @return when a photo taken. */ public static long getExifDateTimeInMillis(String filepath) { Date datetime = getExifDateTime(filepath); if (datetime == null) { return -1; } return datetime.getTime(); } /** * Read exif info and get orientation value of the photo. * * @param filepath to get exif. * @return exif orientation value */ public static int getExifOrientation(String filepath) { ExifInterface exif; try { // ExifInterface does not check whether file path is null or not, // so passing null file path argument to its constructor causing SIGSEGV. // We should avoid such a situation by checking file path string. exif = newInstance(filepath); } catch (IOException ex) { Log.e(TAG, "cannot read exif", ex); return EXIF_DEGREE_FALLBACK_VALUE; } int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, EXIF_DEGREE_FALLBACK_VALUE); if (orientation == EXIF_DEGREE_FALLBACK_VALUE) { return 0; } // We only recognize a subset of orientation tag values. switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: return 90; case ExifInterface.ORIENTATION_ROTATE_180: return 180; case ExifInterface.ORIENTATION_ROTATE_270: return 270; default: return 0; } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.utils; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.MediaStore; import androidx.fragment.app.Fragment; import androidx.core.content.FileProvider; import androidx.core.os.EnvironmentCompat; import com.zhihu.matisse.internal.entity.CaptureStrategy; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Locale; public class MediaStoreCompat { private final WeakReference mContext; private final WeakReference mFragment; private CaptureStrategy mCaptureStrategy; private Uri mCurrentPhotoUri; private String mCurrentPhotoPath; public MediaStoreCompat(Activity activity) { mContext = new WeakReference<>(activity); mFragment = null; } public MediaStoreCompat(Activity activity, Fragment fragment) { mContext = new WeakReference<>(activity); mFragment = new WeakReference<>(fragment); } /** * Checks whether the device has a camera feature or not. * * @param context a context to check for camera feature. * @return true if the device has a camera feature. false otherwise. */ public static boolean hasCameraFeature(Context context) { PackageManager pm = context.getApplicationContext().getPackageManager(); return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA); } public void setCaptureStrategy(CaptureStrategy strategy) { mCaptureStrategy = strategy; } public void dispatchCaptureIntent(Context context, int requestCode) { Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (captureIntent.resolveActivity(context.getPackageManager()) != null) { File photoFile = null; try { photoFile = createImageFile(); } catch (IOException e) { e.printStackTrace(); } if (photoFile != null) { mCurrentPhotoPath = photoFile.getAbsolutePath(); mCurrentPhotoUri = FileProvider.getUriForFile(mContext.get(), mCaptureStrategy.authority, photoFile); captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCurrentPhotoUri); captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { List resInfoList = context.getPackageManager() .queryIntentActivities(captureIntent, PackageManager.MATCH_DEFAULT_ONLY); for (ResolveInfo resolveInfo : resInfoList) { String packageName = resolveInfo.activityInfo.packageName; context.grantUriPermission(packageName, mCurrentPhotoUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } } if (mFragment != null) { mFragment.get().startActivityForResult(captureIntent, requestCode); } else { mContext.get().startActivityForResult(captureIntent, requestCode); } } } } @SuppressWarnings("ResultOfMethodCallIgnored") private File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date()); String imageFileName = String.format("JPEG_%s.jpg", timeStamp); File storageDir; if (mCaptureStrategy.isPublic) { storageDir = Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES); if (!storageDir.exists()) storageDir.mkdirs(); } else { storageDir = mContext.get().getExternalFilesDir(Environment.DIRECTORY_PICTURES); } if (mCaptureStrategy.directory != null) { storageDir = new File(storageDir, mCaptureStrategy.directory); if (!storageDir.exists()) storageDir.mkdirs(); } // Avoid joining path components manually File tempFile = new File(storageDir, imageFileName); // Handle the situation that user's external storage is not ready if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) { return null; } return tempFile; } public Uri getCurrentPhotoUri() { return mCurrentPhotoUri; } public String getCurrentPhotoPath() { return mCurrentPhotoPath; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/PathUtils.java ================================================ package com.zhihu.matisse.internal.utils; import android.annotation.TargetApi; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; /** * http://stackoverflow.com/a/27271131/4739220 */ public class PathUtils { /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and * other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @author paulburke */ @TargetApi(Build.VERSION_CODES.KITKAT) public static String getPath(final Context context, final Uri uri) { // DocumentProvider if (Platform.hasKitKat() && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider if (isExternalStorageDocument(uri)) { final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory() + "/" + split[1]; } // TODO handle non-primary volumes } else if (isDownloadsDocument(uri)) { // DownloadsProvider final String id = DocumentsContract.getDocumentId(uri); final Uri contentUri = ContentUris.withAppendedId( Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); return getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { // MediaProvider final String docId = DocumentsContract.getDocumentId(uri); final String[] split = docId.split(":"); final String type = split[0]; Uri contentUri = null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } final String selection = "_id=?"; final String[] selectionArgs = new String[]{ split[1] }; return getDataColumn(context, contentUri, selection, selectionArgs); } } else if ("content".equalsIgnoreCase(uri.getScheme())) { // MediaStore (and general) return getDataColumn(context, uri, null, null); } else if ("file".equalsIgnoreCase(uri.getScheme())) { // File return uri.getPath(); } return null; } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ public static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { Cursor cursor = null; final String column = "_data"; final String[] projection = { column }; try { cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { final int columnIndex = cursor.getColumnIndexOrThrow(column); return cursor.getString(columnIndex); } } finally { if (cursor != null) cursor.close(); } return null; } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ public static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ public static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ public static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/PhotoMetadataUtils.java ================================================ /* * Copyright (C) 2014 nohana, Inc. * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.utils; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.graphics.BitmapFactory; import android.graphics.Point; import android.media.ExifInterface; import android.net.Uri; import android.provider.MediaStore; import android.util.DisplayMetrics; import android.util.Log; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.R; import com.zhihu.matisse.filter.Filter; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.entity.IncapableCause; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.Locale; public final class PhotoMetadataUtils { private static final String TAG = PhotoMetadataUtils.class.getSimpleName(); private static final int MAX_WIDTH = 1600; private static final String SCHEME_CONTENT = "content"; private PhotoMetadataUtils() { throw new AssertionError("oops! the utility class is about to be instantiated..."); } public static int getPixelsCount(ContentResolver resolver, Uri uri) { Point size = getBitmapBound(resolver, uri); return size.x * size.y; } public static Point getBitmapSize(Uri uri, Activity activity) { ContentResolver resolver = activity.getContentResolver(); Point imageSize = getBitmapBound(resolver, uri); int w = imageSize.x; int h = imageSize.y; if (PhotoMetadataUtils.shouldRotate(resolver, uri)) { w = imageSize.y; h = imageSize.x; } if (h == 0) return new Point(MAX_WIDTH, MAX_WIDTH); DisplayMetrics metrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); float screenWidth = (float) metrics.widthPixels; float screenHeight = (float) metrics.heightPixels; float widthScale = screenWidth / w; float heightScale = screenHeight / h; if (widthScale > heightScale) { return new Point((int) (w * widthScale), (int) (h * heightScale)); } return new Point((int) (w * widthScale), (int) (h * heightScale)); } public static Point getBitmapBound(ContentResolver resolver, Uri uri) { InputStream is = null; try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; is = resolver.openInputStream(uri); BitmapFactory.decodeStream(is, null, options); int width = options.outWidth; int height = options.outHeight; return new Point(width, height); } catch (FileNotFoundException e) { return new Point(0, 0); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } public static String getPath(ContentResolver resolver, Uri uri) { if (uri == null) { return null; } if (SCHEME_CONTENT.equals(uri.getScheme())) { Cursor cursor = null; try { cursor = resolver.query(uri, new String[]{MediaStore.Images.ImageColumns.DATA}, null, null, null); if (cursor == null || !cursor.moveToFirst()) { return null; } return cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); } finally { if (cursor != null) { cursor.close(); } } } return uri.getPath(); } public static IncapableCause isAcceptable(Context context, Item item) { if (!isSelectableType(context, item)) { return new IncapableCause(context.getString(R.string.error_file_type)); } if (SelectionSpec.getInstance().filters != null) { for (Filter filter : SelectionSpec.getInstance().filters) { IncapableCause incapableCause = filter.filter(context, item); if (incapableCause != null) { return incapableCause; } } } return null; } private static boolean isSelectableType(Context context, Item item) { if (context == null) { return false; } ContentResolver resolver = context.getContentResolver(); for (MimeType type : SelectionSpec.getInstance().mimeTypeSet) { if (type.checkType(resolver, item.getContentUri())) { return true; } } return false; } private static boolean shouldRotate(ContentResolver resolver, Uri uri) { ExifInterface exif; try { exif = ExifInterfaceCompat.newInstance(getPath(resolver, uri)); } catch (IOException e) { Log.e(TAG, "could not read exif info of the image: " + uri); return false; } int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1); return orientation == ExifInterface.ORIENTATION_ROTATE_90 || orientation == ExifInterface.ORIENTATION_ROTATE_270; } public static float getSizeInMB(long sizeInBytes) { DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US); df.applyPattern("0.0"); String result = df.format((float) sizeInBytes / 1024 / 1024); Log.e(TAG, "getSizeInMB: " + result); result = result.replaceAll(",", "."); // in some case , 0.0 will be 0,0 return Float.valueOf(result); } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/Platform.java ================================================ package com.zhihu.matisse.internal.utils; import android.os.Build; /** * @author JoongWon Baik */ public class Platform { public static boolean hasICS() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; } public static boolean hasKitKat() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/SingleMediaScanner.java ================================================ package com.zhihu.matisse.internal.utils; import android.content.Context; import android.media.MediaScannerConnection; import android.net.Uri; /** * @author 工藤 * @email gougou@16fan.com * create at 2018年10月23日12:17:59 * description:媒体扫描 */ public class SingleMediaScanner implements MediaScannerConnection.MediaScannerConnectionClient { private MediaScannerConnection mMsc; private String mPath; private ScanListener mListener; public interface ScanListener { /** * scan finish */ void onScanFinish(); } public SingleMediaScanner(Context context, String mPath, ScanListener mListener) { this.mPath = mPath; this.mListener = mListener; this.mMsc = new MediaScannerConnection(context, this); this.mMsc.connect(); } @Override public void onMediaScannerConnected() { mMsc.scanFile(mPath, null); } @Override public void onScanCompleted(String mPath, Uri mUri) { mMsc.disconnect(); if (mListener != null) { mListener.onScanFinish(); } } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.internal.utils; import android.content.Context; public class UIUtils { public static int spanCount(Context context, int gridExpectedSize) { int screenWidth = context.getResources().getDisplayMetrics().widthPixels; float expected = (float) screenWidth / (float) gridExpectedSize; int spanCount = Math.round(expected); if (spanCount == 0) { spanCount = 1; } return spanCount; } } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/listener/OnCheckedListener.java ================================================ package com.zhihu.matisse.listener; /** * when original is enabled , callback immediately when user check or uncheck original. */ public interface OnCheckedListener { void onCheck(boolean isChecked); } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/listener/OnFragmentInteractionListener.java ================================================ package com.zhihu.matisse.listener; /** * PreViewItemFragment 和 BasePreViewActivity 通信的接口 ,为了方便拿到 ImageViewTouch 的点击事件 */ public interface OnFragmentInteractionListener { /** * ImageViewTouch 被点击了 */ void onClick(); } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.listener; import android.net.Uri; import androidx.annotation.NonNull; import java.util.List; public interface OnSelectedListener { /** * @param uriList the selected item {@link Uri} list. * @param pathList the selected item file path list. */ void onSelected(@NonNull List uriList, @NonNull List pathList); } ================================================ FILE: matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.ui; import android.app.Activity; import android.content.Intent; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import android.util.Log; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.LinearLayout; import android.widget.TextView; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.AlbumCollection; import com.zhihu.matisse.internal.model.SelectedItemCollection; import com.zhihu.matisse.internal.ui.AlbumPreviewActivity; import com.zhihu.matisse.internal.ui.BasePreviewActivity; import com.zhihu.matisse.internal.ui.MediaSelectionFragment; import com.zhihu.matisse.internal.ui.SelectedPreviewActivity; import com.zhihu.matisse.internal.ui.adapter.AlbumMediaAdapter; import com.zhihu.matisse.internal.ui.adapter.AlbumsAdapter; import com.zhihu.matisse.internal.ui.widget.AlbumsSpinner; import com.zhihu.matisse.internal.ui.widget.CheckRadioView; import com.zhihu.matisse.internal.ui.widget.IncapableDialog; import com.zhihu.matisse.internal.utils.MediaStoreCompat; import com.zhihu.matisse.internal.utils.PathUtils; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import com.zhihu.matisse.internal.utils.SingleMediaScanner; import java.util.ArrayList; /** * Main Activity to display albums and media content (images/videos) in each album * and also support media selecting operations. */ public class MatisseActivity extends AppCompatActivity implements AlbumCollection.AlbumCallbacks, AdapterView.OnItemSelectedListener, MediaSelectionFragment.SelectionProvider, View.OnClickListener, AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener, AlbumMediaAdapter.OnPhotoCapture { public static final String EXTRA_RESULT_SELECTION = "extra_result_selection"; public static final String EXTRA_RESULT_SELECTION_PATH = "extra_result_selection_path"; public static final String EXTRA_RESULT_ORIGINAL_ENABLE = "extra_result_original_enable"; private static final int REQUEST_CODE_PREVIEW = 23; private static final int REQUEST_CODE_CAPTURE = 24; public static final String CHECK_STATE = "checkState"; private final AlbumCollection mAlbumCollection = new AlbumCollection(); private MediaStoreCompat mMediaStoreCompat; private SelectedItemCollection mSelectedCollection = new SelectedItemCollection(this); private SelectionSpec mSpec; private AlbumsSpinner mAlbumsSpinner; private AlbumsAdapter mAlbumsAdapter; private TextView mButtonPreview; private TextView mButtonApply; private View mContainer; private View mEmptyView; private LinearLayout mOriginalLayout; private CheckRadioView mOriginal; private boolean mOriginalEnable; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { // programmatically set theme before super.onCreate() mSpec = SelectionSpec.getInstance(); setTheme(mSpec.themeId); super.onCreate(savedInstanceState); if (!mSpec.hasInited) { setResult(RESULT_CANCELED); finish(); return; } setContentView(R.layout.activity_matisse); if (mSpec.needOrientationRestriction()) { setRequestedOrientation(mSpec.orientation); } if (mSpec.capture) { mMediaStoreCompat = new MediaStoreCompat(this); if (mSpec.captureStrategy == null) throw new RuntimeException("Don't forget to set CaptureStrategy."); mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy); } Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayHomeAsUpEnabled(true); Drawable navigationIcon = toolbar.getNavigationIcon(); TypedArray ta = getTheme().obtainStyledAttributes(new int[]{R.attr.album_element_color}); int color = ta.getColor(0, 0); ta.recycle(); navigationIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN); mButtonPreview = (TextView) findViewById(R.id.button_preview); mButtonApply = (TextView) findViewById(R.id.button_apply); mButtonPreview.setOnClickListener(this); mButtonApply.setOnClickListener(this); mContainer = findViewById(R.id.container); mEmptyView = findViewById(R.id.empty_view); mOriginalLayout = findViewById(R.id.originalLayout); mOriginal = findViewById(R.id.original); mOriginalLayout.setOnClickListener(this); mSelectedCollection.onCreate(savedInstanceState); if (savedInstanceState != null) { mOriginalEnable = savedInstanceState.getBoolean(CHECK_STATE); } updateBottomToolbar(); mAlbumsAdapter = new AlbumsAdapter(this, null, false); mAlbumsSpinner = new AlbumsSpinner(this); mAlbumsSpinner.setOnItemSelectedListener(this); mAlbumsSpinner.setSelectedTextView((TextView) findViewById(R.id.selected_album)); mAlbumsSpinner.setPopupAnchorView(findViewById(R.id.toolbar)); mAlbumsSpinner.setAdapter(mAlbumsAdapter); mAlbumCollection.onCreate(this, this); mAlbumCollection.onRestoreInstanceState(savedInstanceState); mAlbumCollection.loadAlbums(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); mSelectedCollection.onSaveInstanceState(outState); mAlbumCollection.onSaveInstanceState(outState); outState.putBoolean("checkState", mOriginalEnable); } @Override protected void onDestroy() { super.onDestroy(); mAlbumCollection.onDestroy(); mSpec.onCheckedListener = null; mSpec.onSelectedListener = null; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { onBackPressed(); return true; } return super.onOptionsItemSelected(item); } @Override public void onBackPressed() { setResult(Activity.RESULT_CANCELED); super.onBackPressed(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode != RESULT_OK) return; if (requestCode == REQUEST_CODE_PREVIEW) { Bundle resultBundle = data.getBundleExtra(BasePreviewActivity.EXTRA_RESULT_BUNDLE); ArrayList selected = resultBundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION); mOriginalEnable = data.getBooleanExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, false); int collectionType = resultBundle.getInt(SelectedItemCollection.STATE_COLLECTION_TYPE, SelectedItemCollection.COLLECTION_UNDEFINED); if (data.getBooleanExtra(BasePreviewActivity.EXTRA_RESULT_APPLY, false)) { Intent result = new Intent(); ArrayList selectedUris = new ArrayList<>(); ArrayList selectedPaths = new ArrayList<>(); if (selected != null) { for (Item item : selected) { selectedUris.add(item.getContentUri()); selectedPaths.add(PathUtils.getPath(this, item.getContentUri())); } } result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris); result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths); result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); setResult(RESULT_OK, result); finish(); } else { mSelectedCollection.overwrite(selected, collectionType); Fragment mediaSelectionFragment = getSupportFragmentManager().findFragmentByTag( MediaSelectionFragment.class.getSimpleName()); if (mediaSelectionFragment instanceof MediaSelectionFragment) { ((MediaSelectionFragment) mediaSelectionFragment).refreshMediaGrid(); } updateBottomToolbar(); } } else if (requestCode == REQUEST_CODE_CAPTURE) { // Just pass the data back to previous calling Activity. Uri contentUri = mMediaStoreCompat.getCurrentPhotoUri(); String path = mMediaStoreCompat.getCurrentPhotoPath(); ArrayList selected = new ArrayList<>(); selected.add(contentUri); ArrayList selectedPath = new ArrayList<>(); selectedPath.add(path); Intent result = new Intent(); result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selected); result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPath); setResult(RESULT_OK, result); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) MatisseActivity.this.revokeUriPermission(contentUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); new SingleMediaScanner(this.getApplicationContext(), path, new SingleMediaScanner.ScanListener() { @Override public void onScanFinish() { Log.i("SingleMediaScanner", "scan finish!"); } }); finish(); } } private void updateBottomToolbar() { int selectedCount = mSelectedCollection.count(); if (selectedCount == 0) { mButtonPreview.setEnabled(false); mButtonApply.setEnabled(false); mButtonApply.setText(getString(R.string.button_apply_default)); } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) { mButtonPreview.setEnabled(true); mButtonApply.setText(R.string.button_apply_default); mButtonApply.setEnabled(true); } else { mButtonPreview.setEnabled(true); mButtonApply.setEnabled(true); mButtonApply.setText(getString(R.string.button_apply, selectedCount)); } if (mSpec.originalable) { mOriginalLayout.setVisibility(View.VISIBLE); updateOriginalState(); } else { mOriginalLayout.setVisibility(View.INVISIBLE); } } private void updateOriginalState() { mOriginal.setChecked(mOriginalEnable); if (countOverMaxSize() > 0) { if (mOriginalEnable) { IncapableDialog incapableDialog = IncapableDialog.newInstance("", getString(R.string.error_over_original_size, mSpec.originalMaxSize)); incapableDialog.show(getSupportFragmentManager(), IncapableDialog.class.getName()); mOriginal.setChecked(false); mOriginalEnable = false; } } } private int countOverMaxSize() { int count = 0; int selectedCount = mSelectedCollection.count(); for (int i = 0; i < selectedCount; i++) { Item item = mSelectedCollection.asList().get(i); if (item.isImage()) { float size = PhotoMetadataUtils.getSizeInMB(item.size); if (size > mSpec.originalMaxSize) { count++; } } } return count; } @Override public void onClick(View v) { if (v.getId() == R.id.button_preview) { Intent intent = new Intent(this, SelectedPreviewActivity.class); intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); startActivityForResult(intent, REQUEST_CODE_PREVIEW); } else if (v.getId() == R.id.button_apply) { Intent result = new Intent(); ArrayList selectedUris = (ArrayList) mSelectedCollection.asListOfUri(); result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris); ArrayList selectedPaths = (ArrayList) mSelectedCollection.asListOfString(); result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths); result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); setResult(RESULT_OK, result); finish(); } else if (v.getId() == R.id.originalLayout) { int count = countOverMaxSize(); if (count > 0) { IncapableDialog incapableDialog = IncapableDialog.newInstance("", getString(R.string.error_over_original_count, count, mSpec.originalMaxSize)); incapableDialog.show(getSupportFragmentManager(), IncapableDialog.class.getName()); return; } mOriginalEnable = !mOriginalEnable; mOriginal.setChecked(mOriginalEnable); if (mSpec.onCheckedListener != null) { mSpec.onCheckedListener.onCheck(mOriginalEnable); } } } @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { mAlbumCollection.setStateCurrentSelection(position); mAlbumsAdapter.getCursor().moveToPosition(position); Album album = Album.valueOf(mAlbumsAdapter.getCursor()); if (album.isAll() && SelectionSpec.getInstance().capture) { album.addCaptureCount(); } onAlbumSelected(album); } @Override public void onNothingSelected(AdapterView parent) { } @Override public void onAlbumLoad(final Cursor cursor) { mAlbumsAdapter.swapCursor(cursor); // select default album. Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { cursor.moveToPosition(mAlbumCollection.getCurrentSelection()); mAlbumsSpinner.setSelection(MatisseActivity.this, mAlbumCollection.getCurrentSelection()); Album album = Album.valueOf(cursor); if (album.isAll() && SelectionSpec.getInstance().capture) { album.addCaptureCount(); } onAlbumSelected(album); } }); } @Override public void onAlbumReset() { mAlbumsAdapter.swapCursor(null); } private void onAlbumSelected(Album album) { if (album.isAll() && album.isEmpty()) { mContainer.setVisibility(View.GONE); mEmptyView.setVisibility(View.VISIBLE); } else { mContainer.setVisibility(View.VISIBLE); mEmptyView.setVisibility(View.GONE); Fragment fragment = MediaSelectionFragment.newInstance(album); getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName()) .commitAllowingStateLoss(); } } @Override public void onUpdate() { // notify bottom toolbar that check state changed. updateBottomToolbar(); if (mSpec.onSelectedListener != null) { mSpec.onSelectedListener.onSelected( mSelectedCollection.asListOfUri(), mSelectedCollection.asListOfString()); } } @Override public void onMediaClick(Album album, Item item, int adapterPosition) { Intent intent = new Intent(this, AlbumPreviewActivity.class); intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item); intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); startActivityForResult(intent, REQUEST_CODE_PREVIEW); } @Override public SelectedItemCollection provideSelectedItemCollection() { return mSelectedCollection; } @Override public void capture() { if (mMediaStoreCompat != null) { mMediaStoreCompat.dispatchCaptureIntent(this, REQUEST_CODE_CAPTURE); } } } ================================================ FILE: matisse/src/main/res/color/dracula_bottom_toolbar_apply.xml ================================================ ================================================ FILE: matisse/src/main/res/color/dracula_bottom_toolbar_preview.xml ================================================ ================================================ FILE: matisse/src/main/res/color/dracula_preview_bottom_toolbar_apply.xml ================================================ ================================================ FILE: matisse/src/main/res/color/zhihu_bottom_toolbar_apply.xml ================================================ ================================================ FILE: matisse/src/main/res/color/zhihu_bottom_toolbar_preview.xml ================================================ ================================================ FILE: matisse/src/main/res/color/zhihu_preview_bottom_toolbar_apply.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/activity_matisse.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/activity_media_preview.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/album_list_item.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/fragment_media_selection.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/fragment_preview_item.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/media_grid_content.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/media_grid_item.xml ================================================ ================================================ FILE: matisse/src/main/res/layout/photo_capture_item.xml ================================================ ================================================ FILE: matisse/src/main/res/values/attrs.xml ================================================ ================================================ FILE: matisse/src/main/res/values/colors.xml ================================================ #CC000000 #61FFFFFF ================================================ FILE: matisse/src/main/res/values/colors_dracula.xml ================================================ #263237 #1D282C #34474E #DEFFFFFF #89FFFFFF #455A64 #4DFFFFFF #37474F #263237 #FFFFFF #FFFFFF #232E32 #34474E #DEFFFFFF #4DFFFFFF #03A9F4 #4D03A9F4 #FFFFFF #03A9F4 #4D03A9F4 ================================================ FILE: matisse/src/main/res/values/colors_zhihu.xml ================================================ #1E8AE8 #176EB9 #FFFFFF #DE000000 #999999 #EAEEF4 #4D000000 #EAEEF4 #1E8AE8 #FFFFFF #424242 #FFFFFF #FFFFFF #DE000000 #4D000000 #0077D9 #4D0077D9 #FFFFFF #0077D9 #4D0077D9 #808080 ================================================ FILE: matisse/src/main/res/values/dimens.xml ================================================ 48dp 4dp 72dp ================================================ FILE: matisse/src/main/res/values/strings.xml ================================================ All Media Preview Apply Apply(%1$d) Back Camera No media yet OK You have reached max selectable You can only select up to %1$d media files Under quality Over quality Unsupported file type Can\'t select images and videos at the same time No App found supporting video preview Can\'t select the images larger than %1$d MB %1$d images over %2$d MB. Original will be unchecked Original Sure Sure(%1$d) ================================================ FILE: matisse/src/main/res/values/styles.xml ================================================ //====================================== Theme Zhihu =========================================== //===================================== Theme Dracula ========================================== ================================================ FILE: matisse/src/main/res/values-ar/strings.xml ================================================ جميع وسائل الإعلام معاينة تطبيق تطبيق (%1$d) رجوع كاميرا لا وسائل الإعلام حتى الآن تم لقد وصلت إلى الحد الأقصى للاختيار يمكنك تحديد ما يصل إلى %1$d من ملفات الوسائط فقط تحت الجودة فوق الجودة نوع ملف غير مدعوم لا يمكن تحديد الصور ومقاطع الفيديو في نفس الوقت لم يتم العثور على التطبيق دعم معاينة الفيديو ================================================ FILE: matisse/src/main/res/values-ca/strings.xml ================================================ Tots Previsualitza Selecciona Selecciona(%1$d) Enrere Càmera Cap arxiu seleccionat OK Has sobrepassat el màxim d\'arxius seleccionables Només pots seleccionar fins a %1$d arxius multimedia Baixa qualitat Excés qualitat Tipus d\'arxiu no permés No es poden seleccionar imatges i vídeos al mateix temps No s\'ha trobat cap app que suporti la previsualització de vídeo ================================================ FILE: matisse/src/main/res/values-de/strings.xml ================================================ Alle Medien Vorschau Auswählen Auswählen(%1$d) Zurück Kamera Keine Medien gewählt OK Sie haben die maximale Anzahl an Medien ausgewählt. Sie können nur %1$d Medien auswählen Unter Qualität Über Qualität Dateityp nicht unterstützt Sie können Bilder und Videos nicht gleichzeitig auswählen Keine App zur Videovorschau gefunden ================================================ FILE: matisse/src/main/res/values-es/strings.xml ================================================ Todos Previsualizar Seleccionar Seleccionar(%1$d) Atrás Cámara Ningún archivo seleccionado OK Has alcanzado el máximo de archivos seleccionables Sólo puede seleccionar hasta %1$d archivos multimedia Baja calidad Exceso calidad Tipo de fichero no soportado No se puede seleccionar imágenes y vídeos al mismo tiempo No se ha encontrado ninguna app que soporte la previsualización de vídeo ================================================ FILE: matisse/src/main/res/values-it/strings.xml ================================================ Tutti i media Anteprima Conferma Conferma(%1$d) Indietro Camera Nessun media disponibile OK Hai selezionato il numero massimo di elementi Puoi selezionare fino a %1$d elementi Sotto qualità Sopra qualità Tipo di file non supportato Non è possibile selezionare contemporaneamente immagini e video Non è stata trovata alcuna app che supporti l\'anteprima video ================================================ FILE: matisse/src/main/res/values-ko/strings.xml ================================================ 전체보기 미리보기 카메라 비디오 미리보기를 지원하는 앱을 찾을 수 없습니다. 이미지와 비디오는 동시에 선택 할 수 없습니다. 적용 적용(%1$d) 뒤로 미디어 파일이 없습니다. 확인 더이상 선택할 수 없습니다. 최대 %1$d까지 선택 가능합니다. 화질이 너무 낮습니다. 화질이 너무 높습니다. 지원되지 않는 파일 유형입니다. ================================================ FILE: matisse/src/main/res/values-pl/strings.xml ================================================ Wszystkie Media Preview Zatwierdź Zatwierdź(%1$d) Wstecz Aparat Brak mediów OK Osiągnąłeś limit wybieralnych elementów Możesz wybrać do %1$d plików Poniżej jakości Powyżej jakości Niewspierany typ pliku Nie można wybierać obrazów i filmów w tym samym czasie Nie znaleziono aplikacji wspierającej podgląd wideo Oryginał Zatwierdź Zatwierdź(%1$d) ================================================ FILE: matisse/src/main/res/values-pt-rBR/strings.xml ================================================ Todas as mídias Pré-visualizaçao Aplicar Aplicar(%1$d) Voltar Câmera Nenhuma mídia disponível OK Você atingiu o máximo de itens possíveis para seleção Você só pode selecionar até %1$d arquivos de mídia Abaixo da qualidade Acima da qualidade Tipo de arquivo não suportado Não é possível selecionar arquivos de imagem e vídeo simultaneamente Nenhum player de vídeo disponível para reprodução ================================================ FILE: matisse/src/main/res/values-ru/strings.xml ================================================ Все Предпросмотр Применить Применить(%1$d) Назад Камера Пусто OK Вы выбрали максимальное количество файлов Можно выбрать не более %1$d файла Можно выбрать не более %1$d файлов Можно выбрать не более %1$d файлов Можно выбрать не более %1$d файлов Слишком низкое качество Слишком высокое качество Неподдерживаемый тип файла Невозможно выбрать изображения и видео одновременно Приложение для предпросмотра видео не найдено Оригинал Применить(%1$d) Применить ================================================ FILE: matisse/src/main/res/values-tr-rTR/strings.xml ================================================ Tüm Medya Ön İzleme Uygula Uygula(%1$d) Geri Kamera Henüz medya yok TAMAM Maksimum seçilebilir değere ulaştınız Sadece %1$d medya dosyasını seçebilirsiniz Düşük kalite Yüksek kalite Desteklenmeyen dosya tipi Görüntüleri ve videoları aynı anda seçemezsiniz Video önizlemesini destekleyen hiçbir uygulama bulunamadı ================================================ FILE: matisse/src/main/res/values-uk/strings.xml ================================================ Всі Перегляд Застосувати Застосувати(%1$d) Назад Камера Пусто OK Вы обрали максимальну кількість файлів Можливо обрати не більше %1$d файла Можливо обрати не більше %1$d файлів Можливо обрати не більше %1$d файлів Можливо обрати не більше %1$d файлів Занадто низька якість Занадто висока якість Непідтримуваний тип файла Неможливо обрати зображення і відео одночасно Застосунок для перегляду відео не знайдений ================================================ FILE: matisse/src/main/res/values-vi/strings.xml ================================================ Không thể chọn hình ảnh lớn hơn %1$d MB Tất cả Áp dụng(%1$d) Áp dụng Về Đồng ý Bản gốc Xem trước Đồng ý(%1$d) Đồng ý Chưa có dữ liệu Không hỗ trợ loại tệp này Không tìm thấy ứng dụng nào hỗ trợ xem trước video Bạn chỉ có thể chọn tối đa %1$d tệp Bạn đã đạt đến mức tối đa có thể lựa chọn %1$d hình ảnh trên %2$d MB. Bản gốc sẽ được bỏ chọn Chất lượng quá cao Không thể chọn hình ảnh và video cùng một lúc Chất lượng thấp Camera ================================================ FILE: matisse/src/main/res/values-zh/strings.xml ================================================ 全部 预览 使用 使用(%1$d) 返回 拍一张 还没有图片或视频 我知道了 您已经达到最大选择数量 最多只能选择 %1$d 个文件 图片质量太低 图片质量太高 不支持的文件类型 不能同时选择图片和视频 没有支持视频预览的应用 "该照片大于 %1$d M,无法上传将取消勾选原图" "有 %1$d 张照片大于 %2$d M\n无法上传,将取消勾选原图" 原图 确定 确定(%1$d) ================================================ FILE: matisse/src/main/res/values-zh-rTW/strings.xml ================================================ 全部 預覽 使用 使用(%1$d) 返回 拍一張 還沒有圖片或影片 我知道了 您已經達到最大選擇數量 最多只能選擇 %1$d 個文件 圖片質量太低 圖片質量太高 不支援的文件類型 不能同時選擇圖片和影片 沒有支持影片預覽的應用程式 原圖 确定 确定(%1$d) ================================================ FILE: sample/build.gradle ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'com.android.application' android { compileSdkVersion 29 defaultConfig { applicationId 'com.zhihu.matisse.sample' minSdkVersion 14 targetSdkVersion 29 versionCode 1 versionName "1.0" } lintOptions { abortOnError false } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation project(':matisse') // implementation 'com.zhihu.android:matisse:0.5.2' implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "androidx.appcompat:appcompat:1.1.0" implementation "androidx.recyclerview:recyclerview:1.0.0" implementation 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.5@aar' implementation 'io.reactivex.rxjava2:rxjava:2.2.12' implementation 'com.github.bumptech.glide:glide:4.9.0' implementation 'com.squareup.picasso:picasso:2.5.2' } ================================================ FILE: sample/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in /Library/android-sdk-macosx/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the ProGuard # include property in project.properties. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -dontwarn com.squareup.okhttp.** ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/java/com/zhihu/matisse/sample/GifSizeFilter.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.sample; import android.content.Context; import android.graphics.Point; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.filter.Filter; import com.zhihu.matisse.internal.entity.IncapableCause; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import java.util.HashSet; import java.util.Set; class GifSizeFilter extends Filter { private int mMinWidth; private int mMinHeight; private int mMaxSize; GifSizeFilter(int minWidth, int minHeight, int maxSizeInBytes) { mMinWidth = minWidth; mMinHeight = minHeight; mMaxSize = maxSizeInBytes; } @Override public Set constraintTypes() { return new HashSet() {{ add(MimeType.GIF); }}; } @Override public IncapableCause filter(Context context, Item item) { if (!needFiltering(context, item)) return null; Point size = PhotoMetadataUtils.getBitmapBound(context.getContentResolver(), item.getContentUri()); if (size.x < mMinWidth || size.y < mMinHeight || item.size > mMaxSize) { return new IncapableCause(IncapableCause.DIALOG, context.getString(R.string.error_gif, mMinWidth, String.valueOf(PhotoMetadataUtils.getSizeInMB(mMaxSize)))); } return null; } } ================================================ FILE: sample/src/main/java/com/zhihu/matisse/sample/SampleActivity.java ================================================ /* * Copyright 2017 Zhihu Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zhihu.matisse.sample; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.ActivityInfo; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.tbruyelle.rxpermissions2.RxPermissions; import com.zhihu.matisse.Matisse; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.engine.impl.GlideEngine; import com.zhihu.matisse.engine.impl.PicassoEngine; import com.zhihu.matisse.filter.Filter; import com.zhihu.matisse.internal.entity.CaptureStrategy; import java.util.List; public class SampleActivity extends AppCompatActivity implements View.OnClickListener { private static final int REQUEST_CODE_CHOOSE = 23; private UriAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); findViewById(R.id.zhihu).setOnClickListener(this); findViewById(R.id.dracula).setOnClickListener(this); findViewById(R.id.only_gif).setOnClickListener(this); RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview); recyclerView.setLayoutManager(new LinearLayoutManager(this)); recyclerView.setAdapter(mAdapter = new UriAdapter()); } // @SuppressLint("CheckResult") @Override public void onClick(final View v) { RxPermissions rxPermissions = new RxPermissions(this); rxPermissions.request(Manifest.permission.WRITE_EXTERNAL_STORAGE) .subscribe(aBoolean -> { if (aBoolean) { startAction(v); } else { Toast.makeText(SampleActivity.this, R.string.permission_request_denied, Toast.LENGTH_LONG) .show(); } }, Throwable::printStackTrace); } // private void startAction(View v) { switch (v.getId()) { case R.id.zhihu: Matisse.from(SampleActivity.this) .choose(MimeType.ofImage(), false) .countable(true) .capture(true) .captureStrategy( new CaptureStrategy(true, "com.zhihu.matisse.sample.fileprovider", "test")) .maxSelectable(9) .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) .gridExpectedSize( getResources().getDimensionPixelSize(R.dimen.grid_expected_size)) .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) .thumbnailScale(0.85f) .imageEngine(new GlideEngine()) .setOnSelectedListener((uriList, pathList) -> { Log.e("onSelected", "onSelected: pathList=" + pathList); }) .showSingleMediaType(true) .originalEnable(true) .maxOriginalSize(10) .autoHideToolbarOnSingleTap(true) .setOnCheckedListener(isChecked -> { Log.e("isChecked", "onCheck: isChecked=" + isChecked); }) .forResult(REQUEST_CODE_CHOOSE); break; case R.id.dracula: Matisse.from(SampleActivity.this) .choose(MimeType.ofImage()) .theme(R.style.Matisse_Dracula) .countable(false) .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) .maxSelectable(9) .originalEnable(true) .maxOriginalSize(10) .imageEngine(new PicassoEngine()) .forResult(REQUEST_CODE_CHOOSE); break; case R.id.only_gif: Matisse.from(SampleActivity.this) .choose(MimeType.of(MimeType.GIF), false) .countable(true) .maxSelectable(9) .addFilter(new GifSizeFilter(320, 320, 5 * Filter.K * Filter.K)) .gridExpectedSize( getResources().getDimensionPixelSize(R.dimen.grid_expected_size)) .restrictOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) .thumbnailScale(0.85f) .imageEngine(new GlideEngine()) .showSingleMediaType(true) .originalEnable(true) .maxOriginalSize(10) .autoHideToolbarOnSingleTap(true) .forResult(REQUEST_CODE_CHOOSE); break; default: break; } mAdapter.setData(null, null); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_CHOOSE && resultCode == RESULT_OK) { mAdapter.setData(Matisse.obtainResult(data), Matisse.obtainPathResult(data)); Log.e("OnActivityResult ", String.valueOf(Matisse.obtainOriginalState(data))); } } private static class UriAdapter extends RecyclerView.Adapter { private List mUris; private List mPaths; void setData(List uris, List paths) { mUris = uris; mPaths = paths; notifyDataSetChanged(); } @Override public UriViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new UriViewHolder( LayoutInflater.from(parent.getContext()).inflate(R.layout.uri_item, parent, false)); } @Override public void onBindViewHolder(UriViewHolder holder, int position) { holder.mUri.setText(mUris.get(position).toString()); holder.mPath.setText(mPaths.get(position)); holder.mUri.setAlpha(position % 2 == 0 ? 1.0f : 0.54f); holder.mPath.setAlpha(position % 2 == 0 ? 1.0f : 0.54f); } @Override public int getItemCount() { return mUris == null ? 0 : mUris.size(); } static class UriViewHolder extends RecyclerView.ViewHolder { private TextView mUri; private TextView mPath; UriViewHolder(View contentView) { super(contentView); mUri = (TextView) contentView.findViewById(R.id.uri); mPath = (TextView) contentView.findViewById(R.id.path); } } } } ================================================ FILE: sample/src/main/res/layout/activity_main.xml ================================================