├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── a-regression.md │ ├── b-bug-report.md │ ├── c-feature-request.md │ ├── d-enhancement-proposal.md │ └── e-question.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── photoview ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── github │ └── chrisbanes │ └── photoview │ ├── Compat.java │ ├── CustomGestureDetector.java │ ├── OnGestureListener.java │ ├── OnMatrixChangedListener.java │ ├── OnOutsidePhotoTapListener.java │ ├── OnPhotoTapListener.java │ ├── OnScaleChangedListener.java │ ├── OnSingleFlingListener.java │ ├── OnViewDragListener.java │ ├── OnViewTapListener.java │ ├── PhotoView.java │ ├── PhotoViewAttacher.java │ └── Util.java ├── sample ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── github │ │ └── chrisbanes │ │ └── photoview │ │ └── sample │ │ ├── ActivityTransitionActivity.java │ │ ├── ActivityTransitionToActivity.java │ │ ├── CoilSampleActivity.kt │ │ ├── HackyDrawerLayout.java │ │ ├── HackyViewPager.java │ │ ├── ImageAdapter.java │ │ ├── ImageViewHolder.java │ │ ├── ImmersiveActivity.java │ │ ├── LauncherActivity.java │ │ ├── PicassoSampleActivity.java │ │ ├── RotationSampleActivity.java │ │ ├── SimpleSampleActivity.java │ │ └── ViewPagerActivity.java │ └── res │ ├── drawable-nodpi │ └── wallpaper.jpg │ ├── drawable │ └── ic_arrow_back_white_24dp.xml │ ├── layout │ ├── activity_immersive.xml │ ├── activity_launcher.xml │ ├── activity_rotation_sample.xml │ ├── activity_simple.xml │ ├── activity_simple_sample.xml │ ├── activity_transition.xml │ ├── activity_transition_to.xml │ ├── activity_view_pager.xml │ ├── item_image.xml │ └── item_sample.xml │ ├── menu │ ├── main_menu.xml │ └── rotation.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ ├── styles.xml │ └── transitions.xml └── settings.gradle /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Baseflow 2 | custom: https://baseflow.com/contact 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/a-regression.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🔙 Regression 4 | about: Report unexpected behavior that worked previously 5 | --- 6 | 7 | ## 🔙 Regression 8 | 9 | 10 | 11 | ### Old (and correct) behavior 12 | 13 | ### Current behavior 14 | 15 | ### Reproduction steps 16 | 17 | ### Configuration 18 | 19 | **Version:** 1.x 20 | 21 | **Platform:** :robot: Android 9.x -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/b-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🐛 Bug Report 4 | about: Create a report to help us fix bugs and make improvements 5 | --- 6 | 7 | ## 🐛 Bug Report 8 | 9 | 10 | 11 | ### Expected behavior 12 | 13 | ### Reproduction steps 14 | 15 | ### Configuration 16 | 17 | **Version:** 1.x 18 | 19 | **Platform:** :robot: Android 9.x -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/c-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🚀 Feature Request 4 | about: Want to see something new included in the Framework? Submit it! 5 | --- 6 | 7 | ## 🚀 Feature Requests 8 | 9 | 10 | 11 | ### Contextualize the feature 12 | 13 | 14 | ### Describe the feature 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/d-enhancement-proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🏗 Enhancement Proposal 4 | about: Proposals for code cleanup, refactor and improvements in general 5 | --- 6 | 7 | ## 🏗 Enhancement Proposal 8 | 9 | 10 | 11 | ### Pitch 12 | 13 | 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/e-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 💬 Questions and Help 4 | about: If you have questions, please use this for support 5 | --- 6 | 7 | ## 💬 Questions and Help 8 | 9 | For questions or help we recommend checking: 10 | 11 | - [Stack Overflow](https://stackoverflow.com/) -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### :sparkles: What kind of change does this PR introduce? (Bug fix, feature, docs update...) 2 | 3 | 4 | ### :arrow_heading_down: What is the current behavior? 5 | 6 | 7 | ### :new: What is the new behavior (if this is a feature change)? 8 | 9 | 10 | ### :boom: Does this PR introduce a breaking change? 11 | 12 | 13 | ### :bug: Recommendations for testing 14 | 15 | 16 | ### :memo: Links to relevant issues/docs 17 | 18 | 19 | ### :thinking: Checklist before submitting 20 | 21 | - [ ] All projects build 22 | - [ ] Follows style guide lines 23 | - [ ] Relevant documentation was updated 24 | - [ ] Rebased onto current develop 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Android generated 2 | bin 3 | gen 4 | 5 | #Eclipse 6 | .project 7 | .classpath 8 | .settings 9 | 10 | #IntelliJ IDEA 11 | .idea 12 | *.iml 13 | *.ipr 14 | *.iws 15 | out 16 | 17 | #Maven 18 | target 19 | release.properties 20 | pom.xml.* 21 | 22 | #Ant 23 | build.xml 24 | local.properties 25 | proguard.cfg 26 | 27 | #OSX 28 | .DS_Store 29 | 30 | #Gradle 31 | .gradle 32 | build 33 | 34 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [mvvmcross@gmail.com](mailto:mvvmcross@gmail). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to PhotoView 2 | 3 | ## Finding an issue to work on 4 | 5 | If you'd like to work on something that isn't in a current issue, especially if it would be a big change, please open a new issue for discussion! 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PhotoView 2 | PhotoView aims to help produce an easily usable implementation of a zooming Android ImageView. 3 | 4 | [![](https://jitpack.io/v/chrisbanes/PhotoView.svg)](https://jitpack.io/#chrisbanes/PhotoView) 5 | 6 | [![](https://user-images.githubusercontent.com/12352397/85141529-94648e80-b24f-11ea-9a14-a845fb43b181.gif) 7 | 8 | ## Dependency 9 | 10 | Add this in your root `build.gradle` file (**not** your module `build.gradle` file): 11 | 12 | ```gradle 13 | allprojects { 14 | repositories { 15 | maven { url "https://www.jitpack.io" } 16 | } 17 | } 18 | 19 | buildscript { 20 | repositories { 21 | maven { url "https://www.jitpack.io" } 22 | } 23 | } 24 | ``` 25 | 26 | Then, add the library to your module `build.gradle` 27 | ```gradle 28 | dependencies { 29 | implementation 'com.github.chrisbanes:PhotoView:latest.release.here' 30 | } 31 | ``` 32 | 33 | ## Features 34 | - Out of the box zooming, using multi-touch and double-tap. 35 | - Scrolling, with smooth scrolling fling. 36 | - Works perfectly when used in a scrolling parent (such as ViewPager). 37 | - Allows the application to be notified when the displayed Matrix has changed. Useful for when you need to update your UI based on the current zoom/scroll position. 38 | - Allows the application to be notified when the user taps on the Photo. 39 | 40 | ## Usage 41 | There is a [sample](https://github.com/chrisbanes/PhotoView/tree/master/sample) provided which shows how to use the library in a more advanced way, but for completeness, here is all that is required to get PhotoView working: 42 | ```xml 43 | 47 | ``` 48 | ```java 49 | PhotoView photoView = (PhotoView) findViewById(R.id.photo_view); 50 | photoView.setImageResource(R.drawable.image); 51 | ``` 52 | That's it! 53 | 54 | ## Issues With ViewGroups 55 | There are some ViewGroups (ones that utilize onInterceptTouchEvent) that throw exceptions when a PhotoView is placed within them, most notably [ViewPager](http://developer.android.com/reference/android/support/v4/view/ViewPager.html) and [DrawerLayout](https://developer.android.com/reference/android/support/v4/widget/DrawerLayout.html). This is a framework issue that has not been resolved. In order to prevent this exception (which typically occurs when you zoom out), take a look at [HackyDrawerLayout](https://github.com/chrisbanes/PhotoView/blob/master/sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyDrawerLayout.java) and you can see the solution is to simply catch the exception. Any ViewGroup which uses onInterceptTouchEvent will also need to be extended and exceptions caught. Use the [HackyDrawerLayout](https://github.com/chrisbanes/PhotoView/blob/master/sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyDrawerLayout.java) as a template of how to do so. The basic implementation is: 56 | ```java 57 | public class HackyProblematicViewGroup extends ProblematicViewGroup { 58 | 59 | public HackyProblematicViewGroup(Context context) { 60 | super(context); 61 | } 62 | 63 | @Override 64 | public boolean onInterceptTouchEvent(MotionEvent ev) { 65 | try { 66 | return super.onInterceptTouchEvent(ev); 67 | } catch (IllegalArgumentException e) { 68 | //uncomment if you really want to see these errors 69 | //e.printStackTrace(); 70 | return false; 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | ## Usage with Fresco 77 | Due to the complex nature of Fresco, this library does not currently support Fresco. See [this project](https://github.com/ongakuer/PhotoDraweeView) as an alternative solution. 78 | 79 | ## Subsampling Support 80 | This library aims to keep the zooming implementation simple. If you are looking for an implementation that supports subsampling, check out [this project](https://github.com/davemorrissey/subsampling-scale-image-view) 81 | 82 | License 83 | -------- 84 | 85 | Copyright 2018 Chris Banes 86 | 87 | Licensed under the Apache License, Version 2.0 (the "License"); 88 | you may not use this file except in compliance with the License. 89 | You may obtain a copy of the License at 90 | 91 | http://www.apache.org/licenses/LICENSE-2.0 92 | 93 | Unless required by applicable law or agreed to in writing, software 94 | distributed under the License is distributed on an "AS IS" BASIS, 95 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 96 | See the License for the specific language governing permissions and 97 | limitations under the License. 98 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.72' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:4.1.2' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 11 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | ext { 23 | sdkVersion = 29 24 | minSdkVersion = 16 25 | } 26 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.useAndroidX=true 2 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /photoview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | 4 | android { 5 | compileSdkVersion rootProject.ext.sdkVersion 6 | 7 | defaultConfig { 8 | minSdkVersion rootProject.ext.minSdkVersion 9 | targetSdkVersion rootProject.ext.sdkVersion 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | } 14 | 15 | dependencies { 16 | implementation "androidx.appcompat:appcompat:1.1.0" 17 | } 18 | 19 | afterEvaluate { 20 | publishing { 21 | publications { 22 | release(MavenPublication) { 23 | from components.release 24 | 25 | group = 'com.github.chrisbanes' 26 | artifactId = 'PhotoView' 27 | version = '2.3.0' 28 | 29 | // Adds javadocs and sources as separate jars. 30 | artifact androidJavadocsJar 31 | artifact(sourceJar) 32 | 33 | pom { 34 | name = 'PhotoView' 35 | description = 'A simple ImageView that support zooming, both by Multi-touch gestures and double-tap.' 36 | url = 'https://github.com/Baseflow/PhotoView' 37 | licenses { 38 | license { 39 | name = 'The Apache License, Version 2.0' 40 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 41 | } 42 | } 43 | developers { 44 | developer { 45 | id = 'chrisbanes' 46 | name = 'Chris Banes' 47 | } 48 | } 49 | scm { 50 | connection = 'scm:git@github.com/chrisbanes/PhotoView.git' 51 | developerConnection = 'scm:git@github.com/chrisbanes/PhotoView.git' 52 | url = 'https://github.com/chrisbanes/PhotoView' 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | task androidJavadocs(type: Javadoc) { 61 | source = android.sourceSets.main.java.srcDirs 62 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 63 | android.libraryVariants.all { variant -> 64 | if (variant.name == 'release') { 65 | owner.classpath += variant.javaCompileProvider.get().classpath 66 | } 67 | } 68 | exclude '**/R.html', '**/R.*.html', '**/index.html' 69 | } 70 | 71 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 72 | archiveClassifier.set('javadoc') 73 | from androidJavadocs.destinationDir 74 | } 75 | 76 | task sourceJar(type: Jar) { 77 | from android.sourceSets.main.java.srcDirs 78 | classifier "sources" 79 | } -------------------------------------------------------------------------------- /photoview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/Compat.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview; 17 | 18 | import android.annotation.TargetApi; 19 | import android.os.Build.VERSION; 20 | import android.os.Build.VERSION_CODES; 21 | import android.view.View; 22 | 23 | class Compat { 24 | 25 | private static final int SIXTY_FPS_INTERVAL = 1000 / 60; 26 | 27 | public static void postOnAnimation(View view, Runnable runnable) { 28 | if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { 29 | postOnAnimationJellyBean(view, runnable); 30 | } else { 31 | view.postDelayed(runnable, SIXTY_FPS_INTERVAL); 32 | } 33 | } 34 | 35 | @TargetApi(16) 36 | private static void postOnAnimationJellyBean(View view, Runnable runnable) { 37 | view.postOnAnimation(runnable); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/CustomGestureDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview; 17 | 18 | import android.content.Context; 19 | import android.view.MotionEvent; 20 | import android.view.ScaleGestureDetector; 21 | import android.view.VelocityTracker; 22 | import android.view.ViewConfiguration; 23 | 24 | /** 25 | * Does a whole lot of gesture detecting. 26 | */ 27 | class CustomGestureDetector { 28 | 29 | private static final int INVALID_POINTER_ID = -1; 30 | 31 | private int mActivePointerId = INVALID_POINTER_ID; 32 | private int mActivePointerIndex = 0; 33 | private final ScaleGestureDetector mDetector; 34 | 35 | private VelocityTracker mVelocityTracker; 36 | private boolean mIsDragging; 37 | private float mLastTouchX; 38 | private float mLastTouchY; 39 | private final float mTouchSlop; 40 | private final float mMinimumVelocity; 41 | private OnGestureListener mListener; 42 | 43 | CustomGestureDetector(Context context, OnGestureListener listener) { 44 | final ViewConfiguration configuration = ViewConfiguration 45 | .get(context); 46 | mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); 47 | mTouchSlop = configuration.getScaledTouchSlop(); 48 | 49 | mListener = listener; 50 | ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { 51 | private float lastFocusX, lastFocusY = 0; 52 | 53 | @Override 54 | public boolean onScale(ScaleGestureDetector detector) { 55 | float scaleFactor = detector.getScaleFactor(); 56 | 57 | if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) 58 | return false; 59 | 60 | if (scaleFactor >= 0) { 61 | mListener.onScale(scaleFactor, 62 | detector.getFocusX(), 63 | detector.getFocusY(), 64 | detector.getFocusX() - lastFocusX, 65 | detector.getFocusY() - lastFocusY 66 | ); 67 | lastFocusX = detector.getFocusX(); 68 | lastFocusY = detector.getFocusY(); 69 | } 70 | return true; 71 | } 72 | 73 | @Override 74 | public boolean onScaleBegin(ScaleGestureDetector detector) { 75 | lastFocusX = detector.getFocusX(); 76 | lastFocusY = detector.getFocusY(); 77 | return true; 78 | } 79 | 80 | @Override 81 | public void onScaleEnd(ScaleGestureDetector detector) { 82 | // NO-OP 83 | } 84 | }; 85 | mDetector = new ScaleGestureDetector(context, mScaleListener); 86 | } 87 | 88 | private float getActiveX(MotionEvent ev) { 89 | try { 90 | return ev.getX(mActivePointerIndex); 91 | } catch (Exception e) { 92 | return ev.getX(); 93 | } 94 | } 95 | 96 | private float getActiveY(MotionEvent ev) { 97 | try { 98 | return ev.getY(mActivePointerIndex); 99 | } catch (Exception e) { 100 | return ev.getY(); 101 | } 102 | } 103 | 104 | public boolean isScaling() { 105 | return mDetector.isInProgress(); 106 | } 107 | 108 | public boolean isDragging() { 109 | return mIsDragging; 110 | } 111 | 112 | public boolean onTouchEvent(MotionEvent ev) { 113 | try { 114 | mDetector.onTouchEvent(ev); 115 | return processTouchEvent(ev); 116 | } catch (IllegalArgumentException e) { 117 | // Fix for support lib bug, happening when onDestroy is called 118 | return true; 119 | } 120 | } 121 | 122 | private boolean processTouchEvent(MotionEvent ev) { 123 | final int action = ev.getAction(); 124 | switch (action & MotionEvent.ACTION_MASK) { 125 | case MotionEvent.ACTION_DOWN: 126 | mActivePointerId = ev.getPointerId(0); 127 | 128 | mVelocityTracker = VelocityTracker.obtain(); 129 | if (null != mVelocityTracker) { 130 | mVelocityTracker.addMovement(ev); 131 | } 132 | 133 | mLastTouchX = getActiveX(ev); 134 | mLastTouchY = getActiveY(ev); 135 | mIsDragging = false; 136 | break; 137 | case MotionEvent.ACTION_MOVE: 138 | final float x = getActiveX(ev); 139 | final float y = getActiveY(ev); 140 | final float dx = x - mLastTouchX, dy = y - mLastTouchY; 141 | 142 | if (!mIsDragging) { 143 | // Use Pythagoras to see if drag length is larger than 144 | // touch slop 145 | mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; 146 | } 147 | 148 | if (mIsDragging) { 149 | mListener.onDrag(dx, dy); 150 | mLastTouchX = x; 151 | mLastTouchY = y; 152 | 153 | if (null != mVelocityTracker) { 154 | mVelocityTracker.addMovement(ev); 155 | } 156 | } 157 | break; 158 | case MotionEvent.ACTION_CANCEL: 159 | mActivePointerId = INVALID_POINTER_ID; 160 | // Recycle Velocity Tracker 161 | if (null != mVelocityTracker) { 162 | mVelocityTracker.recycle(); 163 | mVelocityTracker = null; 164 | } 165 | break; 166 | case MotionEvent.ACTION_UP: 167 | mActivePointerId = INVALID_POINTER_ID; 168 | if (mIsDragging) { 169 | if (null != mVelocityTracker) { 170 | mLastTouchX = getActiveX(ev); 171 | mLastTouchY = getActiveY(ev); 172 | 173 | // Compute velocity within the last 1000ms 174 | mVelocityTracker.addMovement(ev); 175 | mVelocityTracker.computeCurrentVelocity(1000); 176 | 177 | final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker 178 | .getYVelocity(); 179 | 180 | // If the velocity is greater than minVelocity, call 181 | // listener 182 | if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { 183 | mListener.onFling(mLastTouchX, mLastTouchY, -vX, 184 | -vY); 185 | } 186 | } 187 | } 188 | 189 | // Recycle Velocity Tracker 190 | if (null != mVelocityTracker) { 191 | mVelocityTracker.recycle(); 192 | mVelocityTracker = null; 193 | } 194 | break; 195 | case MotionEvent.ACTION_POINTER_UP: 196 | final int pointerIndex = Util.getPointerIndex(ev.getAction()); 197 | final int pointerId = ev.getPointerId(pointerIndex); 198 | if (pointerId == mActivePointerId) { 199 | // This was our active pointer going up. Choose a new 200 | // active pointer and adjust accordingly. 201 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0; 202 | mActivePointerId = ev.getPointerId(newPointerIndex); 203 | mLastTouchX = ev.getX(newPointerIndex); 204 | mLastTouchY = ev.getY(newPointerIndex); 205 | } 206 | break; 207 | } 208 | 209 | mActivePointerIndex = ev 210 | .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId 211 | : 0); 212 | return true; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnGestureListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview; 17 | 18 | interface OnGestureListener { 19 | 20 | void onDrag(float dx, float dy); 21 | 22 | void onFling(float startX, float startY, float velocityX, 23 | float velocityY); 24 | 25 | void onScale(float scaleFactor, float focusX, float focusY); 26 | 27 | void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy); 28 | } -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnMatrixChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.graphics.RectF; 4 | 5 | /** 6 | * Interface definition for a callback to be invoked when the internal Matrix has changed for 7 | * this View. 8 | */ 9 | public interface OnMatrixChangedListener { 10 | 11 | /** 12 | * Callback for when the Matrix displaying the Drawable has changed. This could be because 13 | * the View's bounds have changed, or the user has zoomed. 14 | * 15 | * @param rect - Rectangle displaying the Drawable's new bounds. 16 | */ 17 | void onMatrixChanged(RectF rect); 18 | } 19 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnOutsidePhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.widget.ImageView; 4 | 5 | /** 6 | * Callback when the user tapped outside of the photo 7 | */ 8 | public interface OnOutsidePhotoTapListener { 9 | 10 | /** 11 | * The outside of the photo has been tapped 12 | */ 13 | void onOutsidePhotoTap(ImageView imageView); 14 | } 15 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnPhotoTapListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.widget.ImageView; 4 | 5 | /** 6 | * A callback to be invoked when the Photo is tapped with a single 7 | * tap. 8 | */ 9 | public interface OnPhotoTapListener { 10 | 11 | /** 12 | * A callback to receive where the user taps on a photo. You will only receive a callback if 13 | * the user taps on the actual photo, tapping on 'whitespace' will be ignored. 14 | * 15 | * @param view ImageView the user tapped. 16 | * @param x where the user tapped from the of the Drawable, as percentage of the 17 | * Drawable width. 18 | * @param y where the user tapped from the top of the Drawable, as percentage of the 19 | * Drawable height. 20 | */ 21 | void onPhotoTap(ImageView view, float x, float y); 22 | } 23 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnScaleChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | 4 | /** 5 | * Interface definition for callback to be invoked when attached ImageView scale changes 6 | */ 7 | public interface OnScaleChangedListener { 8 | 9 | /** 10 | * Callback for when the scale changes 11 | * 12 | * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) 13 | * @param focusX focal point X position 14 | * @param focusY focal point Y position 15 | */ 16 | void onScaleChange(float scaleFactor, float focusX, float focusY); 17 | } 18 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnSingleFlingListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.view.MotionEvent; 4 | 5 | /** 6 | * A callback to be invoked when the ImageView is flung with a single 7 | * touch 8 | */ 9 | public interface OnSingleFlingListener { 10 | 11 | /** 12 | * A callback to receive where the user flings on a ImageView. You will receive a callback if 13 | * the user flings anywhere on the view. 14 | * 15 | * @param e1 MotionEvent the user first touch. 16 | * @param e2 MotionEvent the user last touch. 17 | * @param velocityX distance of user's horizontal fling. 18 | * @param velocityY distance of user's vertical fling. 19 | */ 20 | boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); 21 | } 22 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnViewDragListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | /** 4 | * Interface definition for a callback to be invoked when the photo is experiencing a drag event 5 | */ 6 | public interface OnViewDragListener { 7 | 8 | /** 9 | * Callback for when the photo is experiencing a drag event. This cannot be invoked when the 10 | * user is scaling. 11 | * 12 | * @param dx The change of the coordinates in the x-direction 13 | * @param dy The change of the coordinates in the y-direction 14 | */ 15 | void onDrag(float dx, float dy); 16 | } 17 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/OnViewTapListener.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.view.View; 4 | 5 | public interface OnViewTapListener { 6 | 7 | /** 8 | * A callback to receive where the user taps on a ImageView. You will receive a callback if 9 | * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. 10 | * 11 | * @param view - View the user tapped. 12 | * @param x - where the user tapped from the left of the View. 13 | * @param y - where the user tapped from the top of the View. 14 | */ 15 | void onViewTap(View view, float x, float y); 16 | } 17 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/PhotoView.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.RectF; 21 | import android.graphics.drawable.Drawable; 22 | import android.net.Uri; 23 | import android.util.AttributeSet; 24 | import android.view.GestureDetector; 25 | 26 | import androidx.appcompat.widget.AppCompatImageView; 27 | 28 | /** 29 | * A zoomable ImageView. See {@link PhotoViewAttacher} for most of the details on how the zooming 30 | * is accomplished 31 | */ 32 | @SuppressWarnings("unused") 33 | public class PhotoView extends AppCompatImageView { 34 | 35 | private PhotoViewAttacher attacher; 36 | private ScaleType pendingScaleType; 37 | 38 | public PhotoView(Context context) { 39 | this(context, null); 40 | } 41 | 42 | public PhotoView(Context context, AttributeSet attr) { 43 | this(context, attr, 0); 44 | } 45 | 46 | public PhotoView(Context context, AttributeSet attr, int defStyle) { 47 | super(context, attr, defStyle); 48 | init(); 49 | } 50 | 51 | private void init() { 52 | attacher = new PhotoViewAttacher(this); 53 | //We always pose as a Matrix scale type, though we can change to another scale type 54 | //via the attacher 55 | super.setScaleType(ScaleType.MATRIX); 56 | //apply the previously applied scale type 57 | if (pendingScaleType != null) { 58 | setScaleType(pendingScaleType); 59 | pendingScaleType = null; 60 | } 61 | } 62 | 63 | /** 64 | * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references 65 | * to this attacher, as it has a reference to this view, which, if a reference is held in the 66 | * wrong place, can cause memory leaks. 67 | * 68 | * @return the attacher. 69 | */ 70 | public PhotoViewAttacher getAttacher() { 71 | return attacher; 72 | } 73 | 74 | @Override 75 | public ScaleType getScaleType() { 76 | return attacher.getScaleType(); 77 | } 78 | 79 | @Override 80 | public Matrix getImageMatrix() { 81 | return attacher.getImageMatrix(); 82 | } 83 | 84 | @Override 85 | public void setOnLongClickListener(OnLongClickListener l) { 86 | attacher.setOnLongClickListener(l); 87 | } 88 | 89 | @Override 90 | public void setOnClickListener(OnClickListener l) { 91 | attacher.setOnClickListener(l); 92 | } 93 | 94 | @Override 95 | public void setScaleType(ScaleType scaleType) { 96 | if (attacher == null) { 97 | pendingScaleType = scaleType; 98 | } else { 99 | attacher.setScaleType(scaleType); 100 | } 101 | } 102 | 103 | @Override 104 | public void setImageDrawable(Drawable drawable) { 105 | super.setImageDrawable(drawable); 106 | // setImageBitmap calls through to this method 107 | if (attacher != null) { 108 | attacher.update(); 109 | } 110 | } 111 | 112 | @Override 113 | public void setImageResource(int resId) { 114 | super.setImageResource(resId); 115 | if (attacher != null) { 116 | attacher.update(); 117 | } 118 | } 119 | 120 | @Override 121 | public void setImageURI(Uri uri) { 122 | super.setImageURI(uri); 123 | if (attacher != null) { 124 | attacher.update(); 125 | } 126 | } 127 | 128 | @Override 129 | protected boolean setFrame(int l, int t, int r, int b) { 130 | boolean changed = super.setFrame(l, t, r, b); 131 | if (changed) { 132 | attacher.update(); 133 | } 134 | return changed; 135 | } 136 | 137 | public void setRotationTo(float rotationDegree) { 138 | attacher.setRotationTo(rotationDegree); 139 | } 140 | 141 | public void setRotationBy(float rotationDegree) { 142 | attacher.setRotationBy(rotationDegree); 143 | } 144 | 145 | public boolean isZoomable() { 146 | return attacher.isZoomable(); 147 | } 148 | 149 | public void setZoomable(boolean zoomable) { 150 | attacher.setZoomable(zoomable); 151 | } 152 | 153 | public RectF getDisplayRect() { 154 | return attacher.getDisplayRect(); 155 | } 156 | 157 | public void getDisplayMatrix(Matrix matrix) { 158 | attacher.getDisplayMatrix(matrix); 159 | } 160 | 161 | @SuppressWarnings("UnusedReturnValue") public boolean setDisplayMatrix(Matrix finalRectangle) { 162 | return attacher.setDisplayMatrix(finalRectangle); 163 | } 164 | 165 | public void getSuppMatrix(Matrix matrix) { 166 | attacher.getSuppMatrix(matrix); 167 | } 168 | 169 | public boolean setSuppMatrix(Matrix matrix) { 170 | return attacher.setDisplayMatrix(matrix); 171 | } 172 | 173 | public float getMinimumScale() { 174 | return attacher.getMinimumScale(); 175 | } 176 | 177 | public float getMediumScale() { 178 | return attacher.getMediumScale(); 179 | } 180 | 181 | public float getMaximumScale() { 182 | return attacher.getMaximumScale(); 183 | } 184 | 185 | public float getScale() { 186 | return attacher.getScale(); 187 | } 188 | 189 | public void setAllowParentInterceptOnEdge(boolean allow) { 190 | attacher.setAllowParentInterceptOnEdge(allow); 191 | } 192 | 193 | public void setMinimumScale(float minimumScale) { 194 | attacher.setMinimumScale(minimumScale); 195 | } 196 | 197 | public void setMediumScale(float mediumScale) { 198 | attacher.setMediumScale(mediumScale); 199 | } 200 | 201 | public void setMaximumScale(float maximumScale) { 202 | attacher.setMaximumScale(maximumScale); 203 | } 204 | 205 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 206 | attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); 207 | } 208 | 209 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 210 | attacher.setOnMatrixChangeListener(listener); 211 | } 212 | 213 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 214 | attacher.setOnPhotoTapListener(listener); 215 | } 216 | 217 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { 218 | attacher.setOnOutsidePhotoTapListener(listener); 219 | } 220 | 221 | public void setOnViewTapListener(OnViewTapListener listener) { 222 | attacher.setOnViewTapListener(listener); 223 | } 224 | 225 | public void setOnViewDragListener(OnViewDragListener listener) { 226 | attacher.setOnViewDragListener(listener); 227 | } 228 | 229 | public void setScale(float scale) { 230 | attacher.setScale(scale); 231 | } 232 | 233 | public void setScale(float scale, boolean animate) { 234 | attacher.setScale(scale, animate); 235 | } 236 | 237 | public void setScale(float scale, float focalX, float focalY, boolean animate) { 238 | attacher.setScale(scale, focalX, focalY, animate); 239 | } 240 | 241 | public void setZoomTransitionDuration(int milliseconds) { 242 | attacher.setZoomTransitionDuration(milliseconds); 243 | } 244 | 245 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { 246 | attacher.setOnDoubleTapListener(onDoubleTapListener); 247 | } 248 | 249 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { 250 | attacher.setOnScaleChangeListener(onScaleChangedListener); 251 | } 252 | 253 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 254 | attacher.setOnSingleFlingListener(onSingleFlingListener); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/PhotoViewAttacher.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 |

4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 |

8 | http://www.apache.org/licenses/LICENSE-2.0 9 |

10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Matrix; 20 | import android.graphics.Matrix.ScaleToFit; 21 | import android.graphics.RectF; 22 | import android.graphics.drawable.Drawable; 23 | import android.view.GestureDetector; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.view.View.OnLongClickListener; 27 | import android.view.ViewParent; 28 | import android.view.animation.AccelerateDecelerateInterpolator; 29 | import android.view.animation.Interpolator; 30 | import android.widget.ImageView; 31 | import android.widget.ImageView.ScaleType; 32 | import android.widget.OverScroller; 33 | 34 | /** 35 | * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc. 36 | * It is made public in case you need to subclass something other than AppCompatImageView and still 37 | * gain the functionality that {@link PhotoView} offers 38 | */ 39 | public class PhotoViewAttacher implements View.OnTouchListener, 40 | View.OnLayoutChangeListener { 41 | 42 | private static float DEFAULT_MAX_SCALE = 3.0f; 43 | private static float DEFAULT_MID_SCALE = 1.75f; 44 | private static float DEFAULT_MIN_SCALE = 1.0f; 45 | private static int DEFAULT_ZOOM_DURATION = 200; 46 | 47 | private static final int HORIZONTAL_EDGE_NONE = -1; 48 | private static final int HORIZONTAL_EDGE_LEFT = 0; 49 | private static final int HORIZONTAL_EDGE_RIGHT = 1; 50 | private static final int HORIZONTAL_EDGE_BOTH = 2; 51 | private static final int VERTICAL_EDGE_NONE = -1; 52 | private static final int VERTICAL_EDGE_TOP = 0; 53 | private static final int VERTICAL_EDGE_BOTTOM = 1; 54 | private static final int VERTICAL_EDGE_BOTH = 2; 55 | private static int SINGLE_TOUCH = 1; 56 | 57 | private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); 58 | private int mZoomDuration = DEFAULT_ZOOM_DURATION; 59 | private float mMinScale = DEFAULT_MIN_SCALE; 60 | private float mMidScale = DEFAULT_MID_SCALE; 61 | private float mMaxScale = DEFAULT_MAX_SCALE; 62 | 63 | private boolean mAllowParentInterceptOnEdge = true; 64 | private boolean mBlockParentIntercept = false; 65 | 66 | private ImageView mImageView; 67 | 68 | // Gesture Detectors 69 | private GestureDetector mGestureDetector; 70 | private CustomGestureDetector mScaleDragDetector; 71 | 72 | // These are set so we don't keep allocating them on the heap 73 | private final Matrix mBaseMatrix = new Matrix(); 74 | private final Matrix mDrawMatrix = new Matrix(); 75 | private final Matrix mSuppMatrix = new Matrix(); 76 | private final RectF mDisplayRect = new RectF(); 77 | private final float[] mMatrixValues = new float[9]; 78 | 79 | // Listeners 80 | private OnMatrixChangedListener mMatrixChangeListener; 81 | private OnPhotoTapListener mPhotoTapListener; 82 | private OnOutsidePhotoTapListener mOutsidePhotoTapListener; 83 | private OnViewTapListener mViewTapListener; 84 | private View.OnClickListener mOnClickListener; 85 | private OnLongClickListener mLongClickListener; 86 | private OnScaleChangedListener mScaleChangeListener; 87 | private OnSingleFlingListener mSingleFlingListener; 88 | private OnViewDragListener mOnViewDragListener; 89 | 90 | private FlingRunnable mCurrentFlingRunnable; 91 | private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 92 | private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 93 | private float mBaseRotation; 94 | 95 | private boolean mZoomEnabled = true; 96 | private ScaleType mScaleType = ScaleType.FIT_CENTER; 97 | 98 | private OnGestureListener onGestureListener = new OnGestureListener() { 99 | @Override 100 | public void onDrag(float dx, float dy) { 101 | if (mScaleDragDetector.isScaling()) { 102 | return; // Do not drag if we are already scaling 103 | } 104 | if (mOnViewDragListener != null) { 105 | mOnViewDragListener.onDrag(dx, dy); 106 | } 107 | mSuppMatrix.postTranslate(dx, dy); 108 | checkAndDisplayMatrix(); 109 | 110 | /* 111 | * Here we decide whether to let the ImageView's parent to start taking 112 | * over the touch event. 113 | * 114 | * First we check whether this function is enabled. We never want the 115 | * parent to take over if we're scaling. We then check the edge we're 116 | * on, and the direction of the scroll (i.e. if we're pulling against 117 | * the edge, aka 'overscrolling', let the parent take over). 118 | */ 119 | ViewParent parent = mImageView.getParent(); 120 | if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { 121 | if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH 122 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) 123 | || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) 124 | || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) 125 | || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { 126 | if (parent != null) { 127 | parent.requestDisallowInterceptTouchEvent(false); 128 | } 129 | } 130 | } else { 131 | if (parent != null) { 132 | parent.requestDisallowInterceptTouchEvent(true); 133 | } 134 | } 135 | } 136 | 137 | @Override 138 | public void onFling(float startX, float startY, float velocityX, float velocityY) { 139 | mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); 140 | mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), 141 | getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); 142 | mImageView.post(mCurrentFlingRunnable); 143 | } 144 | 145 | @Override 146 | public void onScale(float scaleFactor, float focusX, float focusY) { 147 | onScale(scaleFactor, focusX, focusY, 0, 0); 148 | } 149 | 150 | @Override 151 | public void onScale(float scaleFactor, float focusX, float focusY, float dx, float dy) { 152 | if (getScale() < mMaxScale || scaleFactor < 1f) { 153 | if (mScaleChangeListener != null) { 154 | mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); 155 | } 156 | mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); 157 | mSuppMatrix.postTranslate(dx, dy); 158 | checkAndDisplayMatrix(); 159 | } 160 | } 161 | }; 162 | 163 | public PhotoViewAttacher(ImageView imageView) { 164 | mImageView = imageView; 165 | imageView.setOnTouchListener(this); 166 | imageView.addOnLayoutChangeListener(this); 167 | if (imageView.isInEditMode()) { 168 | return; 169 | } 170 | mBaseRotation = 0.0f; 171 | // Create Gesture Detectors... 172 | mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); 173 | mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { 174 | 175 | // forward long click listener 176 | @Override 177 | public void onLongPress(MotionEvent e) { 178 | if (mLongClickListener != null) { 179 | mLongClickListener.onLongClick(mImageView); 180 | } 181 | } 182 | 183 | @Override 184 | public boolean onFling(MotionEvent e1, MotionEvent e2, 185 | float velocityX, float velocityY) { 186 | if (mSingleFlingListener != null) { 187 | if (getScale() > DEFAULT_MIN_SCALE) { 188 | return false; 189 | } 190 | if (e1.getPointerCount() > SINGLE_TOUCH 191 | || e2.getPointerCount() > SINGLE_TOUCH) { 192 | return false; 193 | } 194 | return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); 195 | } 196 | return false; 197 | } 198 | }); 199 | mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { 200 | @Override 201 | public boolean onSingleTapConfirmed(MotionEvent e) { 202 | if (mOnClickListener != null) { 203 | mOnClickListener.onClick(mImageView); 204 | } 205 | final RectF displayRect = getDisplayRect(); 206 | final float x = e.getX(), y = e.getY(); 207 | if (mViewTapListener != null) { 208 | mViewTapListener.onViewTap(mImageView, x, y); 209 | } 210 | if (displayRect != null) { 211 | // Check to see if the user tapped on the photo 212 | if (displayRect.contains(x, y)) { 213 | float xResult = (x - displayRect.left) 214 | / displayRect.width(); 215 | float yResult = (y - displayRect.top) 216 | / displayRect.height(); 217 | if (mPhotoTapListener != null) { 218 | mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); 219 | } 220 | return true; 221 | } else { 222 | if (mOutsidePhotoTapListener != null) { 223 | mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); 224 | } 225 | } 226 | } 227 | return false; 228 | } 229 | 230 | @Override 231 | public boolean onDoubleTap(MotionEvent ev) { 232 | try { 233 | float scale = getScale(); 234 | float x = ev.getX(); 235 | float y = ev.getY(); 236 | if (scale < getMediumScale()) { 237 | setScale(getMediumScale(), x, y, true); 238 | } else if (scale >= getMediumScale() && scale < getMaximumScale()) { 239 | setScale(getMaximumScale(), x, y, true); 240 | } else { 241 | setScale(getMinimumScale(), x, y, true); 242 | } 243 | } catch (ArrayIndexOutOfBoundsException e) { 244 | // Can sometimes happen when getX() and getY() is called 245 | } 246 | return true; 247 | } 248 | 249 | @Override 250 | public boolean onDoubleTapEvent(MotionEvent e) { 251 | // Wait for the confirmed onDoubleTap() instead 252 | return false; 253 | } 254 | }); 255 | } 256 | 257 | public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { 258 | this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); 259 | } 260 | 261 | public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { 262 | this.mScaleChangeListener = onScaleChangeListener; 263 | } 264 | 265 | public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { 266 | this.mSingleFlingListener = onSingleFlingListener; 267 | } 268 | 269 | @Deprecated 270 | public boolean isZoomEnabled() { 271 | return mZoomEnabled; 272 | } 273 | 274 | public RectF getDisplayRect() { 275 | checkMatrixBounds(); 276 | return getDisplayRect(getDrawMatrix()); 277 | } 278 | 279 | public boolean setDisplayMatrix(Matrix finalMatrix) { 280 | if (finalMatrix == null) { 281 | throw new IllegalArgumentException("Matrix cannot be null"); 282 | } 283 | if (mImageView.getDrawable() == null) { 284 | return false; 285 | } 286 | mSuppMatrix.set(finalMatrix); 287 | checkAndDisplayMatrix(); 288 | return true; 289 | } 290 | 291 | public void setBaseRotation(final float degrees) { 292 | mBaseRotation = degrees % 360; 293 | update(); 294 | setRotationBy(mBaseRotation); 295 | checkAndDisplayMatrix(); 296 | } 297 | 298 | public void setRotationTo(float degrees) { 299 | mSuppMatrix.setRotate(degrees % 360); 300 | checkAndDisplayMatrix(); 301 | } 302 | 303 | public void setRotationBy(float degrees) { 304 | mSuppMatrix.postRotate(degrees % 360); 305 | checkAndDisplayMatrix(); 306 | } 307 | 308 | public float getMinimumScale() { 309 | return mMinScale; 310 | } 311 | 312 | public float getMediumScale() { 313 | return mMidScale; 314 | } 315 | 316 | public float getMaximumScale() { 317 | return mMaxScale; 318 | } 319 | 320 | public float getScale() { 321 | return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow 322 | (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); 323 | } 324 | 325 | public ScaleType getScaleType() { 326 | return mScaleType; 327 | } 328 | 329 | @Override 330 | public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int 331 | oldRight, int oldBottom) { 332 | // Update our base matrix, as the bounds have changed 333 | if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { 334 | updateBaseMatrix(mImageView.getDrawable()); 335 | } 336 | } 337 | 338 | @Override 339 | public boolean onTouch(View v, MotionEvent ev) { 340 | boolean handled = false; 341 | if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { 342 | switch (ev.getAction()) { 343 | case MotionEvent.ACTION_DOWN: 344 | ViewParent parent = v.getParent(); 345 | // First, disable the Parent from intercepting the touch 346 | // event 347 | if (parent != null) { 348 | parent.requestDisallowInterceptTouchEvent(true); 349 | } 350 | // If we're flinging, and the user presses down, cancel 351 | // fling 352 | cancelFling(); 353 | break; 354 | case MotionEvent.ACTION_CANCEL: 355 | case MotionEvent.ACTION_UP: 356 | // If the user has zoomed less than min scale, zoom back 357 | // to min scale 358 | if (getScale() < mMinScale) { 359 | RectF rect = getDisplayRect(); 360 | if (rect != null) { 361 | v.post(new AnimatedZoomRunnable(getScale(), mMinScale, 362 | rect.centerX(), rect.centerY())); 363 | handled = true; 364 | } 365 | } else if (getScale() > mMaxScale) { 366 | RectF rect = getDisplayRect(); 367 | if (rect != null) { 368 | v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, 369 | rect.centerX(), rect.centerY())); 370 | handled = true; 371 | } 372 | } 373 | break; 374 | } 375 | // Try the Scale/Drag detector 376 | if (mScaleDragDetector != null) { 377 | boolean wasScaling = mScaleDragDetector.isScaling(); 378 | boolean wasDragging = mScaleDragDetector.isDragging(); 379 | handled = mScaleDragDetector.onTouchEvent(ev); 380 | boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); 381 | boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); 382 | mBlockParentIntercept = didntScale && didntDrag; 383 | } 384 | // Check to see if the user double tapped 385 | if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { 386 | handled = true; 387 | } 388 | 389 | } 390 | return handled; 391 | } 392 | 393 | public void setAllowParentInterceptOnEdge(boolean allow) { 394 | mAllowParentInterceptOnEdge = allow; 395 | } 396 | 397 | public void setMinimumScale(float minimumScale) { 398 | Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); 399 | mMinScale = minimumScale; 400 | } 401 | 402 | public void setMediumScale(float mediumScale) { 403 | Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); 404 | mMidScale = mediumScale; 405 | } 406 | 407 | public void setMaximumScale(float maximumScale) { 408 | Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); 409 | mMaxScale = maximumScale; 410 | } 411 | 412 | public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { 413 | Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); 414 | mMinScale = minimumScale; 415 | mMidScale = mediumScale; 416 | mMaxScale = maximumScale; 417 | } 418 | 419 | public void setOnLongClickListener(OnLongClickListener listener) { 420 | mLongClickListener = listener; 421 | } 422 | 423 | public void setOnClickListener(View.OnClickListener listener) { 424 | mOnClickListener = listener; 425 | } 426 | 427 | public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { 428 | mMatrixChangeListener = listener; 429 | } 430 | 431 | public void setOnPhotoTapListener(OnPhotoTapListener listener) { 432 | mPhotoTapListener = listener; 433 | } 434 | 435 | public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { 436 | this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; 437 | } 438 | 439 | public void setOnViewTapListener(OnViewTapListener listener) { 440 | mViewTapListener = listener; 441 | } 442 | 443 | public void setOnViewDragListener(OnViewDragListener listener) { 444 | mOnViewDragListener = listener; 445 | } 446 | 447 | public void setScale(float scale) { 448 | setScale(scale, false); 449 | } 450 | 451 | public void setScale(float scale, boolean animate) { 452 | setScale(scale, 453 | (mImageView.getRight()) / 2, 454 | (mImageView.getBottom()) / 2, 455 | animate); 456 | } 457 | 458 | public void setScale(float scale, float focalX, float focalY, 459 | boolean animate) { 460 | // Check to see if the scale is within bounds 461 | if (scale < mMinScale || scale > mMaxScale) { 462 | throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); 463 | } 464 | if (animate) { 465 | mImageView.post(new AnimatedZoomRunnable(getScale(), scale, 466 | focalX, focalY)); 467 | } else { 468 | mSuppMatrix.setScale(scale, scale, focalX, focalY); 469 | checkAndDisplayMatrix(); 470 | } 471 | } 472 | 473 | /** 474 | * Set the zoom interpolator 475 | * 476 | * @param interpolator the zoom interpolator 477 | */ 478 | public void setZoomInterpolator(Interpolator interpolator) { 479 | mInterpolator = interpolator; 480 | } 481 | 482 | public void setScaleType(ScaleType scaleType) { 483 | if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { 484 | mScaleType = scaleType; 485 | update(); 486 | } 487 | } 488 | 489 | public boolean isZoomable() { 490 | return mZoomEnabled; 491 | } 492 | 493 | public void setZoomable(boolean zoomable) { 494 | mZoomEnabled = zoomable; 495 | update(); 496 | } 497 | 498 | public void update() { 499 | if (mZoomEnabled) { 500 | // Update the base matrix using the current drawable 501 | updateBaseMatrix(mImageView.getDrawable()); 502 | } else { 503 | // Reset the Matrix... 504 | resetMatrix(); 505 | } 506 | } 507 | 508 | /** 509 | * Get the display matrix 510 | * 511 | * @param matrix target matrix to copy to 512 | */ 513 | public void getDisplayMatrix(Matrix matrix) { 514 | matrix.set(getDrawMatrix()); 515 | } 516 | 517 | /** 518 | * Get the current support matrix 519 | */ 520 | public void getSuppMatrix(Matrix matrix) { 521 | matrix.set(mSuppMatrix); 522 | } 523 | 524 | private Matrix getDrawMatrix() { 525 | mDrawMatrix.set(mBaseMatrix); 526 | mDrawMatrix.postConcat(mSuppMatrix); 527 | return mDrawMatrix; 528 | } 529 | 530 | public Matrix getImageMatrix() { 531 | return mDrawMatrix; 532 | } 533 | 534 | public void setZoomTransitionDuration(int milliseconds) { 535 | this.mZoomDuration = milliseconds; 536 | } 537 | 538 | /** 539 | * Helper method that 'unpacks' a Matrix and returns the required value 540 | * 541 | * @param matrix Matrix to unpack 542 | * @param whichValue Which value from Matrix.M* to return 543 | * @return returned value 544 | */ 545 | private float getValue(Matrix matrix, int whichValue) { 546 | matrix.getValues(mMatrixValues); 547 | return mMatrixValues[whichValue]; 548 | } 549 | 550 | /** 551 | * Resets the Matrix back to FIT_CENTER, and then displays its contents 552 | */ 553 | private void resetMatrix() { 554 | mSuppMatrix.reset(); 555 | setRotationBy(mBaseRotation); 556 | setImageViewMatrix(getDrawMatrix()); 557 | checkMatrixBounds(); 558 | } 559 | 560 | private void setImageViewMatrix(Matrix matrix) { 561 | mImageView.setImageMatrix(matrix); 562 | // Call MatrixChangedListener if needed 563 | if (mMatrixChangeListener != null) { 564 | RectF displayRect = getDisplayRect(matrix); 565 | if (displayRect != null) { 566 | mMatrixChangeListener.onMatrixChanged(displayRect); 567 | } 568 | } 569 | } 570 | 571 | /** 572 | * Helper method that simply checks the Matrix, and then displays the result 573 | */ 574 | private void checkAndDisplayMatrix() { 575 | if (checkMatrixBounds()) { 576 | setImageViewMatrix(getDrawMatrix()); 577 | } 578 | } 579 | 580 | /** 581 | * Helper method that maps the supplied Matrix to the current Drawable 582 | * 583 | * @param matrix - Matrix to map Drawable against 584 | * @return RectF - Displayed Rectangle 585 | */ 586 | private RectF getDisplayRect(Matrix matrix) { 587 | Drawable d = mImageView.getDrawable(); 588 | if (d != null) { 589 | mDisplayRect.set(0, 0, d.getIntrinsicWidth(), 590 | d.getIntrinsicHeight()); 591 | matrix.mapRect(mDisplayRect); 592 | return mDisplayRect; 593 | } 594 | return null; 595 | } 596 | 597 | /** 598 | * Calculate Matrix for FIT_CENTER 599 | * 600 | * @param drawable - Drawable being displayed 601 | */ 602 | private void updateBaseMatrix(Drawable drawable) { 603 | if (drawable == null) { 604 | return; 605 | } 606 | final float viewWidth = getImageViewWidth(mImageView); 607 | final float viewHeight = getImageViewHeight(mImageView); 608 | final int drawableWidth = drawable.getIntrinsicWidth(); 609 | final int drawableHeight = drawable.getIntrinsicHeight(); 610 | mBaseMatrix.reset(); 611 | final float widthScale = viewWidth / drawableWidth; 612 | final float heightScale = viewHeight / drawableHeight; 613 | if (mScaleType == ScaleType.CENTER) { 614 | mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, 615 | (viewHeight - drawableHeight) / 2F); 616 | 617 | } else if (mScaleType == ScaleType.CENTER_CROP) { 618 | float scale = Math.max(widthScale, heightScale); 619 | mBaseMatrix.postScale(scale, scale); 620 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 621 | (viewHeight - drawableHeight * scale) / 2F); 622 | 623 | } else if (mScaleType == ScaleType.CENTER_INSIDE) { 624 | float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); 625 | mBaseMatrix.postScale(scale, scale); 626 | mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, 627 | (viewHeight - drawableHeight * scale) / 2F); 628 | 629 | } else { 630 | RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); 631 | RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); 632 | if ((int) mBaseRotation % 180 != 0) { 633 | mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); 634 | } 635 | switch (mScaleType) { 636 | case FIT_CENTER: 637 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); 638 | break; 639 | case FIT_START: 640 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); 641 | break; 642 | case FIT_END: 643 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); 644 | break; 645 | case FIT_XY: 646 | mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); 647 | break; 648 | default: 649 | break; 650 | } 651 | } 652 | resetMatrix(); 653 | } 654 | 655 | private boolean checkMatrixBounds() { 656 | final RectF rect = getDisplayRect(getDrawMatrix()); 657 | if (rect == null) { 658 | return false; 659 | } 660 | final float height = rect.height(), width = rect.width(); 661 | float deltaX = 0, deltaY = 0; 662 | final int viewHeight = getImageViewHeight(mImageView); 663 | if (height <= viewHeight) { 664 | switch (mScaleType) { 665 | case FIT_START: 666 | deltaY = -rect.top; 667 | break; 668 | case FIT_END: 669 | deltaY = viewHeight - height - rect.top; 670 | break; 671 | default: 672 | deltaY = (viewHeight - height) / 2 - rect.top; 673 | break; 674 | } 675 | mVerticalScrollEdge = VERTICAL_EDGE_BOTH; 676 | } else if (rect.top > 0) { 677 | mVerticalScrollEdge = VERTICAL_EDGE_TOP; 678 | deltaY = -rect.top; 679 | } else if (rect.bottom < viewHeight) { 680 | mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; 681 | deltaY = viewHeight - rect.bottom; 682 | } else { 683 | mVerticalScrollEdge = VERTICAL_EDGE_NONE; 684 | } 685 | final int viewWidth = getImageViewWidth(mImageView); 686 | if (width <= viewWidth) { 687 | switch (mScaleType) { 688 | case FIT_START: 689 | deltaX = -rect.left; 690 | break; 691 | case FIT_END: 692 | deltaX = viewWidth - width - rect.left; 693 | break; 694 | default: 695 | deltaX = (viewWidth - width) / 2 - rect.left; 696 | break; 697 | } 698 | mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; 699 | } else if (rect.left > 0) { 700 | mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; 701 | deltaX = -rect.left; 702 | } else if (rect.right < viewWidth) { 703 | deltaX = viewWidth - rect.right; 704 | mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; 705 | } else { 706 | mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; 707 | } 708 | // Finally actually translate the matrix 709 | mSuppMatrix.postTranslate(deltaX, deltaY); 710 | return true; 711 | } 712 | 713 | private int getImageViewWidth(ImageView imageView) { 714 | return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); 715 | } 716 | 717 | private int getImageViewHeight(ImageView imageView) { 718 | return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); 719 | } 720 | 721 | private void cancelFling() { 722 | if (mCurrentFlingRunnable != null) { 723 | mCurrentFlingRunnable.cancelFling(); 724 | mCurrentFlingRunnable = null; 725 | } 726 | } 727 | 728 | private class AnimatedZoomRunnable implements Runnable { 729 | 730 | private final float mFocalX, mFocalY; 731 | private final long mStartTime; 732 | private final float mZoomStart, mZoomEnd; 733 | 734 | public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, 735 | final float focalX, final float focalY) { 736 | mFocalX = focalX; 737 | mFocalY = focalY; 738 | mStartTime = System.currentTimeMillis(); 739 | mZoomStart = currentZoom; 740 | mZoomEnd = targetZoom; 741 | } 742 | 743 | @Override 744 | public void run() { 745 | float t = interpolate(); 746 | float scale = mZoomStart + t * (mZoomEnd - mZoomStart); 747 | float deltaScale = scale / getScale(); 748 | onGestureListener.onScale(deltaScale, mFocalX, mFocalY); 749 | // We haven't hit our target scale yet, so post ourselves again 750 | if (t < 1f) { 751 | Compat.postOnAnimation(mImageView, this); 752 | } 753 | } 754 | 755 | private float interpolate() { 756 | float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; 757 | t = Math.min(1f, t); 758 | t = mInterpolator.getInterpolation(t); 759 | return t; 760 | } 761 | } 762 | 763 | private class FlingRunnable implements Runnable { 764 | 765 | private final OverScroller mScroller; 766 | private int mCurrentX, mCurrentY; 767 | 768 | public FlingRunnable(Context context) { 769 | mScroller = new OverScroller(context); 770 | } 771 | 772 | public void cancelFling() { 773 | mScroller.forceFinished(true); 774 | } 775 | 776 | public void fling(int viewWidth, int viewHeight, int velocityX, 777 | int velocityY) { 778 | final RectF rect = getDisplayRect(); 779 | if (rect == null) { 780 | return; 781 | } 782 | final int startX = Math.round(-rect.left); 783 | final int minX, maxX, minY, maxY; 784 | if (viewWidth < rect.width()) { 785 | minX = 0; 786 | maxX = Math.round(rect.width() - viewWidth); 787 | } else { 788 | minX = maxX = startX; 789 | } 790 | final int startY = Math.round(-rect.top); 791 | if (viewHeight < rect.height()) { 792 | minY = 0; 793 | maxY = Math.round(rect.height() - viewHeight); 794 | } else { 795 | minY = maxY = startY; 796 | } 797 | mCurrentX = startX; 798 | mCurrentY = startY; 799 | // If we actually can move, fling the scroller 800 | if (startX != maxX || startY != maxY) { 801 | mScroller.fling(startX, startY, velocityX, velocityY, minX, 802 | maxX, minY, maxY, 0, 0); 803 | } 804 | } 805 | 806 | @Override 807 | public void run() { 808 | if (mScroller.isFinished()) { 809 | return; // remaining post that should not be handled 810 | } 811 | if (mScroller.computeScrollOffset()) { 812 | final int newX = mScroller.getCurrX(); 813 | final int newY = mScroller.getCurrY(); 814 | mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); 815 | checkAndDisplayMatrix(); 816 | mCurrentX = newX; 817 | mCurrentY = newY; 818 | // Post On animation 819 | Compat.postOnAnimation(mImageView, this); 820 | } 821 | } 822 | } 823 | } 824 | -------------------------------------------------------------------------------- /photoview/src/main/java/com/github/chrisbanes/photoview/Util.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview; 2 | 3 | import android.view.MotionEvent; 4 | import android.widget.ImageView; 5 | 6 | class Util { 7 | 8 | static void checkZoomLevels(float minZoom, float midZoom, 9 | float maxZoom) { 10 | if (minZoom >= midZoom) { 11 | throw new IllegalArgumentException( 12 | "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); 13 | } else if (midZoom >= maxZoom) { 14 | throw new IllegalArgumentException( 15 | "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); 16 | } 17 | } 18 | 19 | static boolean hasDrawable(ImageView imageView) { 20 | return imageView.getDrawable() != null; 21 | } 22 | 23 | static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { 24 | if (scaleType == null) { 25 | return false; 26 | } 27 | switch (scaleType) { 28 | case MATRIX: 29 | throw new IllegalStateException("Matrix scale type is not supported"); 30 | } 31 | return true; 32 | } 33 | 34 | static int getPointerIndex(int action) { 35 | return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | apply plugin: "kotlin-android" 4 | 5 | android { 6 | compileSdkVersion rootProject.ext.sdkVersion 7 | 8 | defaultConfig { 9 | applicationId "uk.co.senab.photoview.sample" 10 | minSdkVersion rootProject.ext.minSdkVersion 11 | targetSdkVersion rootProject.ext.sdkVersion 12 | versionCode 100 13 | versionName "1.0" 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_8 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | } 20 | 21 | lintOptions { 22 | abortOnError false 23 | } 24 | } 25 | 26 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { 27 | kotlinOptions { 28 | jvmTarget = "1.8" 29 | } 30 | } 31 | 32 | dependencies { 33 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 34 | implementation "androidx.appcompat:appcompat:1.1.0" 35 | implementation "androidx.recyclerview:recyclerview:1.1.0" 36 | 37 | implementation "com.google.android.material:material:1.1.0" 38 | 39 | implementation "com.squareup.picasso:picasso:2.5.2" 40 | implementation("io.coil-kt:coil:0.9.1") 41 | 42 | implementation project(":photoview") 43 | } 44 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ActivityTransitionActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview.sample; 17 | 18 | import android.content.Intent; 19 | import android.os.Build; 20 | import android.os.Bundle; 21 | import android.view.View; 22 | import android.widget.Toast; 23 | 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.core.app.ActivityOptionsCompat; 26 | import androidx.recyclerview.widget.GridLayoutManager; 27 | import androidx.recyclerview.widget.RecyclerView; 28 | 29 | public class ActivityTransitionActivity extends AppCompatActivity { 30 | 31 | @Override 32 | public void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_transition); 35 | 36 | RecyclerView list = findViewById(R.id.list); 37 | list.setLayoutManager(new GridLayoutManager(this, 2)); 38 | ImageAdapter imageAdapter = new ImageAdapter(new ImageAdapter.Listener() { 39 | @Override 40 | public void onImageClicked(View view) { 41 | transition(view); 42 | } 43 | }); 44 | list.setAdapter(imageAdapter); 45 | } 46 | 47 | private void transition(View view) { 48 | if (Build.VERSION.SDK_INT < 21) { 49 | Toast.makeText(ActivityTransitionActivity.this, "21+ only, keep out", Toast.LENGTH_SHORT).show(); 50 | } else { 51 | Intent intent = new Intent(ActivityTransitionActivity.this, ActivityTransitionToActivity.class); 52 | ActivityOptionsCompat options = ActivityOptionsCompat. 53 | makeSceneTransitionAnimation(ActivityTransitionActivity.this, view, getString(R.string.transition_test)); 54 | startActivity(intent, options.toBundle()); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ActivityTransitionToActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.annotation.Nullable; 6 | import androidx.appcompat.app.AppCompatActivity; 7 | 8 | /** 9 | * Activity that gets transitioned to 10 | */ 11 | public class ActivityTransitionToActivity extends AppCompatActivity { 12 | 13 | @Override 14 | protected void onCreate(@Nullable Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setContentView(R.layout.activity_transition_to); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/CoilSampleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import coil.api.load 6 | import com.github.chrisbanes.photoview.PhotoView 7 | 8 | class CoilSampleActivity : AppCompatActivity() { 9 | 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | setContentView(R.layout.activity_simple) 13 | 14 | val photoView = findViewById(R.id.iv_photo) 15 | photoView.load("https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1&auto=format&fit=crop&w=3300&q=80") { 16 | crossfade(true) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyDrawerLayout.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.content.Context; 4 | import android.view.MotionEvent; 5 | 6 | import androidx.drawerlayout.widget.DrawerLayout; 7 | 8 | /** 9 | * Hacky fix for Issue #4 and 10 | * http://code.google.com/p/android/issues/detail?id=18990 11 | *

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

16 | * There's not much I can do in my code for now, but we can mask the result by 17 | * just catching the problem and ignoring it. 18 | */ 19 | public class HackyDrawerLayout extends DrawerLayout { 20 | 21 | public HackyDrawerLayout(Context context) { 22 | super(context); 23 | } 24 | 25 | @Override 26 | public boolean onInterceptTouchEvent(MotionEvent ev) { 27 | try { 28 | return super.onInterceptTouchEvent(ev); 29 | } catch (IllegalArgumentException e) { 30 | e.printStackTrace(); 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/HackyViewPager.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.MotionEvent; 6 | 7 | import androidx.viewpager.widget.ViewPager; 8 | 9 | /** 10 | * Hacky fix for Issue #4 and 11 | * http://code.google.com/p/android/issues/detail?id=18990 12 | *

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

17 | * There's not much I can do in my code for now, but we can mask the result by 18 | * just catching the problem and ignoring it. 19 | * 20 | * @author Chris Banes 21 | */ 22 | public class HackyViewPager extends ViewPager { 23 | 24 | public HackyViewPager(Context context) { 25 | super(context); 26 | } 27 | 28 | public HackyViewPager(Context context, AttributeSet attrs) { 29 | super(context, attrs); 30 | } 31 | 32 | @Override 33 | public boolean onInterceptTouchEvent(MotionEvent ev) { 34 | try { 35 | return super.onInterceptTouchEvent(ev); 36 | } catch (IllegalArgumentException e) { 37 | return false; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ImageAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import androidx.recyclerview.widget.RecyclerView; 7 | 8 | /** 9 | * Image adapter 10 | */ 11 | public class ImageAdapter extends RecyclerView.Adapter { 12 | 13 | Listener mListener; 14 | 15 | public ImageAdapter(Listener listener) { 16 | mListener = listener; 17 | } 18 | 19 | @Override 20 | public ImageViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 21 | ImageViewHolder holder = ImageViewHolder.inflate(parent); 22 | holder.itemView.setOnClickListener(new View.OnClickListener() { 23 | @Override 24 | public void onClick(View view) { 25 | mListener.onImageClicked(view); 26 | } 27 | }); 28 | return holder; 29 | } 30 | 31 | @Override 32 | public void onBindViewHolder(ImageViewHolder holder, int position) { 33 | 34 | } 35 | 36 | @Override 37 | public int getItemCount() { 38 | return 20; 39 | } 40 | 41 | public interface Listener { 42 | void onImageClicked(View view); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ImageViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.view.LayoutInflater; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.TextView; 7 | 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | /** 11 | * Image in recyclerview 12 | */ 13 | public class ImageViewHolder extends RecyclerView.ViewHolder { 14 | 15 | public static ImageViewHolder inflate(ViewGroup parent) { 16 | View view = LayoutInflater.from(parent.getContext()) 17 | .inflate(R.layout.item_image, parent, false); 18 | return new ImageViewHolder(view); 19 | } 20 | 21 | public TextView mTextTitle; 22 | 23 | public ImageViewHolder(View view) { 24 | super(view); 25 | mTextTitle = view.findViewById(R.id.title); 26 | } 27 | 28 | private void bind(String title) { 29 | mTextTitle.setText(title); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ImmersiveActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.os.Build; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.view.View; 7 | import android.widget.ImageView; 8 | 9 | import com.github.chrisbanes.photoview.OnPhotoTapListener; 10 | import com.github.chrisbanes.photoview.PhotoView; 11 | import com.squareup.picasso.Picasso; 12 | 13 | import androidx.annotation.Nullable; 14 | import androidx.appcompat.app.AppCompatActivity; 15 | 16 | import static android.R.attr.uiOptions; 17 | 18 | /** 19 | * Shows immersive image viewer 20 | */ 21 | public class ImmersiveActivity extends AppCompatActivity { 22 | 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | setContentView(R.layout.activity_immersive); 27 | 28 | PhotoView photoView = findViewById(R.id.photo_view); 29 | Picasso.with(this) 30 | .load("http://pbs.twimg.com/media/Bist9mvIYAAeAyQ.jpg") 31 | .into(photoView); 32 | photoView.setOnPhotoTapListener(new OnPhotoTapListener() { 33 | @Override 34 | public void onPhotoTap(ImageView view, float x, float y) { 35 | //fullScreen(); 36 | } 37 | }); 38 | fullScreen(); 39 | } 40 | 41 | public void fullScreen() { 42 | 43 | // BEGIN_INCLUDE (get_current_ui_flags) 44 | // The UI options currently enabled are represented by a bitfield. 45 | // getSystemUiVisibility() gives us that bitfield. 46 | int uiOptions = getWindow().getDecorView().getSystemUiVisibility(); 47 | int newUiOptions = uiOptions; 48 | // END_INCLUDE (get_current_ui_flags) 49 | // BEGIN_INCLUDE (toggle_ui_flags) 50 | boolean isImmersiveModeEnabled = isImmersiveModeEnabled(); 51 | if (isImmersiveModeEnabled) { 52 | Log.i("TEST", "Turning immersive mode mode off. "); 53 | } else { 54 | Log.i("TEST", "Turning immersive mode mode on."); 55 | } 56 | 57 | // Navigation bar hiding: Backwards compatible to ICS. 58 | if (Build.VERSION.SDK_INT >= 14) { 59 | newUiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; 60 | } 61 | 62 | // Status bar hiding: Backwards compatible to Jellybean 63 | if (Build.VERSION.SDK_INT >= 16) { 64 | newUiOptions ^= View.SYSTEM_UI_FLAG_FULLSCREEN; 65 | } 66 | 67 | // Immersive mode: Backward compatible to KitKat. 68 | // Note that this flag doesn't do anything by itself, it only augments the behavior 69 | // of HIDE_NAVIGATION and FLAG_FULLSCREEN. For the purposes of this sample 70 | // all three flags are being toggled together. 71 | // Note that there are two immersive mode UI flags, one of which is referred to as "sticky". 72 | // Sticky immersive mode differs in that it makes the navigation and status bars 73 | // semi-transparent, and the UI flag does not get cleared when the user interacts with 74 | // the screen. 75 | if (Build.VERSION.SDK_INT >= 18) { 76 | newUiOptions ^= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 77 | } 78 | 79 | getWindow().getDecorView().setSystemUiVisibility(newUiOptions); 80 | //END_INCLUDE (set_ui_flags) 81 | } 82 | 83 | private boolean isImmersiveModeEnabled() { 84 | return ((uiOptions | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) == uiOptions); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/LauncherActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview.sample; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.os.Bundle; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.appcompat.widget.Toolbar; 28 | import androidx.recyclerview.widget.LinearLayoutManager; 29 | import androidx.recyclerview.widget.RecyclerView; 30 | 31 | public class LauncherActivity extends AppCompatActivity { 32 | 33 | public static final String[] options = { 34 | "Simple Sample", 35 | "ViewPager Sample", 36 | "Rotation Sample", 37 | "Picasso Sample", 38 | "Coil Sample", 39 | "Activity Transition Sample", 40 | "Immersive Sample" 41 | }; 42 | 43 | @Override 44 | protected void onCreate(Bundle savedInstanceState) { 45 | super.onCreate(savedInstanceState); 46 | setContentView(R.layout.activity_launcher); 47 | Toolbar toolbar = findViewById(R.id.toolbar); 48 | toolbar.setTitle(R.string.app_name); 49 | RecyclerView recyclerView = findViewById(R.id.list); 50 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 51 | recyclerView.setAdapter(new ItemAdapter()); 52 | } 53 | 54 | 55 | private static class ItemAdapter extends RecyclerView.Adapter { 56 | @Override 57 | public ItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 58 | final ItemViewHolder holder = ItemViewHolder.newInstance(parent); 59 | holder.itemView.setOnClickListener(v -> { 60 | Class clazz; 61 | 62 | switch (holder.getAdapterPosition()) { 63 | default: 64 | case 0: 65 | clazz = SimpleSampleActivity.class; 66 | break; 67 | case 1: 68 | clazz = ViewPagerActivity.class; 69 | break; 70 | case 2: 71 | clazz = RotationSampleActivity.class; 72 | break; 73 | case 3: 74 | clazz = PicassoSampleActivity.class; 75 | break; 76 | case 4: 77 | clazz = CoilSampleActivity.class; 78 | break; 79 | case 5: 80 | clazz = ActivityTransitionActivity.class; 81 | break; 82 | case 6: 83 | clazz = ImmersiveActivity.class; 84 | } 85 | 86 | Context context = holder.itemView.getContext(); 87 | context.startActivity(new Intent(context, clazz)); 88 | }); 89 | return holder; 90 | } 91 | 92 | @Override 93 | public void onBindViewHolder(final ItemViewHolder holder, int position) { 94 | holder.bind(options[position]); 95 | } 96 | 97 | @Override 98 | public int getItemCount() { 99 | return options.length; 100 | } 101 | } 102 | 103 | private static class ItemViewHolder extends RecyclerView.ViewHolder { 104 | 105 | public static ItemViewHolder newInstance(ViewGroup parent) { 106 | View view = LayoutInflater.from(parent.getContext()) 107 | .inflate(R.layout.item_sample, parent, false); 108 | return new ItemViewHolder(view); 109 | } 110 | 111 | public TextView mTextTitle; 112 | 113 | public ItemViewHolder(View view) { 114 | super(view); 115 | mTextTitle = view.findViewById(R.id.title); 116 | } 117 | 118 | private void bind(String title) { 119 | mTextTitle.setText(title); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/PicassoSampleActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.chrisbanes.photoview.sample; 2 | 3 | import android.os.Bundle; 4 | 5 | import com.github.chrisbanes.photoview.PhotoView; 6 | import com.squareup.picasso.Picasso; 7 | 8 | import androidx.appcompat.app.AppCompatActivity; 9 | 10 | public class PicassoSampleActivity extends AppCompatActivity { 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | setContentView(R.layout.activity_simple); 16 | 17 | final PhotoView photoView = findViewById(R.id.iv_photo); 18 | 19 | Picasso.with(this) 20 | .load("https://images.unsplash.com/photo-1577643816920-65b43ba99fba?ixlib=rb-1.2.1&auto=format&fit=crop&w=3300&q=80") 21 | .into(photoView); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/RotationSampleActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview.sample; 17 | 18 | import android.os.Bundle; 19 | import android.os.Handler; 20 | import android.view.MenuItem; 21 | 22 | import com.github.chrisbanes.photoview.PhotoView; 23 | 24 | import androidx.appcompat.app.AppCompatActivity; 25 | import androidx.appcompat.widget.Toolbar; 26 | 27 | public class RotationSampleActivity extends AppCompatActivity { 28 | 29 | private PhotoView photo; 30 | private final Handler handler = new Handler(); 31 | private boolean rotating = false; 32 | 33 | @Override 34 | public void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | setContentView(R.layout.activity_rotation_sample); 37 | Toolbar toolbar = findViewById(R.id.toolbar); 38 | toolbar.inflateMenu(R.menu.rotation); 39 | toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { 40 | @Override 41 | public boolean onMenuItemClick(MenuItem item) { 42 | switch (item.getItemId()) { 43 | case R.id.action_rotate_10_right: 44 | photo.setRotationBy(10); 45 | return true; 46 | case R.id.action_rotate_10_left: 47 | photo.setRotationBy(-10); 48 | return true; 49 | case R.id.action_toggle_automatic_rotation: 50 | toggleRotation(); 51 | return true; 52 | case R.id.action_reset_to_0: 53 | photo.setRotationTo(0); 54 | return true; 55 | case R.id.action_reset_to_90: 56 | photo.setRotationTo(90); 57 | return true; 58 | case R.id.action_reset_to_180: 59 | photo.setRotationTo(180); 60 | return true; 61 | case R.id.action_reset_to_270: 62 | photo.setRotationTo(270); 63 | return true; 64 | } 65 | return false; 66 | } 67 | }); 68 | photo = findViewById(R.id.iv_photo); 69 | photo.setImageResource(R.drawable.wallpaper); 70 | } 71 | 72 | @Override 73 | protected void onPause() { 74 | super.onPause(); 75 | handler.removeCallbacksAndMessages(null); 76 | } 77 | 78 | private void toggleRotation() { 79 | if (rotating) { 80 | handler.removeCallbacksAndMessages(null); 81 | } else { 82 | rotateLoop(); 83 | } 84 | rotating = !rotating; 85 | } 86 | 87 | private void rotateLoop() { 88 | handler.postDelayed(new Runnable() { 89 | @Override 90 | public void run() { 91 | photo.setRotationBy(1); 92 | rotateLoop(); 93 | } 94 | }, 15); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/SimpleSampleActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview.sample; 17 | 18 | import android.graphics.Matrix; 19 | import android.graphics.RectF; 20 | import android.graphics.drawable.Drawable; 21 | import android.os.Bundle; 22 | import android.util.Log; 23 | import android.view.MenuItem; 24 | import android.view.MotionEvent; 25 | import android.view.View; 26 | import android.widget.ImageView; 27 | import android.widget.TextView; 28 | import android.widget.Toast; 29 | 30 | import com.github.chrisbanes.photoview.OnMatrixChangedListener; 31 | import com.github.chrisbanes.photoview.OnPhotoTapListener; 32 | import com.github.chrisbanes.photoview.OnSingleFlingListener; 33 | import com.github.chrisbanes.photoview.PhotoView; 34 | 35 | import java.util.Random; 36 | 37 | import androidx.appcompat.app.AppCompatActivity; 38 | import androidx.appcompat.widget.Toolbar; 39 | import androidx.core.content.ContextCompat; 40 | 41 | public class SimpleSampleActivity extends AppCompatActivity { 42 | 43 | static final String PHOTO_TAP_TOAST_STRING = "Photo Tap! X: %.2f %% Y:%.2f %% ID: %d"; 44 | static final String SCALE_TOAST_STRING = "Scaled to: %.2ff"; 45 | static final String FLING_LOG_STRING = "Fling velocityX: %.2f, velocityY: %.2f"; 46 | 47 | private PhotoView mPhotoView; 48 | private TextView mCurrMatrixTv; 49 | 50 | private Toast mCurrentToast; 51 | 52 | private Matrix mCurrentDisplayMatrix = null; 53 | 54 | @Override 55 | public void onCreate(Bundle savedInstanceState) { 56 | super.onCreate(savedInstanceState); 57 | setContentView(R.layout.activity_simple_sample); 58 | 59 | Toolbar toolbar = findViewById(R.id.toolbar); 60 | toolbar.setTitle("Simple Sample"); 61 | toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp); 62 | toolbar.setNavigationOnClickListener(new View.OnClickListener() { 63 | @Override 64 | public void onClick(View v) { 65 | onBackPressed(); 66 | } 67 | }); 68 | toolbar.inflateMenu(R.menu.main_menu); 69 | toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() { 70 | @Override 71 | public boolean onMenuItemClick(MenuItem item) { 72 | switch (item.getItemId()) { 73 | case R.id.menu_zoom_toggle: 74 | mPhotoView.setZoomable(!mPhotoView.isZoomable()); 75 | item.setTitle(mPhotoView.isZoomable() ? R.string.menu_zoom_disable : R.string.menu_zoom_enable); 76 | return true; 77 | 78 | case R.id.menu_scale_fit_center: 79 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER); 80 | return true; 81 | 82 | case R.id.menu_scale_fit_start: 83 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_START); 84 | return true; 85 | 86 | case R.id.menu_scale_fit_end: 87 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_END); 88 | return true; 89 | 90 | case R.id.menu_scale_fit_xy: 91 | mPhotoView.setScaleType(ImageView.ScaleType.FIT_XY); 92 | return true; 93 | 94 | case R.id.menu_scale_scale_center: 95 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER); 96 | return true; 97 | 98 | case R.id.menu_scale_scale_center_crop: 99 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER_CROP); 100 | return true; 101 | 102 | case R.id.menu_scale_scale_center_inside: 103 | mPhotoView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 104 | return true; 105 | 106 | case R.id.menu_scale_random_animate: 107 | case R.id.menu_scale_random: 108 | Random r = new Random(); 109 | 110 | float minScale = mPhotoView.getMinimumScale(); 111 | float maxScale = mPhotoView.getMaximumScale(); 112 | float randomScale = minScale + (r.nextFloat() * (maxScale - minScale)); 113 | mPhotoView.setScale(randomScale, item.getItemId() == R.id.menu_scale_random_animate); 114 | 115 | showToast(String.format(SCALE_TOAST_STRING, randomScale)); 116 | 117 | return true; 118 | case R.id.menu_matrix_restore: 119 | if (mCurrentDisplayMatrix == null) 120 | showToast("You need to capture display matrix first"); 121 | else 122 | mPhotoView.setDisplayMatrix(mCurrentDisplayMatrix); 123 | return true; 124 | case R.id.menu_matrix_capture: 125 | mCurrentDisplayMatrix = new Matrix(); 126 | mPhotoView.getDisplayMatrix(mCurrentDisplayMatrix); 127 | return true; 128 | } 129 | return false; 130 | } 131 | }); 132 | mPhotoView = findViewById(R.id.iv_photo); 133 | mCurrMatrixTv = findViewById(R.id.tv_current_matrix); 134 | 135 | Drawable bitmap = ContextCompat.getDrawable(this, R.drawable.wallpaper); 136 | mPhotoView.setImageDrawable(bitmap); 137 | 138 | // Lets attach some listeners, not required though! 139 | mPhotoView.setOnMatrixChangeListener(new MatrixChangeListener()); 140 | mPhotoView.setOnPhotoTapListener(new PhotoTapListener()); 141 | mPhotoView.setOnSingleFlingListener(new SingleFlingListener()); 142 | } 143 | 144 | private class PhotoTapListener implements OnPhotoTapListener { 145 | 146 | @Override 147 | public void onPhotoTap(ImageView view, float x, float y) { 148 | float xPercentage = x * 100f; 149 | float yPercentage = y * 100f; 150 | 151 | showToast(String.format(PHOTO_TAP_TOAST_STRING, xPercentage, yPercentage, view == null ? 0 : view.getId())); 152 | } 153 | } 154 | 155 | private void showToast(CharSequence text) { 156 | if (mCurrentToast != null) { 157 | mCurrentToast.cancel(); 158 | } 159 | 160 | mCurrentToast = Toast.makeText(SimpleSampleActivity.this, text, Toast.LENGTH_SHORT); 161 | mCurrentToast.show(); 162 | } 163 | 164 | private class MatrixChangeListener implements OnMatrixChangedListener { 165 | 166 | @Override 167 | public void onMatrixChanged(RectF rect) { 168 | mCurrMatrixTv.setText(rect.toString()); 169 | } 170 | } 171 | 172 | private class SingleFlingListener implements OnSingleFlingListener { 173 | 174 | @Override 175 | public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { 176 | Log.d("PhotoView", String.format(FLING_LOG_STRING, velocityX, velocityY)); 177 | return true; 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /sample/src/main/java/com/github/chrisbanes/photoview/sample/ViewPagerActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2011, 2012 Chris Banes. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package com.github.chrisbanes.photoview.sample; 17 | 18 | import android.os.Bundle; 19 | 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.view.ViewGroup.LayoutParams; 23 | 24 | import com.github.chrisbanes.photoview.PhotoView; 25 | 26 | import androidx.appcompat.app.AppCompatActivity; 27 | import androidx.viewpager.widget.PagerAdapter; 28 | import androidx.viewpager.widget.ViewPager; 29 | 30 | public class ViewPagerActivity extends AppCompatActivity { 31 | 32 | @Override 33 | public void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_view_pager); 36 | ViewPager viewPager = findViewById(R.id.view_pager); 37 | viewPager.setAdapter(new SamplePagerAdapter()); 38 | } 39 | 40 | static class SamplePagerAdapter extends PagerAdapter { 41 | 42 | private static final int[] sDrawables = {R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper, 43 | R.drawable.wallpaper, R.drawable.wallpaper, R.drawable.wallpaper}; 44 | 45 | @Override 46 | public int getCount() { 47 | return sDrawables.length; 48 | } 49 | 50 | @Override 51 | public View instantiateItem(ViewGroup container, int position) { 52 | PhotoView photoView = new PhotoView(container.getContext()); 53 | photoView.setImageResource(sDrawables[position]); 54 | // Now just add PhotoView to ViewPager and return it 55 | container.addView(photoView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 56 | return photoView; 57 | } 58 | 59 | @Override 60 | public void destroyItem(ViewGroup container, int position, Object object) { 61 | container.removeView((View) object); 62 | } 63 | 64 | @Override 65 | public boolean isViewFromObject(View view, Object object) { 66 | return view == object; 67 | } 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/wallpaper.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/drawable-nodpi/wallpaper.jpg -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_arrow_back_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_immersive.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_rotation_sample.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 16 | 17 | 18 | 19 | 24 | 25 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_simple.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_simple_sample.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | 11 | 16 | 17 | 18 | 19 | 23 | 24 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_transition.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_transition_to.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_view_pager.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_image.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 23 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/main_menu.xml: -------------------------------------------------------------------------------- 1 | 2 |

5 | 6 | 9 | 12 | 15 | 18 | 21 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 43 | -------------------------------------------------------------------------------- /sample/src/main/res/menu/rotation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 31 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Baseflow/PhotoView/565505d5cb84f5977771b5d2ccb7726338e77224/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #AACE30 4 | #142D3E 5 | #001425 6 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PhotoView Sample 4 | Enable Zoom 5 | Disable Zoom 6 | Change to FIT_CENTER 7 | Change to FIT_START 8 | Change to FIT_END 9 | Change to FIT_XY 10 | Change to CENTER 11 | Change to CENTER_INSIDE 12 | Change to CENTER_CROP 13 | Animate scale to random value 14 | Set scale to random value 15 | Restore Display Matrix 16 | Capture Display Matrix 17 | Extract visible bitmap 18 | 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/values/transitions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | test 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'photoview' 2 | include 'sample' 3 | --------------------------------------------------------------------------------