├── .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/#chrisbanes/PhotoView)
5 |
6 | [
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 |
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 |