├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── Previews ├── ZoomHelper-custom.gif ├── ZoomHelper-simple.gif ├── ZoomHelper_rv.gif └── header.png ├── README.md ├── ZoomHelper ├── .DS_Store ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── .DS_Store │ ├── androidTest │ └── java │ │ └── com │ │ └── aghajari │ │ └── zoomhelper │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── aghajari │ │ │ └── zoomhelper │ │ │ ├── InstanceState.kt │ │ │ ├── PlaceHolderView.kt │ │ │ ├── Utils.kt │ │ │ └── ZoomHelper.kt │ └── res │ │ ├── values-v19 │ │ └── style.xml │ │ └── values │ │ ├── ids.xml │ │ ├── strings.xml │ │ └── style.xml │ └── test │ └── java │ └── com │ └── aghajari │ └── zoomhelper │ └── ExampleUnitTest.java ├── app ├── .DS_Store ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── .DS_Store │ ├── androidTest │ ├── .DS_Store │ └── java │ │ └── com │ │ └── aghajari │ │ └── zoomhelper │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── .DS_Store │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── aghajari │ │ │ └── zoomhelper │ │ │ ├── MainActivity.kt │ │ │ ├── ZoomableActivity.kt │ │ │ ├── recycler │ │ │ ├── Adapter.kt │ │ │ ├── FakeData.kt │ │ │ ├── Model.kt │ │ │ └── RecyclerViewActivity.kt │ │ │ └── simpleact │ │ │ ├── CustomActivity.kt │ │ │ └── SimpleActivity.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── courteney_cox.jpg │ │ ├── david_schwimmer.jpg │ │ ├── ic_launcher_background.xml │ │ ├── jennifer_aniston.jpg │ │ ├── lisa_kudrow.jpg │ │ ├── matt_leblanc.jpg │ │ ├── matthew_perry.jpg │ │ └── place_holder.xml │ │ ├── layout │ │ ├── activity_custom.xml │ │ ├── activity_main.xml │ │ ├── activity_recycler.xml │ │ ├── activity_simple.xml │ │ └── rv_item.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── aghajari │ └── zoomhelper │ └── ExampleUnitTest.kt ├── build.gradle ├── deploy.settings ├── gradle.properties ├── gradle ├── .DS_Store └── wrapper │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── zoom.iml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | .DS_Store 25 | .DS_Store 26 | ZoomHelper/.DS_Store 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Previews/ZoomHelper-custom.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/Previews/ZoomHelper-custom.gif -------------------------------------------------------------------------------- /Previews/ZoomHelper-simple.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/Previews/ZoomHelper-simple.gif -------------------------------------------------------------------------------- /Previews/ZoomHelper_rv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/Previews/ZoomHelper_rv.gif -------------------------------------------------------------------------------- /Previews/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/Previews/header.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZoomHelper 2 | ![ZoomHelper](Previews/header.png) 3 | 4 | [![Platform](https://img.shields.io/badge/platform-android-green.svg)](http://developer.android.com/index.html) 5 | [![API](https://img.shields.io/badge/API-16%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=16) 6 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.aghajari/ZoomHelper.svg?label=Maven%20Central)](https://search.maven.org/artifact/io.github.aghajari/ZoomHelper/1.1.0/aar) 7 | [![Join the chat at https://gitter.im/Aghajari/community](https://badges.gitter.im/Aghajari/community.svg)](https://gitter.im/Aghajari/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | ZoomHelper will make any view to be zoomable just like the Instagram pinch-to-zoom. :wink: 10 | 11 | ![ZoomHelper (RecyclerViewSample)](Previews/ZoomHelper_rv.gif) 12 | 13 | ## Installation 14 | 15 | ZoomHelper is available in the `mavenCentral()`, so you just need to add it as a dependency (Module gradle) 16 | 17 | Gradle 18 | ```gradle 19 | implementation 'io.github.aghajari:ZoomHelper:1.1.0' 20 | ``` 21 | 22 | Maven 23 | ```xml 24 | 25 | io.github.aghajari 26 | ZoomHelper 27 | 1.1.0 28 | pom 29 | 30 | ``` 31 | 32 | ## Usage (Kotlin) 33 | 34 | just override dispatchTouchEvent in your Activity and pass all touch events to ZoomHelper! 35 | ```kotlin 36 | 37 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 38 | return ZoomHelper.getInstance().dispatchTouchEvent(ev!!,this) || super.dispatchTouchEvent(ev) 39 | } 40 | 41 | ``` 42 | 43 | And set View to be zoomable : 44 | ```kotlin 45 | 46 | ZoomHelper.addZoomableView(view) 47 | //ZoomHelper.addZoomableView(view,tag) 48 | 49 | ``` 50 | 51 | Or make a zoomable View by using xml layout : 52 | ```xml 53 | 57 | 58 | 59 | 60 | ``` 61 | *Note: \ is only used in API level 21 and higher* 62 | 63 | And we are Done! :smiley: 64 | 65 | 66 | 67 | ## Usage (Java) 68 | **Installation:** you have to add kotlin as a dependency! (Module gradle) 69 | ```gradle 70 | implementation 'com.aghajari.zoomhelper:ZoomHelper:1.0.1' 71 | implementation 'org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion' //1.4.0-rc 72 | ``` 73 | 74 | and just override dispatchTouchEvent in your Activity and pass all touch events to ZoomHelper! 75 | ```java 76 | 77 | @Override 78 | public boolean dispatchTouchEvent(MotionEvent ev) { 79 | return ZoomHelper.Companion.getInstance().dispatchTouchEvent(ev,this) || super.dispatchTouchEvent(ev); 80 | } 81 | 82 | ``` 83 | 84 | And set View to be zoomable : 85 | ```java 86 | 87 | ZoomHelper.Companion.addZoomableView(view); 88 | //ZoomHelper.Companion.addZoomableView(view,tag); 89 | 90 | ``` 91 | 92 | Or make a zoomable View by using xml layout : 93 | ```xml 94 | 98 | 99 | 100 | 101 | ``` 102 | *Note: \ is only used in API level 21 and higher* 103 | 104 | And we are Done! :smiley: 105 | 106 | 107 | 108 | ## Customization 109 | **Kotlin:** 110 | ```kotlin 111 | ZoomHelper.getInstance().minScale = 1f 112 | ZoomHelper.getInstance().maxScale = Float.MAX_VALUE 113 | ZoomHelper.getInstance().shadowColor = Color.BLACK 114 | ZoomHelper.getInstance().maxShadowAlpha = 0.6f 115 | ZoomHelper.getInstance().shadowAlphaFactory = 4 116 | ZoomHelper.getInstance().dismissDuration = 200 117 | ZoomHelper.getInstance().layoutTheme = android.R.style.Theme_Translucent_NoTitleBar_Fullscreen 118 | ZoomHelper.getInstance().isEnabled = true 119 | ``` 120 | **Java:** 121 | ```Java 122 | ZoomHelper zoomHelper = ZoomHelper.Companion.getInstance(); 123 | zoomHelper.setMinScale(1f); 124 | zoomHelper.setMaxScale(Float.MAX_VALUE); 125 | zoomHelper.setShadowColor(Color.BLACK); 126 | zoomHelper.setMaxShadowAlpha(0.6f); 127 | zoomHelper.setShadowAlphaFactory(4); 128 | zoomHelper.setDismissDuration(200); 129 | zoomHelper.setLayoutTheme(android.R.style.Theme_Translucent_NoTitleBar_Fullscreen); 130 | zoomHelper.setEnabled(true); 131 | ``` 132 | 133 | ![ZoomHelper (Simple)](Previews/ZoomHelper-simple.gif) ![ZoomHelper (Custom)](Previews/ZoomHelper-custom.gif) 134 | 135 | you can disable a zoomable view by using this code : 136 | ```kotlin 137 | ZoomHelper.removeZoomableView(view) 138 | ``` 139 | Or skip a View or a ViewGroup and all its children : 140 | ```kotlin 141 | ZoomHelper.skipLayout(view,true) 142 | ``` 143 | And also you can disable ZoomHelper by using this code : 144 | ```kotlin 145 | ZoomHelper.getInstance().isEnabled = false 146 | ``` 147 | Check the layout is zooming currently : 148 | ```kotlin 149 | if (ZoomHelper.getInstance().isZooming) 150 | ``` 151 | 152 | ### Listeners : 153 | You can add callbacks to listen for specific events. 154 | 155 | ```kotlin 156 | ZoomHelper.getInstance().addOnZoomStateChangedListener(object : ZoomHelper.OnZoomStateChangedListener{ 157 | override fun onZoomStateChanged(zoomHelper: ZoomHelper, zoomableView: View, isZooming: Boolean) { 158 | Toast.makeText(zoomableView.context,if (isZooming) "Zooming started" else "Zooming ended", Toast.LENGTH_SHORT).show() 159 | // you can also get zoomableView Tag 160 | val tag = ZoomHelper.getZoomableViewTag(zoomableView) 161 | } 162 | }) 163 | 164 | ZoomHelper.getInstance().addOnZoomScaleChangedListener(object : ZoomHelper.OnZoomScaleChangedListener{ 165 | override fun onZoomScaleChanged(zoomHelper: ZoomHelper, zoomableView: View, scale: Float, event: MotionEvent?) { 166 | // called when zoom scale changed 167 | } 168 | }) 169 | 170 | ZoomHelper.getInstance().addOnZoomLayoutCreatorListener(object : ZoomHelper.OnZoomLayoutCreatorListener{ 171 | override fun onZoomLayoutCreated(zoomHelper: ZoomHelper, zoomableView: View, zoomLayout: FrameLayout) { 172 | // called when zoomlayout created (zoom started) 173 | } 174 | }) 175 | ``` 176 | Also the default view listeners (onClick,onLongClick,onTouch) will work as well! 177 | 178 | ## Author 179 | Amir Hossein Aghajari 180 | 181 | Inspired by [ImageZoom](https://github.com/okaybroda/ImageZoom) library. 182 | 183 | License 184 | ======= 185 | 186 | Copyright 2020 Amir Hossein Aghajari 187 | Licensed under the Apache License, Version 2.0 (the "License"); 188 | you may not use this file except in compliance with the License. 189 | You may obtain a copy of the License at 190 | 191 | http://www.apache.org/licenses/LICENSE-2.0 192 | 193 | Unless required by applicable law or agreed to in writing, software 194 | distributed under the License is distributed on an "AS IS" BASIS, 195 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 196 | See the License for the specific language governing permissions and 197 | limitations under the License. 198 | 199 | 200 |

201 |
202 | LCoders | AmirHosseinAghajari 203 |
Amir Hossein AghajariEmailGitHub 204 |
205 | -------------------------------------------------------------------------------- /ZoomHelper/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/ZoomHelper/.DS_Store -------------------------------------------------------------------------------- /ZoomHelper/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /ZoomHelper/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 30 7 | 8 | defaultConfig { 9 | minSdkVersion 16 10 | targetSdkVersion 30 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | 25 | } 26 | 27 | dependencies { 28 | implementation fileTree(dir: 'libs', include: ['*.jar']) 29 | 30 | implementation 'androidx.appcompat:appcompat:1.3.1' 31 | testImplementation 'junit:junit:4.13.2' 32 | androidTestImplementation 'androidx.test:runner:1.4.0' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 34 | implementation "androidx.core:core-ktx:1.6.0" 35 | implementation "org.jetbrains.kotlin:kotlin-stdlib:1.5.10" 36 | 37 | } 38 | 39 | // PUBLISH 40 | 41 | apply plugin: 'maven' 42 | apply plugin: 'maven-publish' 43 | apply plugin: 'signing' 44 | 45 | task sourcesJar(type: Jar) { 46 | from android.sourceSets.main.java.srcDirs 47 | classifier = 'sources' 48 | } 49 | 50 | task javadoc(type: Javadoc) { 51 | source = android.sourceSets.main.java.srcDirs 52 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 53 | } 54 | 55 | task javadocJar(type: Jar, dependsOn: javadoc) { 56 | classifier = 'javadoc' 57 | from javadoc.destinationDir 58 | } 59 | 60 | artifacts { 61 | archives javadocJar 62 | archives sourcesJar 63 | } 64 | 65 | signing { 66 | useInMemoryPgpKeys( 67 | rootProject.ext["signing.keyId"], 68 | rootProject.ext["signing.key"], 69 | rootProject.ext["signing.password"], 70 | ) 71 | sign publishing.publications 72 | } 73 | 74 | tasks.withType(Javadoc) { 75 | options.addStringOption('Xdoclint:none', '-quiet') 76 | options.addStringOption('encoding', 'UTF-8') 77 | options.addStringOption('charSet', 'UTF-8') 78 | } 79 | 80 | def artifact = new Properties() 81 | artifact.load(new FileInputStream(rootProject.rootDir.getAbsolutePath() + "/deploy.settings")) 82 | 83 | version = artifact.version 84 | group = artifact.groupId 85 | archivesBaseName = artifact.id 86 | 87 | afterEvaluate { 88 | publishing { 89 | publications { 90 | release(MavenPublication) { 91 | groupId artifact.groupId 92 | artifactId artifact.id 93 | version artifact.version 94 | from components.release 95 | 96 | pom { 97 | name = artifact.id 98 | packaging = 'aar' 99 | description = 'ZoomHelper will make any view to be zoomable just like Instagram pinch-to-zoom' 100 | url = artifact.siteUrl 101 | 102 | scm { 103 | connection = artifact.gitUrl 104 | developerConnection = artifact.gitUrl 105 | url = artifact.siteUrl 106 | } 107 | 108 | licenses { 109 | license { 110 | name = 'The Apache License, Version 2.0' 111 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 112 | } 113 | } 114 | 115 | developers { 116 | developer { 117 | id = 'Aghajari' 118 | name = 'AmirHossein Aghajari' 119 | email = 'amirhossein.aghajari.82@gmail.com' 120 | } 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } -------------------------------------------------------------------------------- /ZoomHelper/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /ZoomHelper/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/ZoomHelper/src/.DS_Store -------------------------------------------------------------------------------- /ZoomHelper/src/androidTest/java/com/aghajari/zoomhelper/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.aghajari.zoomhelper.test", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ZoomHelper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /ZoomHelper/src/main/java/com/aghajari/zoomhelper/InstanceState.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | internal object InstanceState { 4 | private var helper: ZoomHelper? = null 5 | 6 | fun getZoomHelper(): ZoomHelper { 7 | if (helper == null) helper = ZoomHelper() 8 | return helper!! 9 | } 10 | 11 | fun release() { 12 | helper = null 13 | } 14 | } -------------------------------------------------------------------------------- /ZoomHelper/src/main/java/com/aghajari/zoomhelper/PlaceHolderView.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.view.View 6 | 7 | internal class PlaceHolderView(context: Context?) : View(context) { 8 | private var view: View? = null 9 | 10 | constructor(view: View) : this(view.context) { 11 | this.view = view 12 | } 13 | 14 | override fun onDraw(canvas: Canvas?) { 15 | if (view != null && isEnabled) { 16 | view!!.draw(canvas) 17 | return 18 | } 19 | super.onDraw(canvas) 20 | } 21 | } -------------------------------------------------------------------------------- /ZoomHelper/src/main/java/com/aghajari/zoomhelper/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import android.graphics.Rect 4 | import android.view.MotionEvent 5 | import android.view.View 6 | import android.view.ViewGroup 7 | 8 | 9 | internal object Utils { 10 | 11 | fun findZoomableView(ev: MotionEvent, vararg parents: View): View? { 12 | if (parents.isEmpty()) return null; 13 | val p1 = MotionEvent.PointerCoords() 14 | ev.getPointerCoords(0, p1) 15 | 16 | val p2 = MotionEvent.PointerCoords() 17 | ev.getPointerCoords(1, p2) 18 | 19 | for (parent in parents) { 20 | if (!ZoomHelper.isSkippingLayout(parent)) { 21 | 22 | if (isViewInArea(parent, p1.x, p1.y, p2.x, p2.y) 23 | && ZoomHelper.isZoomableView(parent) 24 | ) return parent 25 | 26 | if (parent is ViewGroup) { 27 | val v = checkChild(parent, p1.x, p1.y, p2.x, p2.y) 28 | if (v != null) return v 29 | } 30 | } 31 | } 32 | 33 | return null 34 | } 35 | 36 | private fun checkChild(vg: ViewGroup?, x1: Float, y1: Float, x2: Float, y2: Float): View? { 37 | if (vg == null) return null 38 | 39 | val childCount = vg.childCount 40 | for (i in 0 until childCount) { 41 | val child = vg.getChildAt(i) 42 | if (!ZoomHelper.isSkippingLayout(child)) { 43 | if (isViewInArea(child, x1, y1, x2, y2)) { 44 | if (ZoomHelper.isZoomableView(child)) { 45 | return child; 46 | } else if (child is ViewGroup) { 47 | val v = checkChild(child, x1, y1, x2, y2) 48 | if (v != null) return v 49 | } 50 | } 51 | } 52 | } 53 | return null 54 | } 55 | 56 | private fun isViewInArea(view: View?, x1: Float, y1: Float, x2: Float, y2: Float): Boolean { 57 | if (view == null) return false 58 | val location = IntArray(2) 59 | view.getLocationOnScreen(location) 60 | 61 | val visibleRect = Rect() 62 | visibleRect.left = location[0] 63 | visibleRect.top = location[1] 64 | visibleRect.right = visibleRect.left + view.width 65 | visibleRect.bottom = visibleRect.top + view.height 66 | 67 | return (visibleRect.contains(x1.toInt(), y1.toInt()) 68 | && visibleRect.contains(x2.toInt(), y2.toInt())) 69 | } 70 | 71 | fun getDistance(x1: Double, y1: Double, x2: Double, y2: Double): Int { 72 | return Math.sqrt(Math.pow(x2 - x1, 2.0) + Math.pow(y2 - y1, 2.0)).toInt() 73 | } 74 | } -------------------------------------------------------------------------------- /ZoomHelper/src/main/java/com/aghajari/zoomhelper/ZoomHelper.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import android.animation.Animator 4 | import android.animation.ValueAnimator 5 | import android.app.Activity 6 | import android.app.Dialog 7 | import android.content.Context 8 | import android.graphics.Color 9 | import android.view.MotionEvent 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.widget.FrameLayout 13 | import kotlin.math.abs 14 | import kotlin.math.max 15 | import kotlin.math.min 16 | 17 | /** 18 | * @author AmirHossein Aghajari 19 | * @version 1.1.0 20 | */ 21 | class ZoomHelper { 22 | 23 | /** 24 | * get current zooming view 25 | */ 26 | var zoomableView: View? = null 27 | private set 28 | 29 | /** 30 | * get current zooming view parent in activity 31 | */ 32 | var zoomableViewParent: ViewGroup? = null 33 | private set 34 | 35 | /** 36 | * get zoomLayout's dialog 37 | */ 38 | var dialog: Dialog? = null 39 | private set 40 | 41 | private var shadowView: View? = null 42 | 43 | /** 44 | * @return true if currently is zooming by user 45 | */ 46 | var isZooming: Boolean = false 47 | private set 48 | 49 | private var isAnimating: Boolean = false 50 | 51 | private var onZoomStateChangedListener = arrayListOf() 52 | private var onZoomScaleChangedListener = arrayListOf() 53 | private var onZoomLayoutCreatorListener = arrayListOf() 54 | 55 | private var originalXY: IntArray? = null 56 | private var twoPointerCenter: IntArray? = null 57 | private var viewIndex: Int? = 0 58 | private var originalDistance: Int = 0 59 | private var pivotX: Float = 0f 60 | private var pivotY: Float = 0f 61 | private var viewLayoutParams: ViewGroup.LayoutParams? = null 62 | private var viewFrameLayoutParams: FrameLayout.LayoutParams? = null 63 | private var placeHolderView: PlaceHolderView? = null 64 | 65 | /** 66 | * max zoom scale, or -1 for unlimited [Float.MAX_VALUE] 67 | * default: -1 [Float.MAX_VALUE] 68 | */ 69 | var maxScale = -1f 70 | 71 | /** 72 | * min zoom scale, or -1 for unlimited [Float.MIN_VALUE] 73 | * default: 1f 74 | */ 75 | var minScale = 1f 76 | 77 | var shadowAlphaFactory = 6f 78 | 79 | /** 80 | * max shadow alpha 81 | * default: 0.8f 82 | */ 83 | var maxShadowAlpha = 0.8f 84 | 85 | /** 86 | * shadow color 87 | * default: black 88 | */ 89 | var shadowColor = Color.BLACK 90 | 91 | /** 92 | * zoom layout theme 93 | * default: Translucent_NoTitleBar_Fullscreen 94 | */ 95 | var layoutTheme = R.style.ZoomLayoutStyle 96 | //var layoutTheme = android.R.style.Theme_Translucent_NoTitleBar_Fullscreen 97 | 98 | /** 99 | * dismiss animation duration 100 | * default: android default shortAnimTime 101 | */ 102 | var dismissDuration: Long = -1 103 | 104 | var placeHolderDelay: Long = 80 105 | var placeHolderDismissDelay: Long = 30 106 | private var placeHolderEnabled = true 107 | 108 | var isPlaceHolderEnabled: Boolean 109 | get() = placeHolderEnabled 110 | set(value) { 111 | placeHolderEnabled = value 112 | placeHolderView?.isEnabled = value 113 | } 114 | 115 | private var lastScale = -10f 116 | private var enabled = true 117 | 118 | var isEnabled: Boolean 119 | get() = enabled 120 | set(value) { 121 | enabled = value 122 | if (zoomableView != null && !isAnimating) { 123 | animateDismiss() 124 | } 125 | } 126 | 127 | private fun init(context: Context) { 128 | if (dismissDuration < 0) 129 | dismissDuration = 130 | context.resources.getInteger(android.R.integer.config_shortAnimTime).toLong() 131 | } 132 | 133 | /** 134 | * handle touch event for an specific View or ViewGroup and its children 135 | * call this method in [Activity.dispatchTouchEvent] 136 | */ 137 | fun dispatchTouchEvent(ev: MotionEvent, parent: View): Boolean { 138 | return load(ev, parent) 139 | } 140 | 141 | /** 142 | * handle touch event for a Fragment 143 | * call this method in [Activity.dispatchTouchEvent] 144 | */ 145 | fun dispatchTouchEvent(ev: MotionEvent, fragment: androidx.fragment.app.Fragment): Boolean { 146 | if (fragment.view == null) 147 | return false 148 | return load(ev, fragment.requireView()) 149 | } 150 | 151 | /** 152 | * handle touch event for an Activity 153 | * call this method in [Activity.dispatchTouchEvent] 154 | */ 155 | fun dispatchTouchEvent(ev: MotionEvent, parent: Activity): Boolean { 156 | return load(ev, parent.findViewById(android.R.id.content) as View) 157 | } 158 | 159 | /** 160 | * handle touch event for specific Views or ViewGroups and their children 161 | * call this method in [Activity.dispatchTouchEvent] 162 | */ 163 | fun dispatchTouchEvent(ev: MotionEvent, vararg parents: View): Boolean { 164 | return load(ev, *parents) 165 | } 166 | 167 | private fun load(ev: MotionEvent, vararg parents: View): Boolean { 168 | if (!isEnabled) return isAnimating 169 | if (parents.isEmpty()) return false 170 | val context = parents[0].context 171 | init(context) 172 | 173 | if (ev.pointerCount >= 2) { 174 | if (zoomableView == null) { 175 | val view: View? = Utils.findZoomableView(ev, *parents) ?: return false 176 | zoomableView = view 177 | 178 | reset() 179 | isAnimating = false 180 | isZooming = true 181 | originalXY = IntArray(2) 182 | view?.getLocationOnScreen(originalXY) 183 | 184 | val frameLayout = FrameLayout(context) 185 | shadowView = View(context) 186 | shadowView?.setBackgroundColor(shadowColor) 187 | shadowView?.alpha = 0f 188 | 189 | frameLayout.addView( 190 | shadowView, FrameLayout.LayoutParams( 191 | FrameLayout.LayoutParams.MATCH_PARENT, 192 | FrameLayout.LayoutParams.MATCH_PARENT 193 | ) 194 | ) 195 | 196 | dialog = Dialog(context, layoutTheme) 197 | dialog?.addContentView( 198 | frameLayout, ViewGroup.LayoutParams( 199 | ViewGroup.LayoutParams.MATCH_PARENT, 200 | ViewGroup.LayoutParams.MATCH_PARENT 201 | ) 202 | ) 203 | dialogCreated(frameLayout) 204 | dialog?.show() 205 | 206 | zoomableViewParent = view?.parent as ViewGroup 207 | viewIndex = zoomableViewParent?.indexOfChild(zoomableView) 208 | viewLayoutParams = view.layoutParams 209 | 210 | viewFrameLayoutParams = FrameLayout.LayoutParams(view.width, view.height) 211 | viewFrameLayoutParams?.leftMargin = originalXY!![0] 212 | viewFrameLayoutParams?.topMargin = originalXY!![1] 213 | 214 | placeHolderView = PlaceHolderView(view) 215 | placeHolderView?.isEnabled = placeHolderEnabled 216 | 217 | val index: Int = viewIndex ?: 0 218 | zoomableViewParent?.addView(placeHolderView, index, viewLayoutParams) 219 | zoomableViewParent?.removeView(view) 220 | frameLayout.addView(zoomableView, viewFrameLayoutParams) 221 | 222 | if (placeHolderEnabled) { 223 | view.postDelayed(Runnable { 224 | if (zoomableView == null || zoomableView!!.parent == null || placeHolderView == null) { 225 | dismissDialog() 226 | return@Runnable 227 | } 228 | placeHolderView!!.visibility = View.INVISIBLE 229 | }, placeHolderDelay) 230 | } 231 | 232 | val p1 = MotionEvent.PointerCoords() 233 | ev.getPointerCoords(0, p1) 234 | 235 | val p2 = MotionEvent.PointerCoords() 236 | ev.getPointerCoords(1, p2) 237 | 238 | originalDistance = Utils.getDistance( 239 | p1.x.toDouble(), 240 | p1.y.toDouble(), 241 | p2.x.toDouble(), 242 | p2.y.toDouble() 243 | ) 244 | twoPointerCenter = 245 | intArrayOf(((p2.x + p1.x) / 2).toInt(), ((p2.y + p1.y) / 2).toInt()) 246 | 247 | pivotX = (ev.rawX - originalXY!![0]) 248 | pivotY = (ev.rawY - originalXY!![1]) 249 | 250 | stateChanged() 251 | } else { 252 | val p1 = MotionEvent.PointerCoords() 253 | ev.getPointerCoords(0, p1) 254 | 255 | val p2 = MotionEvent.PointerCoords() 256 | ev.getPointerCoords(1, p2) 257 | 258 | val newCenter = intArrayOf(((p2.x + p1.x) / 2).toInt(), ((p2.y + p1.y) / 2).toInt()) 259 | val currentDistance = Utils.getDistance( 260 | p1.x.toDouble(), 261 | p1.y.toDouble(), 262 | p2.x.toDouble(), 263 | p2.y.toDouble() 264 | ) 265 | val pctIncrease = 266 | (currentDistance.toDouble() - originalDistance.toDouble()) / originalDistance.toDouble() 267 | 268 | zoomableView!!.pivotX = pivotX 269 | zoomableView!!.pivotY = pivotY 270 | 271 | var scale = (1 + pctIncrease).toFloat() 272 | if (minScale != -1f) scale = max(minScale, scale) 273 | if (maxScale != -1f) scale = min(maxScale, scale) 274 | scale = max(Float.MIN_VALUE, scale) 275 | scale = min(Float.MAX_VALUE, scale) 276 | 277 | zoomableView!!.scaleX = scale 278 | zoomableView!!.scaleY = scale 279 | 280 | if (twoPointerCenter != null && originalXY != null) { 281 | updateZoomableViewMargins( 282 | (newCenter[0] - twoPointerCenter!![0] + originalXY!![0]).toFloat(), 283 | (newCenter[1] - twoPointerCenter!![1] + originalXY!![1].toFloat()) 284 | ) 285 | } 286 | 287 | scaleChanged(scale, ev) 288 | if (lastScale != -10f && lastScale == scale) return true 289 | lastScale = scale 290 | shadowView?.alpha = 291 | max(min(maxShadowAlpha, abs(pctIncrease / shadowAlphaFactory).toFloat()), 0f) 292 | } 293 | return true 294 | } else { 295 | if (zoomableView != null) { 296 | if (!isAnimating) { 297 | animateDismiss() 298 | } 299 | return true 300 | } 301 | } 302 | 303 | return false 304 | } 305 | 306 | private fun reset() { 307 | lastScale = -10f 308 | } 309 | 310 | private fun animateDismiss() { 311 | reset() 312 | if (zoomableView == null || originalXY == null) return 313 | isAnimating = true 314 | val scaleYStart = zoomableView!!.scaleY 315 | val scaleXStart = zoomableView!!.scaleX 316 | val leftMarginStart = viewFrameLayoutParams!!.leftMargin 317 | val topMarginStart = viewFrameLayoutParams!!.topMargin 318 | val alphaStart = shadowView!!.alpha 319 | 320 | val scaleYEnd = 1f 321 | val scaleXEnd = 1f 322 | val leftMarginEnd = originalXY!![0] 323 | val topMarginEnd = originalXY!![1] 324 | val alphaEnd = 0f 325 | 326 | val valueAnimator = ValueAnimator.ofFloat(0f, 1f) 327 | valueAnimator.duration = dismissDuration 328 | valueAnimator.addUpdateListener { 329 | val animatedFraction = it.animatedFraction 330 | if (zoomableView != null && zoomableView!!.parent != null) { 331 | updateZoomableView( 332 | animatedFraction, 333 | scaleYStart, 334 | scaleXStart, 335 | leftMarginStart, 336 | topMarginStart, 337 | scaleXEnd, 338 | scaleYEnd, 339 | leftMarginEnd, 340 | topMarginEnd 341 | ) 342 | } 343 | if (shadowView != null) { 344 | shadowView?.alpha = max( 345 | min( 346 | maxShadowAlpha, 347 | ((alphaEnd - alphaStart) * animatedFraction) + alphaStart 348 | ), 0f 349 | ) 350 | } 351 | } 352 | valueAnimator.addListener(object : Animator.AnimatorListener { 353 | override fun onAnimationEnd(animation: Animator) { 354 | end() 355 | } 356 | 357 | override fun onAnimationCancel(animation: Animator) { 358 | end() 359 | } 360 | 361 | private fun end() { 362 | if (zoomableView != null && zoomableView!!.parent != null) { 363 | updateZoomableView( 364 | 1f, 365 | scaleYStart, 366 | scaleXStart, 367 | leftMarginStart, 368 | topMarginStart, 369 | scaleXEnd, 370 | scaleYEnd, 371 | leftMarginEnd, 372 | topMarginEnd 373 | ) 374 | } 375 | dismissDialogAndViews() 376 | valueAnimator.removeAllListeners() 377 | valueAnimator.removeAllUpdateListeners() 378 | } 379 | 380 | override fun onAnimationStart(animation: Animator) {} 381 | override fun onAnimationRepeat(animation: Animator) {} 382 | }) 383 | valueAnimator.start() 384 | 385 | } 386 | 387 | private fun updateZoomableView( 388 | animatedFraction: Float, scaleYStart: Float, scaleXStart: Float, 389 | leftMarginStart: Int, topMarginStart: Int, scaleXEnd: Float, scaleYEnd: Float, 390 | leftMarginEnd: Int, topMarginEnd: Int 391 | ) { 392 | zoomableView?.scaleX = ((scaleXEnd - scaleXStart) * animatedFraction) + scaleXStart 393 | zoomableView?.scaleY = ((scaleYEnd - scaleYStart) * animatedFraction) + scaleYStart 394 | scaleChanged(zoomableView!!.scaleX, null) 395 | 396 | updateZoomableViewMargins( 397 | ((leftMarginEnd - leftMarginStart) * animatedFraction) + leftMarginStart, 398 | ((topMarginEnd - topMarginStart) * animatedFraction) + topMarginStart 399 | ) 400 | } 401 | 402 | private fun updateZoomableViewMargins(left: Float, top: Float) { 403 | if (zoomableView != null && viewFrameLayoutParams != null) { 404 | viewFrameLayoutParams!!.leftMargin = left.toInt() 405 | viewFrameLayoutParams!!.topMargin = top.toInt() 406 | zoomableView?.layoutParams = viewFrameLayoutParams 407 | } 408 | } 409 | 410 | private fun dismissDialogAndViews() { 411 | if (zoomableView != null && zoomableView!!.parent != null) { 412 | zoomableView?.visibility = View.VISIBLE 413 | if (placeHolderEnabled) { 414 | placeHolderView?.visibility = View.VISIBLE 415 | placeHolderView?.postDelayed(Runnable { 416 | if (zoomableView == null || zoomableView!!.parent == null) { 417 | dismissDialog() 418 | return@Runnable 419 | } 420 | val parent = zoomableView!!.parent as ViewGroup 421 | parent.removeView(zoomableView) 422 | if (zoomableViewParent != null) { 423 | zoomableViewParent?.addView(zoomableView!!, viewIndex!!, viewLayoutParams) 424 | zoomableViewParent?.removeView(placeHolderView) 425 | } 426 | dismissDialog() 427 | }, placeHolderDismissDelay) 428 | } else { 429 | val parent = zoomableView!!.parent as ViewGroup 430 | parent.removeView(zoomableView) 431 | if (zoomableViewParent != null) { 432 | zoomableViewParent?.addView(zoomableView!!, viewIndex!!, viewLayoutParams) 433 | zoomableViewParent?.removeView(placeHolderView) 434 | } 435 | dismissDialog() 436 | } 437 | } else { 438 | dismissDialog() 439 | } 440 | 441 | isAnimating = false 442 | isZooming = false 443 | stateChanged() 444 | } 445 | 446 | private fun dismissDialog() { 447 | if (dialog != null) { 448 | dialog?.dismiss() 449 | dialog = null 450 | } 451 | 452 | shadowView = null 453 | placeHolderView = null 454 | 455 | if (zoomableView != null) { 456 | zoomableView?.invalidate() 457 | zoomableView = null 458 | } 459 | 460 | isAnimating = false 461 | isZooming = false 462 | } 463 | 464 | private fun stateChanged() { 465 | if (zoomableView == null) return 466 | for (l in onZoomStateChangedListener) { 467 | l.onZoomStateChanged(this, zoomableView!!, isZooming) 468 | } 469 | } 470 | 471 | private fun scaleChanged(scale: Float, ev: MotionEvent?) { 472 | if (zoomableView == null) return 473 | for (l in onZoomScaleChangedListener) { 474 | l.onZoomScaleChanged(this, zoomableView!!, scale, ev) 475 | } 476 | } 477 | 478 | private fun dialogCreated(layout: FrameLayout) { 479 | if (zoomableView == null) return 480 | for (l in onZoomLayoutCreatorListener) { 481 | l.onZoomLayoutCreated(this, zoomableView!!, layout) 482 | } 483 | } 484 | 485 | fun addOnZoomStateChangedListener(listener: OnZoomStateChangedListener) { 486 | onZoomStateChangedListener.add(listener) 487 | } 488 | 489 | fun removeOnZoomStateChangedListener(listener: OnZoomStateChangedListener) { 490 | onZoomStateChangedListener.remove(listener) 491 | } 492 | 493 | fun addOnZoomScaleChangedListener(listener: OnZoomScaleChangedListener) { 494 | onZoomScaleChangedListener.add(listener) 495 | } 496 | 497 | fun removeOnZoomScaleChangedListener(listener: OnZoomScaleChangedListener) { 498 | onZoomScaleChangedListener.remove(listener) 499 | } 500 | 501 | fun addOnZoomLayoutCreatorListener(listener: OnZoomLayoutCreatorListener) { 502 | onZoomLayoutCreatorListener.add(listener) 503 | } 504 | 505 | fun removeOnZoomLayoutCreatorListener(listener: OnZoomLayoutCreatorListener) { 506 | onZoomLayoutCreatorListener.remove(listener) 507 | } 508 | 509 | 510 | interface OnZoomStateChangedListener { 511 | fun onZoomStateChanged(zoomHelper: ZoomHelper, zoomableView: View, isZooming: Boolean) 512 | } 513 | 514 | interface OnZoomScaleChangedListener { 515 | fun onZoomScaleChanged( 516 | zoomHelper: ZoomHelper, 517 | zoomableView: View, 518 | scale: Float, 519 | event: MotionEvent? 520 | ) 521 | } 522 | 523 | interface OnZoomLayoutCreatorListener { 524 | fun onZoomLayoutCreated( 525 | zoomHelper: ZoomHelper, 526 | zoomableView: View, 527 | zoomLayout: FrameLayout 528 | ) 529 | } 530 | 531 | /** 532 | * dismiss zoom layout without animation 533 | * @see isEnabled 534 | */ 535 | fun dismiss() { 536 | dismissDialog() 537 | } 538 | 539 | /** 540 | * dismiss and release the static ZoomHelper 541 | * All the static ZoomHelper settings will change to the default value! 542 | * Listeners will 543 | */ 544 | fun release() { 545 | dismiss() 546 | InstanceState.release() 547 | } 548 | 549 | companion object { 550 | 551 | /** 552 | * @return true if view is zoomable 553 | */ 554 | fun isZoomableView(view: View) = (view.getTag(R.id.zoomable) != null) 555 | 556 | /** 557 | * Set view to be zoomable 558 | */ 559 | fun addZoomableView(view: View) = view.setTag(R.id.zoomable, Object()) 560 | 561 | /** 562 | * Set view to be zoomable 563 | * @param tag any tag 564 | * @see getZoomableViewTag (View) 565 | */ 566 | fun addZoomableView(view: View, tag: Any?) { 567 | var vt = tag 568 | if (vt == null) vt = Object() 569 | view.setTag(R.id.zoomable, vt) 570 | } 571 | 572 | /** 573 | * @return zoomableView tag or null if view is not zoomable 574 | * @see addZoomableView(View,Object) 575 | */ 576 | fun getZoomableViewTag(view: View): Any? = view.getTag(R.id.zoomable) 577 | 578 | /** 579 | * @see addZoomableView 580 | */ 581 | fun removeZoomableView(view: View) = view.setTag(R.id.zoomable, null) 582 | 583 | fun getInstance() = InstanceState.getZoomHelper() 584 | 585 | /** 586 | * @return true if layout is skipping 587 | * @see skipLayout(View,Boolean) 588 | */ 589 | fun isSkippingLayout(view: View) = (view.getTag(R.id.skip_zoom_layout) != null) 590 | 591 | /** 592 | * skip layout and all layout's zoomable children 593 | */ 594 | fun skipLayout(view: View, skip: Boolean) = 595 | view.setTag(R.id.skip_zoom_layout, if (skip) Object() else null) 596 | } 597 | 598 | } -------------------------------------------------------------------------------- /ZoomHelper/src/main/res/values-v19/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /ZoomHelper/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ZoomHelper/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ZoomHelper 3 | 4 | -------------------------------------------------------------------------------- /ZoomHelper/src/main/res/values/style.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /ZoomHelper/src/test/java/com/aghajari/zoomhelper/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/.DS_Store -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 30 7 | 8 | defaultConfig { 9 | applicationId "com.aghajari.zoomhelper" 10 | minSdkVersion 21 11 | targetSdkVersion 30 12 | versionCode 1 13 | versionName "1.0" 14 | 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation fileTree(dir: 'libs', include: ['*.jar']) 27 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 28 | implementation 'androidx.appcompat:appcompat:1.3.1' 29 | implementation 'androidx.core:core-ktx:1.6.0' 30 | implementation 'androidx.constraintlayout:constraintlayout:2.1.0' 31 | implementation 'androidx.recyclerview:recyclerview:1.2.1' 32 | testImplementation 'junit:junit:4.13.2' 33 | androidTestImplementation 'androidx.test:runner:1.4.0' 34 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 35 | 36 | implementation project(":ZoomHelper:") 37 | implementation 'com.github.bumptech.glide:glide:4.12.0' 38 | } 39 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/.DS_Store -------------------------------------------------------------------------------- /app/src/androidTest/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/androidTest/.DS_Store -------------------------------------------------------------------------------- /app/src/androidTest/java/com/aghajari/zoomhelper/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getTargetContext() 22 | assertEquals("com.aghajari.zoomhelper", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/.DS_Store -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import android.content.Intent 4 | import androidx.appcompat.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.view.View 7 | import com.aghajari.zoomhelper.recycler.RecyclerViewActivity 8 | import com.aghajari.zoomhelper.simpleact.SimpleActivity 9 | import com.aghajari.zoomhelper.simpleact.CustomActivity 10 | 11 | 12 | class MainActivity : AppCompatActivity() { 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_main) 17 | } 18 | 19 | fun onSimpleActivityClick(v: View) { 20 | startActivity(Intent(this, SimpleActivity::class.java)) 21 | } 22 | 23 | fun onCustomActivityClick(v: View) { 24 | startActivity(Intent(this, CustomActivity::class.java)) 25 | } 26 | 27 | fun onRecyclerViewActivityClick(v: View) { 28 | startActivity(Intent(this, RecyclerViewActivity::class.java)) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/ZoomableActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper 2 | 3 | import android.os.Bundle 4 | import android.view.MotionEvent 5 | import androidx.appcompat.app.AppCompatActivity 6 | 7 | open class ZoomableActivity : AppCompatActivity() { 8 | 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | ZoomHelper.getInstance().minScale = 1f 12 | 13 | } 14 | 15 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 16 | return ZoomHelper.getInstance().dispatchTouchEvent(ev!!, this) || super.dispatchTouchEvent(ev) 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/recycler/Adapter.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.recycler 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.Toast 7 | import androidx.appcompat.widget.AppCompatImageView 8 | import androidx.appcompat.widget.AppCompatTextView 9 | import androidx.recyclerview.widget.RecyclerView 10 | import com.aghajari.zoomhelper.R 11 | import com.aghajari.zoomhelper.ZoomHelper 12 | import com.bumptech.glide.Glide 13 | import com.bumptech.glide.request.RequestOptions 14 | 15 | class Adapter(private val data: List) : RecyclerView.Adapter(), 16 | ZoomHelper.OnZoomStateChangedListener { 17 | 18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 19 | val inflatedView = 20 | LayoutInflater.from(parent.context).inflate(R.layout.rv_item, parent, false) 21 | return ViewHolder(inflatedView) 22 | } 23 | 24 | override fun getItemCount(): Int = data.size 25 | 26 | override fun onBindViewHolder(holder: ViewHolder, position: Int) = holder.bind(data[position]) 27 | 28 | class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { 29 | val avatar: AppCompatImageView = itemView.findViewById(R.id.avatar) 30 | val image: AppCompatImageView = itemView.findViewById(R.id.image) 31 | val name: AppCompatTextView = itemView.findViewById(R.id.name) 32 | val text: AppCompatTextView = itemView.findViewById(R.id.text) 33 | 34 | fun bind(model: Model) { 35 | image.layoutParams.height = model.height 36 | name.text = model.name 37 | text.text = model.text 38 | ZoomHelper.addZoomableView(image, model) 39 | 40 | Glide.with(itemView) 41 | .load(model.avatar) 42 | .apply(RequestOptions.circleCropTransform().placeholder(R.drawable.place_holder)) 43 | .into(avatar) 44 | 45 | Glide.with(itemView) 46 | .load(model.image) 47 | .apply(RequestOptions.centerCropTransform().placeholder(R.drawable.place_holder)) 48 | .into(image) 49 | } 50 | 51 | } 52 | 53 | override fun onZoomStateChanged( 54 | zoomHelper: ZoomHelper, 55 | zoomableView: View, 56 | isZooming: Boolean 57 | ) { 58 | if (isZooming) { 59 | val model = ZoomHelper.getZoomableViewTag(zoomableView) as Model 60 | Toast.makeText( 61 | zoomableView.context, 62 | model.name + "'s post started zooming!", 63 | Toast.LENGTH_SHORT 64 | ).show() 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/recycler/FakeData.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.recycler 2 | 3 | import android.content.Context 4 | import com.aghajari.zoomhelper.R 5 | 6 | 7 | object FakeData { 8 | val image = "https://picsum.photos/520/320?id=" 9 | val names = listOf( 10 | "Matt LeBlanc", "Matthew Perry", "David Schwimmer", 11 | "Courteney Cox", "Jennifer Aniston", "Lisa Kudrow" 12 | ) 13 | val text = 14 | "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu." 15 | 16 | fun fakeData(context: Context): List { 17 | val height = context.resources.displayMetrics.widthPixels * 320 / 520; 18 | val list = arrayListOf() 19 | for (j in 0..3) { 20 | for (i in 0 until names.size) { 21 | list.add( 22 | Model( 23 | findAvatar(i), 24 | image + (i + (j * names.size)).toString(), 25 | names[i], 26 | text, 27 | height 28 | ) 29 | ) 30 | } 31 | } 32 | return list 33 | } 34 | 35 | private fun findAvatar(i: Int): Int { 36 | return when (i) { 37 | 0 -> R.drawable.matt_leblanc 38 | 1 -> R.drawable.matthew_perry 39 | 2 -> R.drawable.david_schwimmer 40 | 3 -> R.drawable.courteney_cox 41 | 4 -> R.drawable.jennifer_aniston 42 | 5 -> R.drawable.lisa_kudrow 43 | else -> R.drawable.matt_leblanc 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/recycler/Model.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.recycler 2 | 3 | class Model( 4 | var avatar: Int, 5 | var image: String, 6 | var name: String, 7 | var text: String, 8 | var height: Int 9 | ) { 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/recycler/RecyclerViewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.recycler 2 | 3 | import android.os.Bundle 4 | import androidx.recyclerview.widget.LinearLayoutManager 5 | import androidx.recyclerview.widget.RecyclerView 6 | import com.aghajari.zoomhelper.R 7 | import com.aghajari.zoomhelper.ZoomHelper 8 | import com.aghajari.zoomhelper.ZoomableActivity 9 | 10 | class RecyclerViewActivity : ZoomableActivity() { 11 | 12 | lateinit var adapter: Adapter 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | setContentView(R.layout.activity_recycler) 17 | 18 | val rv = findViewById(R.id.rv) 19 | rv.layoutManager = LinearLayoutManager(this) 20 | adapter = Adapter(FakeData.fakeData(this)) 21 | rv.adapter = adapter 22 | } 23 | 24 | override fun onResume() { 25 | super.onResume() 26 | ZoomHelper.getInstance().addOnZoomStateChangedListener(adapter) 27 | } 28 | 29 | override fun onPause() { 30 | super.onPause() 31 | ZoomHelper.getInstance().removeOnZoomStateChangedListener(adapter) 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/simpleact/CustomActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.simpleact 2 | 3 | import android.graphics.Color 4 | import android.os.Bundle 5 | import android.view.MotionEvent 6 | import android.widget.ImageView 7 | import androidx.appcompat.app.AppCompatActivity 8 | import com.aghajari.zoomhelper.R 9 | import com.aghajari.zoomhelper.ZoomHelper 10 | import com.bumptech.glide.Glide 11 | import com.bumptech.glide.request.RequestOptions 12 | 13 | class CustomActivity : AppCompatActivity() { 14 | 15 | val zoomHelper = ZoomHelper() 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | setContentView(R.layout.activity_custom) 20 | 21 | zoomHelper.shadowColor = Color.RED 22 | zoomHelper.maxScale = 2f 23 | zoomHelper.minScale = 1f 24 | zoomHelper.shadowAlphaFactory = 4f 25 | 26 | val imageView = findViewById(R.id.imageView) 27 | imageView.layoutParams.height = resources.displayMetrics.widthPixels * 350 / 600 28 | 29 | //ZoomHelper.addZoomableView(imageView) 30 | 31 | Glide.with(this) 32 | .load("https://picsum.photos/600/350") 33 | .apply(RequestOptions.centerCropTransform().placeholder(R.drawable.place_holder)) 34 | .into(imageView) 35 | } 36 | 37 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { 38 | return zoomHelper.dispatchTouchEvent(ev!!, this) || super.dispatchTouchEvent(ev) 39 | } 40 | 41 | 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/aghajari/zoomhelper/simpleact/SimpleActivity.kt: -------------------------------------------------------------------------------- 1 | package com.aghajari.zoomhelper.simpleact 2 | 3 | import android.os.Bundle 4 | import android.view.MotionEvent 5 | import android.view.View 6 | import android.widget.ImageView 7 | import android.widget.Toast 8 | import com.aghajari.zoomhelper.R 9 | import com.aghajari.zoomhelper.ZoomHelper 10 | import com.aghajari.zoomhelper.ZoomableActivity 11 | import com.bumptech.glide.Glide 12 | import com.bumptech.glide.request.RequestOptions 13 | 14 | class SimpleActivity : ZoomableActivity(), ZoomHelper.OnZoomStateChangedListener { 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | setContentView(R.layout.activity_simple) 19 | 20 | val imageView = findViewById(R.id.imageView) 21 | imageView.layoutParams.height = resources.displayMetrics.widthPixels * 350 / 600 22 | 23 | //ZoomHelper.addZoomableView(imageView) //layout will make this 24 | ZoomHelper.addZoomableView(findViewById(R.id.textView)) 25 | 26 | Glide.with(this) 27 | .load("https://picsum.photos/600/350") 28 | .apply(RequestOptions.centerCropTransform().placeholder(R.drawable.place_holder)) 29 | .into(imageView) 30 | } 31 | 32 | override fun onZoomStateChanged( 33 | zoomHelper: ZoomHelper, 34 | zoomableView: View, 35 | isZooming: Boolean 36 | ) { 37 | Toast.makeText( 38 | this, 39 | if (isZooming) "Zooming started" else "Zooming ended", 40 | Toast.LENGTH_SHORT 41 | ).show() 42 | } 43 | 44 | override fun onResume() { 45 | super.onResume() 46 | ZoomHelper.getInstance().addOnZoomStateChangedListener(this) 47 | } 48 | 49 | override fun onPause() { 50 | super.onPause() 51 | ZoomHelper.getInstance().removeOnZoomStateChangedListener(this) 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/courteney_cox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/courteney_cox.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/david_schwimmer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/david_schwimmer.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/jennifer_aniston.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/jennifer_aniston.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/lisa_kudrow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/lisa_kudrow.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/matt_leblanc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/matt_leblanc.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/matthew_perry.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Aghajari/ZoomHelper/5e0403c692bf487aac5efb4fc60f700151784e0d/app/src/main/res/drawable/matthew_perry.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/place_holder.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_custom.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 |