├── .github └── workflows │ └── build.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── library ├── build.gradle.kts ├── proguard-rules.txt └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── davemorrissey │ │ └── labs │ │ └── subscaleview │ │ ├── ImageRotation.kt │ │ ├── ImageSource.kt │ │ ├── ImageViewState.kt │ │ ├── SubsamplingScaleImageView.java │ │ ├── decoder │ │ ├── Decoder.kt │ │ └── ImageRegionDecoder.kt │ │ └── provider │ │ ├── AssetInputProvider.kt │ │ ├── InputProvider.kt │ │ ├── OpenStreamProvider.kt │ │ ├── ResourceInputProvider.kt │ │ └── UriInputProvider.kt │ └── res │ └── values │ └── attrs.xml ├── sample ├── assets │ ├── card.png │ ├── sanmartino.jpg │ └── swissroad.jpg ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ ├── kotlin │ └── com │ │ └── davemorrissey │ │ └── labs │ │ └── subscaleview │ │ └── test │ │ ├── AbstractFragmentsActivity.kt │ │ ├── AbstractPagesActivity.kt │ │ ├── MainActivity.kt │ │ ├── Page.kt │ │ ├── animation │ │ └── AnimationActivity.kt │ │ ├── basicfeatures │ │ └── BasicFeaturesActivity.kt │ │ ├── configuration │ │ └── ConfigurationActivity.kt │ │ ├── eventhandling │ │ └── EventHandlingActivity.kt │ │ ├── eventhandlingadvanced │ │ └── AdvancedEventHandlingActivity.kt │ │ ├── extension │ │ ├── ExtensionActivity.kt │ │ ├── ExtensionCircleFragment.kt │ │ ├── ExtensionFreehandFragment.kt │ │ ├── ExtensionPinFragment.kt │ │ └── views │ │ │ ├── CircleView.kt │ │ │ ├── FreehandView.kt │ │ │ └── PinView.kt │ │ ├── imagedisplay │ │ ├── ImageDisplayActivity.kt │ │ ├── ImageDisplayLargeFragment.kt │ │ ├── ImageDisplayRegionFragment.kt │ │ └── ImageDisplayRotateFragment.kt │ │ └── viewpager │ │ ├── VerticalViewPager.kt │ │ ├── ViewPagerActivity.kt │ │ └── ViewPagerFragment.kt │ └── res │ ├── drawable-nodpi │ ├── button_standout_inactive.xml │ ├── button_standout_pressed.xml │ ├── button_transparent_pressed.xml │ ├── buttonstate_standout.xml │ ├── buttonstate_transparent.xml │ ├── pushpin_blue.png │ └── transparent.xml │ ├── drawable-xhdpi │ ├── next.png │ ├── play.png │ ├── previous.png │ ├── reset.png │ └── rotate.png │ ├── layout │ ├── animation_activity.xml │ ├── extension_circle_fragment.xml │ ├── extension_freehand_fragment.xml │ ├── extension_pin_fragment.xml │ ├── fragments_activity.xml │ ├── imagedisplay_large_fragment.xml │ ├── imagedisplay_region_fragment.xml │ ├── imagedisplay_rotate_fragment.xml │ ├── main_activity.xml │ ├── pages_activity.xml │ ├── view_pager.xml │ └── view_pager_page.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 │ └── style.xml └── settings.gradle.kts /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - "**.md" 9 | pull_request: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone repo 20 | uses: actions/checkout@v4 21 | 22 | - name: Validate Gradle Wrapper 23 | uses: gradle/wrapper-validation-action@v2 24 | 25 | - name: Set up JDK 26 | uses: actions/setup-java@v4 27 | with: 28 | java-version: 17 29 | distribution: adopt 30 | 31 | - name: Setup Gradle 32 | uses: gradle/actions/setup-gradle@v3 33 | 34 | - name: Build app 35 | run: ./gradlew assemble 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Android Studio generated folders 9 | .navigation/ 10 | captures/ 11 | .externalNativeBuild 12 | 13 | # IntelliJ project files 14 | *.iml 15 | .idea/ 16 | 17 | # Misc 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Before raising a new issue, please check the following places for an answer to your question! 2 | 3 | * Read through [the wiki](https://github.com/davemorrissey/subsampling-scale-image-view/wiki) for a comprehensive guide to using the view. 4 | * Search through [open and closed issues](https://github.com/davemorrissey/subsampling-scale-image-view/issues?utf8=%E2%9C%93&q=is%3Aissue) 5 | * Check examples in [the sample project](https://github.com/davemorrissey/subsampling-scale-image-view/tree/master/sample/src/com/davemorrissey/labs/subscaleview/sample) - most common uses are covered. 6 | * See if there's an answer to your question on [StackOverflow](http://stackoverflow.com/). 7 | 8 | If you get stuck adding the view in your project or need help extending it for your requirements, please consider asking for help on StackOverflow instead of raising an issue. This issue tracker is intended for reporting bugs and raising feature requests. 9 | 10 | Thanks for reading! 11 | -------------------------------------------------------------------------------- /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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Subsampling Scale Image View 2 | =========================== 3 | 4 | A custom image view for Android, designed for photo galleries and displaying huge images (e.g. maps and building plans) without `OutOfMemoryError`s. Includes pinch to zoom, panning, rotation and animation support, and allows easy extension so you can add your own overlays and touch event detection. 5 | 6 | The view optionally uses subsampling and tiles to support very large images - a low resolution base layer is loaded and as you zoom in, it is overlaid with smaller high resolution tiles for the visible area. This avoids holding too much data in memory. It's ideal for displaying large images while allowing you to zoom in to the high resolution details. You can disable tiling for smaller images and when displaying a bitmap object. There are some advantages and disadvantages to disabling tiling so to decide which is best, see [the wiki](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/02.-Displaying-images). 7 | 8 | #### Guides 9 | 10 | * [Releases & downloads](https://github.com/davemorrissey/subsampling-scale-image-view/releases) 11 | * [Installation and setup](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/01.-Setup) 12 | * [Image display notes & limitations](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/02.-Displaying-images) 13 | * [Using preview images](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/03.-Preview-images) 14 | * [Handling orientation changes](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/05.-Orientation-changes) 15 | * [Advanced configuration](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/07.-Configuration) 16 | * [Event handling](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/09.-Events) 17 | * [Animation](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/08.-Animation) 18 | * [Extension](https://github.com/davemorrissey/subsampling-scale-image-view/wiki/10.-Extension) 19 | 20 | ## Features 21 | 22 | #### Image display 23 | 24 | * Display images from assets, resources, the file system or bitmaps 25 | * Automatically rotate images from the file system (e.g. the camera or gallery) according to EXIF 26 | * Manually rotate images in 90° increments 27 | * Display a region of the source image 28 | * Use a preview image while large images load 29 | * Swap images at runtime 30 | * Use a custom bitmap decoder 31 | 32 | *With tiling enabled:* 33 | 34 | * Display huge images, larger than can be loaded into memory 35 | * Show high resolution detail on zooming in 36 | * Tested up to 20,000x20,000px, though larger images are slower 37 | 38 | #### Gesture detection 39 | 40 | * One finger pan 41 | * Two finger pinch to zoom 42 | * Quick scale (one finger zoom) 43 | * Pan while zooming 44 | * Seamless switch between pan and zoom 45 | * Fling momentum after panning 46 | * Double tap to zoom in and out 47 | * Options to disable pan and/or zoom gestures 48 | 49 | #### Animation 50 | 51 | * Public methods for animating the scale and center 52 | * Customisable duration and easing 53 | * Optional uninterruptible animations 54 | 55 | #### Overridable event detection 56 | * Supports `OnClickListener` and `OnLongClickListener` 57 | * Supports interception of events using `GestureDetector` and `OnTouchListener` 58 | * Extend to add your own gestures 59 | 60 | #### Easy integration 61 | * Use within a `ViewPager` to create a photo gallery 62 | * Easily restore scale, center and orientation after screen rotation 63 | * Can be extended to add overlay graphics that move and scale with the image 64 | * Handles view resizing and `wrap_content` layout 65 | 66 | ## Quick start 67 | 68 | **1)** Add this library as a dependency in your app's build.gradle file. 69 | 70 | ```gradle 71 | repositories { 72 | // ... Your other repos... 73 | 74 | maven(url = "https://www.jitpack.io") 75 | } 76 | 77 | dependencies { 78 | implementation("com.github.tachiyomiorg:subsampling-scale-image-view:") 79 | } 80 | ``` 81 | 82 | **2)** Add the view to your layout XML. 83 | 84 | ```xml 85 | 88 | 89 | 93 | 94 | 95 | ``` 96 | 97 | **3a)** Now, in your fragment or activity, set the image resource, asset name or file path. 98 | 99 | ```java 100 | SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView); 101 | imageView.setImage(ImageSource.resource(R.drawable.monkey)); 102 | // ... or ... 103 | imageView.setImage(ImageSource.asset("map.png")) 104 | // ... or ... 105 | imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG")); 106 | ``` 107 | 108 | **3b)** Or, if you have a `Bitmap` object in memory, load it into the view. This is unsuitable for large images because it bypasses subsampling - you may get an `OutOfMemoryError`. 109 | 110 | ```java 111 | SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView); 112 | imageView.setImage(ImageSource.bitmap(bitmap)); 113 | ``` 114 | 115 | ## Photo credits 116 | 117 | * San Martino by Luca Bravo, via [unsplash.com](https://unsplash.com/photos/lWAOc0UuJ-A) 118 | * Swiss Road by Ludovic Fremondiere, via [unsplash.com](https://unsplash.com/photos/3XN-BNRDUyY) 119 | 120 | ## About 121 | 122 | Copyright 2018 David Morrissey, and licensed under the Apache License, Version 2.0. No attribution is necessary but it's very much appreciated. Star this project if you like it! 123 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) apply false 3 | alias(libs.plugins.kotlin.android) apply false 4 | } 5 | 6 | tasks.register("clean") { 7 | delete(rootProject.layout.buildDirectory) 8 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | org.gradle.jvmargs=-Xmx1536m 13 | 14 | org.gradle.parallel=true 15 | org.gradle.caching=true 16 | 17 | # AndroidX support 18 | android.useAndroidX=true 19 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [plugins] 2 | android-library = { id = "com.android.library", version = "8.3.0" } 3 | kotlin-android = { id = "org.jetbrains.kotlin.android", version = "1.9.23" } 4 | 5 | [libraries] 6 | androidx-annotation = { module = "androidx.annotation:annotation", version = "1.7.1" } 7 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version = "1.7.0-alpha03" } 8 | androidx-viewpager = { module = "androidx.viewpager:viewpager", version = "1.1.0-alpha01" } 9 | 10 | image-decoder = { module = "com.github.tachiyomiorg:image-decoder", version = "e08e9be535" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - sdk install java 17.0.9-open 5 | - sdk use java 17.0.9-open 6 | install: 7 | - ./gradlew clean :library:assembleRelease :library:publishToMavenLocal -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | kotlin("android") 4 | `maven-publish` 5 | } 6 | 7 | android { 8 | namespace = "com.davemorrissey.labs.subscaleview" 9 | compileSdk = 34 10 | 11 | defaultConfig { 12 | minSdk = 21 13 | 14 | consumerProguardFiles("proguard-rules.txt") 15 | } 16 | 17 | compileOptions { 18 | sourceCompatibility = JavaVersion.VERSION_17 19 | targetCompatibility = JavaVersion.VERSION_17 20 | } 21 | 22 | kotlinOptions { 23 | compileOptions { 24 | sourceCompatibility = JavaVersion.VERSION_17 25 | targetCompatibility = JavaVersion.VERSION_17 26 | } 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation(libs.androidx.annotation) 32 | api(libs.image.decoder) 33 | } 34 | 35 | afterEvaluate { 36 | publishing { 37 | publications { 38 | create("release") { 39 | groupId = "com.github.tachiyomiorg" 40 | artifactId = "subsampling-scale-image-view" 41 | version = "4.0.0" 42 | 43 | from(components["release"]) 44 | } 45 | } 46 | 47 | repositories { 48 | maven { 49 | name = "jitpack" 50 | url = uri("https://jitpack.io") 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /library/proguard-rules.txt: -------------------------------------------------------------------------------- 1 | -keep class com.davemorrissey.labs.subscaleview.** { *; } 2 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/ImageRotation.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview 2 | 3 | enum class ImageRotation(val rotation: Int) { 4 | ROTATION_0(0), ROTATION_90(90), ROTATION_180(180), ROTATION_270(270); 5 | 6 | fun rotateBy90Degrees(): ImageRotation = when (this) { 7 | ROTATION_0 -> ROTATION_90 8 | ROTATION_90 -> ROTATION_180 9 | ROTATION_180 -> ROTATION_270 10 | ROTATION_270 -> ROTATION_0 11 | } 12 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/ImageSource.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Rect 6 | import android.net.Uri 7 | import com.davemorrissey.labs.subscaleview.provider.AssetInputProvider 8 | import com.davemorrissey.labs.subscaleview.provider.InputProvider 9 | import com.davemorrissey.labs.subscaleview.provider.OpenStreamProvider 10 | import com.davemorrissey.labs.subscaleview.provider.ResourceInputProvider 11 | import com.davemorrissey.labs.subscaleview.provider.UriInputProvider 12 | import java.io.InputStream 13 | 14 | /** 15 | * Helper class used to set the source and additional attributes from a variety of sources. Supports 16 | * use of a bitmap, asset, resource, external file or any other URI. 17 | * 18 | * 19 | * When you are using a preview image, you must set the dimensions of the full size image on the 20 | * ImageSource object for the full size image using the [.dimensions] method. 21 | */ 22 | @Suppress("unused") 23 | class ImageSource { 24 | 25 | val bitmap: Bitmap? 26 | val provider: InputProvider? 27 | 28 | var sWidth = 0 29 | private set 30 | var sHeight = 0 31 | private set 32 | var sRegion: Rect? = null 33 | private set 34 | var isCached = false 35 | private set 36 | 37 | private constructor(bitmap: Bitmap, cached: Boolean) { 38 | this.bitmap = bitmap 39 | provider = null 40 | sWidth = bitmap.width 41 | sHeight = bitmap.height 42 | isCached = cached 43 | } 44 | 45 | private constructor(provider: InputProvider) { 46 | bitmap = null 47 | this.provider = provider 48 | } 49 | 50 | /** 51 | * Use a region of the source image. Region must be set independently for the full size image and the preview if 52 | * you are using one. 53 | * 54 | * @param sRegion the region of the source image to be displayed. 55 | * @return this instance for chaining. 56 | */ 57 | fun region(sRegion: Rect?): ImageSource { 58 | this.sRegion = sRegion 59 | setInvariants() 60 | return this 61 | } 62 | 63 | /** 64 | * Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI 65 | * and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare 66 | * the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset. 67 | * 68 | * @param sWidth width of the source image. 69 | * @param sHeight height of the source image. 70 | * @return this instance for chaining. 71 | */ 72 | fun dimensions(sWidth: Int, sHeight: Int): ImageSource { 73 | if (bitmap == null) { 74 | this.sWidth = sWidth 75 | this.sHeight = sHeight 76 | } 77 | setInvariants() 78 | return this 79 | } 80 | 81 | private fun setInvariants() { 82 | if (sRegion != null) { 83 | sWidth = sRegion!!.width() 84 | sHeight = sRegion!!.height() 85 | } 86 | } 87 | 88 | companion object { 89 | /** 90 | * Create an instance from a resource. The correct resource for the device screen resolution will be used. 91 | * 92 | * @param resId resource ID. 93 | * @return an [ImageSource] instance. 94 | */ 95 | @JvmStatic 96 | fun resource(context: Context, resId: Int): ImageSource { 97 | return ImageSource(ResourceInputProvider(context, resId)) 98 | } 99 | 100 | /** 101 | * Create an instance from an asset name. 102 | * 103 | * @param assetName asset name. 104 | * @return an [ImageSource] instance. 105 | */ 106 | @JvmStatic 107 | fun asset(context: Context, assetName: String): ImageSource { 108 | return ImageSource(AssetInputProvider(context, assetName)) 109 | } 110 | 111 | /** 112 | * Create an instance from a URI. 113 | * 114 | * @param uri image URI. 115 | * @return an [ImageSource] instance. 116 | */ 117 | @JvmStatic 118 | fun uri(context: Context, uri: Uri): ImageSource { 119 | return ImageSource(UriInputProvider(context, uri)) 120 | } 121 | 122 | /** 123 | * Create an instance from an input provider. 124 | * 125 | * @param provider input stream provider. 126 | * @return an [ImageSource] instance. 127 | */ 128 | @JvmStatic 129 | fun provider(provider: InputProvider): ImageSource { 130 | return ImageSource(provider) 131 | } 132 | 133 | /** 134 | * Create an instance from an input stream. 135 | * 136 | * @param stream open input stream. 137 | * @return an [ImageSource] instance. 138 | */ 139 | @JvmStatic 140 | fun inputStream(stream: InputStream): ImageSource { 141 | return ImageSource(OpenStreamProvider(stream)) 142 | } 143 | 144 | /** 145 | * Provide a loaded bitmap for display. 146 | * 147 | * @param bitmap bitmap to be displayed. 148 | * @return an [ImageSource] instance. 149 | */ 150 | @JvmStatic 151 | fun bitmap(bitmap: Bitmap): ImageSource { 152 | return ImageSource(bitmap, false) 153 | } 154 | 155 | /** 156 | * Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no 157 | * longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso 158 | * or Volley. 159 | * 160 | * @param bitmap bitmap to be displayed. 161 | * @return an [ImageSource] instance. 162 | */ 163 | @JvmStatic 164 | fun cachedBitmap(bitmap: Bitmap): ImageSource { 165 | return ImageSource(bitmap, true) 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/ImageViewState.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview 2 | 3 | import android.graphics.PointF 4 | import java.io.Serializable 5 | 6 | /** 7 | * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate. 8 | */ 9 | class ImageViewState( 10 | val scale: Float, 11 | center: PointF, 12 | ) : Serializable { 13 | 14 | private val centerX: Float = center.x 15 | private val centerY: Float = center.y 16 | 17 | val center: PointF 18 | get() = PointF(centerX, centerY) 19 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/decoder/Decoder.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.decoder 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Point 6 | import android.graphics.Rect 7 | import android.os.Build 8 | import android.util.Log 9 | import com.davemorrissey.labs.subscaleview.provider.InputProvider 10 | import tachiyomi.decoder.ImageDecoder 11 | import tachiyomi.decoder.ImageDecoder.Companion.newInstance 12 | 13 | class Decoder( 14 | private val cropBorders: Boolean, 15 | private val hardwareConfig: Boolean, 16 | private val displayProfile: ByteArray, 17 | ) : ImageRegionDecoder { 18 | 19 | private var decoder: ImageDecoder? = null 20 | 21 | /** 22 | * Initialise the decoder. When possible, perform initial setup work once in this method. The 23 | * dimensions of the image must be returned. 24 | * 25 | * @param context Application context. A reference may be held, but must be cleared on recycle. 26 | * @param provider Provider of the image. 27 | * @return Dimensions of the image. 28 | * @throws Exception if initialisation fails. 29 | */ 30 | override fun init(context: Context, provider: InputProvider): Point { 31 | try { 32 | provider.openStream().use { inputStream -> 33 | decoder = newInstance(inputStream!!, cropBorders, displayProfile) 34 | } 35 | } catch (e: Exception) { 36 | Log.e(TAG, "Failed to init decoder", e) 37 | } 38 | if (decoder == null) { 39 | error("Image decoder failed to initialize and get image size") 40 | } 41 | return Point(decoder!!.width, decoder!!.height) 42 | } 43 | 44 | /** 45 | * Decode a region of the image with the given sample size. This method is called off the UI 46 | * thread so it can safely load the image on the current thread. It is called from 47 | * [android.os.AsyncTask]s running in an executor that may have multiple threads, so 48 | * implementations must be thread safe. Adding `synchronized` to the method signature 49 | * is the simplest way to achieve this, but bear in mind the [.recycle] method can be 50 | * called concurrently. 51 | * 52 | * @param sRect Source image rectangle to decode. 53 | * @param sampleSize Sample size. 54 | * @return The decoded region. It is safe to return null if decoding fails. 55 | */ 56 | override fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap { 57 | var bitmap = decoder?.decode(sRect, sampleSize) 58 | check(bitmap != null) { "Failed to decode region" } 59 | if (hardwareConfig && Build.VERSION.SDK_INT >= 26) { 60 | val hwBitmap = bitmap.copy(Bitmap.Config.HARDWARE, false) 61 | if (hwBitmap != null) { 62 | bitmap.recycle() 63 | bitmap = hwBitmap 64 | } 65 | } 66 | return bitmap 67 | } 68 | 69 | /** 70 | * Status check. Should return false before initialisation and after recycle. 71 | * 72 | * @return true if the decoder is ready to be used. 73 | */ 74 | override fun isReady(): Boolean { 75 | return decoder != null && !decoder!!.isRecycled 76 | } 77 | 78 | /** 79 | * This method will be called when the decoder is no longer required. It should clean up any 80 | * resources still in use. 81 | */ 82 | override fun recycle() { 83 | decoder?.recycle() 84 | } 85 | } 86 | 87 | private val TAG = Decoder::class.java.simpleName 88 | -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/decoder/ImageRegionDecoder.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.decoder 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.Point 6 | import android.graphics.Rect 7 | import androidx.annotation.WorkerThread 8 | import com.davemorrissey.labs.subscaleview.provider.InputProvider 9 | 10 | /** 11 | * Interface for image decoding classes. 12 | */ 13 | interface ImageRegionDecoder { 14 | 15 | /** 16 | * Initialise the decoder. When possible, perform initial setup work once in this method. The 17 | * dimensions of the image must be returned. 18 | * 19 | * @param context Application context. A reference may be held, but must be cleared on recycle. 20 | * @param provider Provider of the image. 21 | * @return Dimensions of the image. 22 | * @throws Exception if initialisation fails. 23 | */ 24 | fun init(context: Context, provider: InputProvider): Point 25 | 26 | /** 27 | * Decode a region of the image with the given sample size. This method is called off the UI 28 | * thread so it can safely load the image on the current thread. It is called from 29 | * [android.os.AsyncTask]s running in an executor that may have multiple threads, so 30 | * implementations must be thread safe. Adding `synchronized` to the method signature 31 | * is the simplest way to achieve this, but bear in mind the [.recycle] method can be 32 | * called concurrently. 33 | * 34 | * @param sRect Source image rectangle to decode. 35 | * @param sampleSize Sample size. 36 | * @return The decoded region. It is safe to return null if decoding fails. 37 | */ 38 | @WorkerThread 39 | fun decodeRegion(sRect: Rect, sampleSize: Int): Bitmap 40 | 41 | /** 42 | * Status check. Should return false before initialisation and after recycle. 43 | * 44 | * @return true if the decoder is ready to be used. 45 | */ 46 | fun isReady(): Boolean 47 | 48 | /** 49 | * This method will be called when the decoder is no longer required. It should clean up any 50 | * resources still in use. 51 | */ 52 | fun recycle() 53 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/provider/AssetInputProvider.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.provider 2 | 3 | import android.content.Context 4 | import android.content.res.AssetManager 5 | import java.io.IOException 6 | import java.io.InputStream 7 | 8 | class AssetInputProvider( 9 | context: Context, 10 | private val assetName: String, 11 | ) : InputProvider { 12 | 13 | private val assets: AssetManager = context.assets 14 | 15 | @Throws(IOException::class) 16 | override fun openStream(): InputStream { 17 | return assets.open(assetName) 18 | } 19 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/provider/InputProvider.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.provider 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | 6 | fun interface InputProvider { 7 | 8 | @Throws(IOException::class) 9 | fun openStream(): InputStream? 10 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/provider/OpenStreamProvider.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.provider 2 | 3 | import java.io.IOException 4 | import java.io.InputStream 5 | 6 | /** 7 | * Provider from an already open input stream. 8 | * 9 | * Please keep in mind this provider is not reusable. 10 | */ 11 | class OpenStreamProvider(private val stream: InputStream) : InputProvider { 12 | @Throws(IOException::class) 13 | override fun openStream(): InputStream { 14 | return stream 15 | } 16 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/provider/ResourceInputProvider.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.provider 2 | 3 | import android.content.Context 4 | import android.content.res.Resources 5 | import java.io.InputStream 6 | 7 | class ResourceInputProvider( 8 | context: Context, 9 | private val resource: Int, 10 | ) : InputProvider { 11 | 12 | private val resources: Resources = context.resources 13 | 14 | override fun openStream(): InputStream { 15 | return resources.openRawResource(resource) 16 | } 17 | } -------------------------------------------------------------------------------- /library/src/main/java/com/davemorrissey/labs/subscaleview/provider/UriInputProvider.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.provider 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import android.net.Uri 6 | import java.io.IOException 7 | import java.io.InputStream 8 | 9 | class UriInputProvider( 10 | context: Context, 11 | private val uri: Uri, 12 | ) : InputProvider { 13 | 14 | private val resolver: ContentResolver = context.contentResolver 15 | 16 | @Throws(IOException::class) 17 | override fun openStream(): InputStream? { 18 | return resolver.openInputStream(uri) 19 | } 20 | } -------------------------------------------------------------------------------- /library/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /sample/assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/assets/card.png -------------------------------------------------------------------------------- /sample/assets/sanmartino.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/assets/sanmartino.jpg -------------------------------------------------------------------------------- /sample/assets/swissroad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/assets/swissroad.jpg -------------------------------------------------------------------------------- /sample/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | kotlin("android") 4 | } 5 | 6 | android { 7 | namespace = "com.davemorrissey.labs.subscaleview.test" 8 | compileSdk = 34 9 | 10 | defaultConfig { 11 | applicationId = "com.davemorrissey.labs.subscaleview.test" 12 | minSdk = 21 13 | targetSdk = 34 14 | 15 | versionCode = 4 16 | versionName = "3.1.0" 17 | 18 | defaultConfig { 19 | packagingOptions { 20 | jniLibs.keepDebugSymbols.addAll(listOf("*/mips/*.so", "*/mips64/*.so")) 21 | } 22 | } 23 | 24 | compileOptions { 25 | viewBinding.isEnabled = true 26 | } 27 | } 28 | 29 | sourceSets { 30 | getByName("main").assets.srcDirs("assets") 31 | } 32 | 33 | compileOptions { 34 | sourceCompatibility = JavaVersion.VERSION_17 35 | targetCompatibility = JavaVersion.VERSION_17 36 | } 37 | 38 | kotlinOptions { 39 | compileOptions { 40 | sourceCompatibility = JavaVersion.VERSION_17 41 | targetCompatibility = JavaVersion.VERSION_17 42 | } 43 | } 44 | } 45 | 46 | dependencies { 47 | implementation(project(":library")) 48 | 49 | implementation(libs.androidx.appcompat) 50 | implementation(libs.androidx.viewpager) 51 | } 52 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 27 | 30 | 33 | 36 | 39 | 42 | 45 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/AbstractFragmentsActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import androidx.fragment.app.FragmentActivity 6 | 7 | abstract class AbstractFragmentsActivity protected constructor( 8 | private val title: Int, 9 | private val layout: Int, 10 | private val notes: List 11 | ) : FragmentActivity() { 12 | 13 | private var page = 0 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContentView(layout) 18 | 19 | actionBar?.title = getString(title) 20 | actionBar?.setDisplayHomeAsUpEnabled(true) 21 | 22 | if (savedInstanceState != null && savedInstanceState.containsKey(BUNDLE_PAGE)) { 23 | page = savedInstanceState.getInt(BUNDLE_PAGE) 24 | } 25 | } 26 | 27 | override fun onResume() { 28 | super.onResume() 29 | updateNotes() 30 | } 31 | 32 | override fun onSaveInstanceState(outState: Bundle) { 33 | super.onSaveInstanceState(outState) 34 | outState.putInt(BUNDLE_PAGE, page) 35 | } 36 | 37 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 38 | finish() 39 | return true 40 | } 41 | 42 | operator fun next() { 43 | page++ 44 | updateNotes() 45 | } 46 | 47 | fun previous() { 48 | page-- 49 | updateNotes() 50 | } 51 | 52 | protected abstract fun onPageChanged(page: Int) 53 | 54 | private fun updateNotes() { 55 | if (page > notes.size - 1) { 56 | return 57 | } 58 | 59 | actionBar?.setSubtitle(notes[page].subtitle) 60 | onPageChanged(page) 61 | } 62 | } 63 | 64 | private const val BUNDLE_PAGE = "page" 65 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/AbstractPagesActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test 2 | 3 | import android.os.Bundle 4 | import android.view.MenuItem 5 | import android.view.View 6 | import android.widget.TextView 7 | import androidx.fragment.app.FragmentActivity 8 | import com.davemorrissey.labs.subscaleview.test.R.id 9 | 10 | abstract class AbstractPagesActivity protected constructor( 11 | private val title: Int, 12 | private val layout: Int, 13 | private val notes: List 14 | ) : FragmentActivity() { 15 | 16 | protected var page = 0 17 | private set 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(layout) 22 | 23 | actionBar?.title = getString(title) 24 | actionBar?.setDisplayHomeAsUpEnabled(true) 25 | 26 | findViewById(id.next).setOnClickListener { next() } 27 | findViewById(id.previous).setOnClickListener { previous() } 28 | 29 | if (savedInstanceState != null && savedInstanceState.containsKey(BUNDLE_PAGE)) { 30 | page = savedInstanceState.getInt(BUNDLE_PAGE) 31 | } 32 | } 33 | 34 | override fun onResume() { 35 | super.onResume() 36 | updateNotes() 37 | } 38 | 39 | override fun onSaveInstanceState(outState: Bundle) { 40 | super.onSaveInstanceState(outState) 41 | outState.putInt(BUNDLE_PAGE, page) 42 | } 43 | 44 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 45 | finish() 46 | return true 47 | } 48 | 49 | private operator fun next() { 50 | page++ 51 | updateNotes() 52 | } 53 | 54 | private fun previous() { 55 | page-- 56 | updateNotes() 57 | } 58 | 59 | private fun updateNotes() { 60 | if (page > notes.size - 1) { 61 | return 62 | } 63 | val actionBar = actionBar 64 | actionBar?.setSubtitle(notes[page].subtitle) 65 | findViewById(id.note).setText(notes[page].text) 66 | findViewById(id.next).visibility = 67 | if (page >= notes.size - 1) View.INVISIBLE else View.VISIBLE 68 | findViewById(id.previous).visibility = 69 | if (page <= 0) View.INVISIBLE else View.VISIBLE 70 | onPageChanged(page) 71 | } 72 | 73 | protected open fun onPageChanged(page: Int) {} 74 | } 75 | 76 | private const val BUNDLE_PAGE = "page" 77 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test 2 | 3 | import android.app.Activity 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.net.toUri 8 | import com.davemorrissey.labs.subscaleview.test.R.string 9 | import com.davemorrissey.labs.subscaleview.test.animation.AnimationActivity 10 | import com.davemorrissey.labs.subscaleview.test.basicfeatures.BasicFeaturesActivity 11 | import com.davemorrissey.labs.subscaleview.test.configuration.ConfigurationActivity 12 | import com.davemorrissey.labs.subscaleview.test.databinding.MainActivityBinding 13 | import com.davemorrissey.labs.subscaleview.test.eventhandling.EventHandlingActivity 14 | import com.davemorrissey.labs.subscaleview.test.eventhandlingadvanced.AdvancedEventHandlingActivity 15 | import com.davemorrissey.labs.subscaleview.test.extension.ExtensionActivity 16 | import com.davemorrissey.labs.subscaleview.test.imagedisplay.ImageDisplayActivity 17 | import com.davemorrissey.labs.subscaleview.test.viewpager.ViewPagerActivity 18 | 19 | class MainActivity : AppCompatActivity() { 20 | 21 | private lateinit var binding: MainActivityBinding 22 | 23 | override fun onCreate(savedInstanceState: Bundle?) { 24 | super.onCreate(savedInstanceState) 25 | 26 | binding = MainActivityBinding.inflate(layoutInflater) 27 | setContentView(binding.root) 28 | 29 | actionBar?.setTitle(string.main_title) 30 | 31 | binding.basicFeatures.setOnClickListener { 32 | startActivity(BasicFeaturesActivity::class.java) 33 | } 34 | binding.imageDisplay.setOnClickListener { 35 | startActivity(ImageDisplayActivity::class.java) 36 | } 37 | binding.eventHandling.setOnClickListener { 38 | startActivity(EventHandlingActivity::class.java) 39 | } 40 | binding.advancedEventHandling.setOnClickListener { 41 | startActivity(AdvancedEventHandlingActivity::class.java) 42 | } 43 | binding.viewPagerGalleries.setOnClickListener { 44 | startActivity(ViewPagerActivity::class.java) 45 | } 46 | binding.animation.setOnClickListener { 47 | startActivity(AnimationActivity::class.java) 48 | } 49 | binding.extension.setOnClickListener { 50 | startActivity(ExtensionActivity::class.java) 51 | } 52 | binding.configuration.setOnClickListener { 53 | startActivity(ConfigurationActivity::class.java) 54 | } 55 | binding.github.setOnClickListener { 56 | openGitHub() 57 | } 58 | } 59 | 60 | private fun startActivity(activity: Class) { 61 | startActivity(Intent(this, activity)) 62 | } 63 | 64 | private fun openGitHub() { 65 | startActivity( 66 | Intent(Intent.ACTION_VIEW).apply { 67 | data = "https://github.com/tachiyomiorg/subsampling-scale-image-view".toUri() 68 | } 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/Page.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test 2 | 3 | data class Page(val subtitle: Int, val text: Int) -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/animation/AnimationActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.animation 2 | 3 | import android.graphics.PointF 4 | import android.os.Bundle 5 | import android.view.View 6 | import com.davemorrissey.labs.subscaleview.ImageSource 7 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 8 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 9 | import com.davemorrissey.labs.subscaleview.test.Page 10 | import com.davemorrissey.labs.subscaleview.test.R.id 11 | import com.davemorrissey.labs.subscaleview.test.R.layout 12 | import com.davemorrissey.labs.subscaleview.test.R.string 13 | import com.davemorrissey.labs.subscaleview.test.extension.views.PinView 14 | import java.util.Random 15 | 16 | class AnimationActivity : AbstractPagesActivity( 17 | string.animation_title, layout.animation_activity, listOf( 18 | Page(string.animation_p1_subtitle, string.animation_p1_text), 19 | Page(string.animation_p2_subtitle, string.animation_p2_text), 20 | Page(string.animation_p3_subtitle, string.animation_p3_text), 21 | Page(string.animation_p4_subtitle, string.animation_p4_text) 22 | ) 23 | ) { 24 | private var view: PinView? = null 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | findViewById(id.play).setOnClickListener { play() } 29 | view = findViewById(id.imageView) 30 | view?.setImage(ImageSource.asset(this, "sanmartino.jpg")) 31 | } 32 | 33 | override fun onPageChanged(page: Int) { 34 | if (page == 2) { 35 | view?.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_CENTER) 36 | } else { 37 | view?.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) 38 | } 39 | } 40 | 41 | private fun play() { 42 | val random = Random() 43 | if (view!!.isReady) { 44 | val maxScale = view!!.maxScale 45 | val minScale = view!!.minScale 46 | val scale = random.nextFloat() * (maxScale - minScale) + minScale 47 | val center = PointF( 48 | random.nextInt(view!!.sWidth).toFloat(), random.nextInt( 49 | view!!.sHeight 50 | ).toFloat() 51 | ) 52 | view!!.setPin(center) 53 | val animationBuilder = view!!.animateScaleAndCenter(scale, center) 54 | if (page == 3) { 55 | animationBuilder!!.withDuration(2000) 56 | .withEasing(SubsamplingScaleImageView.EASE_OUT_QUAD).withInterruptible(false) 57 | .start() 58 | } else { 59 | animationBuilder!!.withDuration(750).start() 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/basicfeatures/BasicFeaturesActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.basicfeatures 2 | 3 | import android.os.Bundle 4 | import com.davemorrissey.labs.subscaleview.ImageSource 5 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 6 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 7 | import com.davemorrissey.labs.subscaleview.test.Page 8 | import com.davemorrissey.labs.subscaleview.test.R.id 9 | import com.davemorrissey.labs.subscaleview.test.R.layout 10 | import com.davemorrissey.labs.subscaleview.test.R.string 11 | 12 | class BasicFeaturesActivity : AbstractPagesActivity( 13 | string.basic_title, layout.pages_activity, listOf( 14 | Page(string.basic_p1_subtitle, string.basic_p1_text), 15 | Page(string.basic_p2_subtitle, string.basic_p2_text), 16 | Page(string.basic_p3_subtitle, string.basic_p3_text), 17 | Page(string.basic_p4_subtitle, string.basic_p4_text), 18 | Page(string.basic_p5_subtitle, string.basic_p5_text) 19 | ) 20 | ) { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | val view = findViewById(id.imageView) 25 | view.setImage(ImageSource.asset(this, "sanmartino.jpg")) 26 | } 27 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/configuration/ConfigurationActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.configuration 2 | 3 | import android.graphics.PointF 4 | import android.os.Bundle 5 | import com.davemorrissey.labs.subscaleview.ImageSource 6 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 7 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 8 | import com.davemorrissey.labs.subscaleview.test.Page 9 | import com.davemorrissey.labs.subscaleview.test.R.id 10 | import com.davemorrissey.labs.subscaleview.test.R.layout 11 | import com.davemorrissey.labs.subscaleview.test.R.string 12 | 13 | class ConfigurationActivity : AbstractPagesActivity( 14 | string.configuration_title, layout.pages_activity, listOf( 15 | Page(string.configuration_p1_subtitle, string.configuration_p1_text), 16 | Page(string.configuration_p2_subtitle, string.configuration_p2_text), 17 | Page(string.configuration_p3_subtitle, string.configuration_p3_text), 18 | Page(string.configuration_p4_subtitle, string.configuration_p4_text), 19 | Page(string.configuration_p5_subtitle, string.configuration_p5_text), 20 | Page(string.configuration_p6_subtitle, string.configuration_p6_text), 21 | Page(string.configuration_p7_subtitle, string.configuration_p7_text), 22 | Page(string.configuration_p8_subtitle, string.configuration_p8_text), 23 | Page(string.configuration_p9_subtitle, string.configuration_p9_text), 24 | Page(string.configuration_p10_subtitle, string.configuration_p10_text) 25 | ) 26 | ) { 27 | 28 | private var view: SubsamplingScaleImageView? = null 29 | 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | view = findViewById(id.imageView) 33 | view?.setImage(ImageSource.asset(this, "card.png")) 34 | } 35 | 36 | override fun onPageChanged(page: Int) { 37 | if (page == 0) { 38 | view!!.setMinimumDpi(50) 39 | } else { 40 | view!!.maxScale = 2f 41 | } 42 | if (page == 1) { 43 | view!!.setMinimumTileDpi(50) 44 | } else { 45 | view!!.setMinimumTileDpi(320) 46 | } 47 | if (page == 4) { 48 | view!!.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER) 49 | } else if (page == 5) { 50 | view!!.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER_IMMEDIATE) 51 | } else { 52 | view!!.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED) 53 | } 54 | if (page == 6) { 55 | view!!.setDoubleTapZoomDpi(240) 56 | } else { 57 | view!!.setDoubleTapZoomScale(1f) 58 | } 59 | if (page == 7) { 60 | view!!.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_CENTER) 61 | } else if (page == 8) { 62 | view!!.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_OUTSIDE) 63 | } else { 64 | view!!.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE) 65 | } 66 | if (page == 9) { 67 | view!!.setDebug(true) 68 | } else { 69 | view!!.setDebug(false) 70 | } 71 | if (page == 2) { 72 | view!!.setScaleAndCenter(0f, PointF(3900f, 3120f)) 73 | view!!.isPanEnabled = false 74 | } else { 75 | view!!.isPanEnabled = true 76 | } 77 | if (page == 3) { 78 | view!!.setScaleAndCenter(1f, PointF(3900f, 3120f)) 79 | view!!.isZoomEnabled = false 80 | } else { 81 | view!!.isZoomEnabled = true 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/eventhandling/EventHandlingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.eventhandling 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.widget.Toast 6 | import com.davemorrissey.labs.subscaleview.ImageSource 7 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 8 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 9 | import com.davemorrissey.labs.subscaleview.test.Page 10 | import com.davemorrissey.labs.subscaleview.test.R.id 11 | import com.davemorrissey.labs.subscaleview.test.R.layout 12 | import com.davemorrissey.labs.subscaleview.test.R.string 13 | 14 | class EventHandlingActivity : AbstractPagesActivity( 15 | string.event_title, layout.pages_activity, listOf( 16 | Page(string.event_p1_subtitle, string.event_p1_text), 17 | Page(string.event_p2_subtitle, string.event_p2_text), 18 | Page(string.event_p3_subtitle, string.event_p3_text) 19 | ) 20 | ) { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | val imageView = findViewById(id.imageView) 24 | imageView.setImage(ImageSource.asset(this, "sanmartino.jpg")) 25 | imageView.setOnClickListener { v: View -> 26 | Toast.makeText( 27 | v.context, 28 | "Clicked", 29 | Toast.LENGTH_SHORT 30 | ).show() 31 | } 32 | imageView.setOnLongClickListener { v: View -> 33 | Toast.makeText(v.context, "Long clicked", Toast.LENGTH_SHORT).show() 34 | true 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/eventhandlingadvanced/AdvancedEventHandlingActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.eventhandlingadvanced 2 | 3 | import android.os.Bundle 4 | import android.view.GestureDetector 5 | import android.view.GestureDetector.SimpleOnGestureListener 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import android.widget.Toast 9 | import com.davemorrissey.labs.subscaleview.ImageSource 10 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 11 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 12 | import com.davemorrissey.labs.subscaleview.test.Page 13 | import com.davemorrissey.labs.subscaleview.test.R.id 14 | import com.davemorrissey.labs.subscaleview.test.R.layout 15 | import com.davemorrissey.labs.subscaleview.test.R.string 16 | 17 | class AdvancedEventHandlingActivity : AbstractPagesActivity( 18 | string.advancedevent_title, layout.pages_activity, listOf( 19 | Page(string.advancedevent_p1_subtitle, string.advancedevent_p1_text), 20 | Page(string.advancedevent_p2_subtitle, string.advancedevent_p2_text), 21 | Page(string.advancedevent_p3_subtitle, string.advancedevent_p3_text), 22 | Page(string.advancedevent_p4_subtitle, string.advancedevent_p4_text), 23 | Page(string.advancedevent_p5_subtitle, string.advancedevent_p5_text) 24 | ) 25 | ) { 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | val imageView = findViewById(id.imageView) 29 | val gestureDetector = GestureDetector(this, object : SimpleOnGestureListener() { 30 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean { 31 | if (imageView.isReady) { 32 | val sCoord = imageView.viewToSourceCoord(e.x, e.y) 33 | Toast.makeText( 34 | applicationContext, 35 | "Single tap: " + sCoord!!.x.toInt() + ", " + sCoord.y.toInt(), 36 | Toast.LENGTH_SHORT 37 | ).show() 38 | } else { 39 | Toast.makeText( 40 | applicationContext, 41 | "Single tap: Image not ready", 42 | Toast.LENGTH_SHORT 43 | ).show() 44 | } 45 | return true 46 | } 47 | 48 | override fun onLongPress(e: MotionEvent) { 49 | if (imageView.isReady) { 50 | val sCoord = imageView.viewToSourceCoord(e.x, e.y) 51 | Toast.makeText( 52 | applicationContext, 53 | "Long press: " + sCoord!!.x.toInt() + ", " + sCoord.y.toInt(), 54 | Toast.LENGTH_SHORT 55 | ).show() 56 | } else { 57 | Toast.makeText( 58 | applicationContext, 59 | "Long press: Image not ready", 60 | Toast.LENGTH_SHORT 61 | ).show() 62 | } 63 | } 64 | 65 | override fun onDoubleTap(e: MotionEvent): Boolean { 66 | if (imageView.isReady) { 67 | val sCoord = imageView.viewToSourceCoord(e.x, e.y) 68 | Toast.makeText( 69 | applicationContext, 70 | "Double tap: " + sCoord!!.x.toInt() + ", " + sCoord.y.toInt(), 71 | Toast.LENGTH_SHORT 72 | ).show() 73 | } else { 74 | Toast.makeText( 75 | applicationContext, 76 | "Double tap: Image not ready", 77 | Toast.LENGTH_SHORT 78 | ).show() 79 | } 80 | return true 81 | } 82 | }) 83 | imageView.setImage(ImageSource.asset(this, "sanmartino.jpg")) 84 | imageView.setOnTouchListener { view: View?, motionEvent: MotionEvent? -> 85 | gestureDetector.onTouchEvent( 86 | motionEvent!! 87 | ) 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/ExtensionActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension 2 | 3 | import android.util.Log 4 | import com.davemorrissey.labs.subscaleview.test.AbstractFragmentsActivity 5 | import com.davemorrissey.labs.subscaleview.test.Page 6 | import com.davemorrissey.labs.subscaleview.test.R.id 7 | import com.davemorrissey.labs.subscaleview.test.R.layout 8 | import com.davemorrissey.labs.subscaleview.test.R.string 9 | import com.davemorrissey.labs.subscaleview.test.imagedisplay.ImageDisplayActivity 10 | 11 | class ExtensionActivity : AbstractFragmentsActivity( 12 | string.extension_title, layout.fragments_activity, listOf( 13 | Page(string.extension_p1_subtitle, string.extension_p1_text), 14 | Page(string.extension_p2_subtitle, string.extension_p2_text), 15 | Page(string.extension_p3_subtitle, string.extension_p3_text) 16 | ) 17 | ) { 18 | override fun onPageChanged(page: Int) { 19 | try { 20 | supportFragmentManager 21 | .beginTransaction() 22 | .replace(id.frame, FRAGMENTS[page].newInstance()) 23 | .commit() 24 | } catch (e: Exception) { 25 | Log.e(ImageDisplayActivity::class.java.name, "Failed to load fragment", e) 26 | } 27 | } 28 | } 29 | 30 | private val FRAGMENTS = listOf( 31 | ExtensionPinFragment::class.java, 32 | ExtensionCircleFragment::class.java, 33 | ExtensionFreehandFragment::class.java 34 | ) -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/ExtensionCircleFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.davemorrissey.labs.subscaleview.ImageSource 9 | import com.davemorrissey.labs.subscaleview.test.databinding.ExtensionCircleFragmentBinding 10 | 11 | class ExtensionCircleFragment : Fragment() { 12 | 13 | private lateinit var binding: ExtensionCircleFragmentBinding 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View { 20 | binding = ExtensionCircleFragmentBinding.inflate(inflater, container, false) 21 | 22 | val activity = activity as? ExtensionActivity 23 | binding.previous.setOnClickListener { activity?.previous() } 24 | binding.next.setOnClickListener { activity?.next() } 25 | 26 | binding.imageView.setImage(ImageSource.asset(requireContext(), "sanmartino.jpg")) 27 | 28 | return binding.root 29 | } 30 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/ExtensionFreehandFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.davemorrissey.labs.subscaleview.ImageSource 9 | import com.davemorrissey.labs.subscaleview.test.databinding.ExtensionFreehandFragmentBinding 10 | 11 | class ExtensionFreehandFragment : Fragment() { 12 | 13 | private lateinit var binding: ExtensionFreehandFragmentBinding 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View { 20 | binding = ExtensionFreehandFragmentBinding.inflate(inflater, container, false) 21 | 22 | val activity = activity as? ExtensionActivity 23 | binding.previous.setOnClickListener { activity?.previous() } 24 | binding.reset.setOnClickListener { binding.imageView.reset() } 25 | 26 | binding.imageView.setImage(ImageSource.asset(requireContext(), "sanmartino.jpg")) 27 | 28 | return binding.root 29 | } 30 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/ExtensionPinFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension 2 | 3 | import android.graphics.PointF 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.davemorrissey.labs.subscaleview.ImageSource 10 | import com.davemorrissey.labs.subscaleview.test.databinding.ExtensionPinFragmentBinding 11 | 12 | class ExtensionPinFragment : Fragment() { 13 | 14 | private lateinit var binding: ExtensionPinFragmentBinding 15 | 16 | override fun onCreateView( 17 | inflater: LayoutInflater, 18 | container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ): View { 21 | binding = ExtensionPinFragmentBinding.inflate(inflater, container, false) 22 | 23 | val activity = activity as? ExtensionActivity 24 | binding.next.setOnClickListener { activity?.next() } 25 | 26 | binding.imageView.setImage(ImageSource.asset(requireContext(), "sanmartino.jpg")) 27 | binding.imageView.setPin(PointF(1602f, 405f)) 28 | 29 | return binding.root 30 | } 31 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/views/CircleView.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension.views 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Paint.Cap 8 | import android.graphics.PointF 9 | import android.util.AttributeSet 10 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 11 | 12 | class CircleView @JvmOverloads constructor(context: Context?, attr: AttributeSet? = null) : 13 | SubsamplingScaleImageView(context, attr) { 14 | 15 | private val sCenter = PointF() 16 | private val vCenter = PointF() 17 | private val paint = Paint() 18 | private var strokeWidth = 0 19 | 20 | init { 21 | initialise() 22 | } 23 | 24 | private fun initialise() { 25 | val density = resources.displayMetrics.densityDpi.toFloat() 26 | strokeWidth = (density / 60f).toInt() 27 | } 28 | 29 | override fun onDraw(canvas: Canvas) { 30 | super.onDraw(canvas) 31 | 32 | // Don't draw pin before image is ready so it doesn't move around during setup. 33 | if (!isReady) { 34 | return 35 | } 36 | 37 | sCenter[(sWidth / 2).toFloat()] = (sHeight / 2).toFloat() 38 | sourceToViewCoord(sCenter, vCenter) 39 | val radius = scale * sWidth * 0.25f 40 | paint.isAntiAlias = true 41 | paint.style = Paint.Style.STROKE 42 | paint.strokeCap = Cap.ROUND 43 | paint.strokeWidth = (strokeWidth * 2).toFloat() 44 | paint.color = Color.BLACK 45 | canvas.drawCircle(vCenter.x, vCenter.y, radius, paint) 46 | paint.strokeWidth = strokeWidth.toFloat() 47 | paint.color = Color.argb(255, 51, 181, 229) 48 | canvas.drawCircle(vCenter.x, vCenter.y, radius, paint) 49 | } 50 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/views/FreehandView.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension.views 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Color 6 | import android.graphics.Paint 7 | import android.graphics.Paint.Cap 8 | import android.graphics.Path 9 | import android.graphics.PointF 10 | import android.util.AttributeSet 11 | import android.view.MotionEvent 12 | import android.view.View 13 | import android.view.View.OnTouchListener 14 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 15 | import kotlin.math.abs 16 | 17 | class FreehandView @JvmOverloads constructor(context: Context?, attr: AttributeSet? = null) : 18 | SubsamplingScaleImageView(context, attr), OnTouchListener { 19 | 20 | private val paint = Paint() 21 | private val vPath = Path() 22 | private val vPoint = PointF() 23 | private var vPrev = PointF() 24 | private var vPrevious: PointF? = null 25 | private var vStart: PointF? = null 26 | private var drawing = false 27 | private var strokeWidth = 0 28 | private var sPoints: MutableList? = null 29 | 30 | init { 31 | setOnTouchListener(this) 32 | val density = resources.displayMetrics.densityDpi.toFloat() 33 | strokeWidth = (density / 60f).toInt() 34 | } 35 | 36 | override fun onTouch(view: View, motionEvent: MotionEvent): Boolean { 37 | return false 38 | } 39 | 40 | override fun onTouchEvent(event: MotionEvent): Boolean { 41 | if (sPoints != null && !drawing) { 42 | return super.onTouchEvent(event) 43 | } 44 | 45 | var consumed = false 46 | val touchCount = event.pointerCount 47 | when (event.actionMasked) { 48 | MotionEvent.ACTION_DOWN -> if (event.actionIndex == 0) { 49 | vStart = PointF(event.x, event.y) 50 | vPrevious = PointF(event.x, event.y) 51 | } else { 52 | vStart = null 53 | vPrevious = null 54 | } 55 | 56 | MotionEvent.ACTION_MOVE -> { 57 | val sCurrentF = viewToSourceCoord(event.x, event.y) 58 | val sCurrent = PointF(sCurrentF!!.x, sCurrentF.y) 59 | val sStart = if (vStart == null) null else PointF( 60 | viewToSourceCoord(vStart)!!.x, 61 | viewToSourceCoord(vStart)!!.y 62 | ) 63 | if (touchCount == 1 && vStart != null) { 64 | val vDX = abs(event.x - vPrevious!!.x) 65 | val vDY = abs(event.y - vPrevious!!.y) 66 | if (vDX >= strokeWidth * 5 || vDY >= strokeWidth * 5) { 67 | if (sPoints == null) { 68 | sPoints = ArrayList() 69 | sPoints?.add(sStart) 70 | } 71 | sPoints!!.add(sCurrent) 72 | vPrevious!!.x = event.x 73 | vPrevious!!.y = event.y 74 | drawing = true 75 | } 76 | consumed = true 77 | invalidate() 78 | } else if (touchCount == 1) { 79 | // Consume all one touch drags to prevent odd panning effects handled by the superclass. 80 | consumed = true 81 | } 82 | } 83 | 84 | MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { 85 | invalidate() 86 | drawing = false 87 | vPrevious = null 88 | vStart = null 89 | } 90 | } 91 | // Use parent to handle pinch and two-finger pan. 92 | return consumed || super.onTouchEvent(event) 93 | } 94 | 95 | override fun onDraw(canvas: Canvas) { 96 | super.onDraw(canvas) 97 | 98 | // Don't draw anything before image is ready. 99 | if (!isReady) { 100 | return 101 | } 102 | 103 | paint.isAntiAlias = true 104 | if (sPoints != null && sPoints!!.size >= 2) { 105 | vPath.reset() 106 | sourceToViewCoord(sPoints!![0]!!.x, sPoints!![0]!!.y, vPrev) 107 | vPath.moveTo(vPrev.x, vPrev.y) 108 | for (i in 1 until sPoints!!.size) { 109 | sourceToViewCoord(sPoints!![i]!!.x, sPoints!![i]!!.y, vPoint) 110 | vPath.quadTo(vPrev.x, vPrev.y, (vPoint.x + vPrev.x) / 2, (vPoint.y + vPrev.y) / 2) 111 | vPrev = vPoint 112 | } 113 | paint.style = Paint.Style.STROKE 114 | paint.strokeCap = Cap.ROUND 115 | paint.strokeWidth = (strokeWidth * 2).toFloat() 116 | paint.color = Color.BLACK 117 | canvas.drawPath(vPath, paint) 118 | paint.strokeWidth = strokeWidth.toFloat() 119 | paint.color = Color.argb(255, 51, 181, 229) 120 | canvas.drawPath(vPath, paint) 121 | } 122 | } 123 | 124 | fun reset() { 125 | sPoints = null 126 | invalidate() 127 | } 128 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/extension/views/PinView.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.extension.views 2 | 3 | import android.content.Context 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import android.graphics.Canvas 7 | import android.graphics.Paint 8 | import android.graphics.PointF 9 | import android.util.AttributeSet 10 | import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView 11 | import com.davemorrissey.labs.subscaleview.test.R.drawable 12 | 13 | class PinView @JvmOverloads constructor(context: Context?, attr: AttributeSet? = null) : 14 | SubsamplingScaleImageView(context, attr) { 15 | 16 | private val paint = Paint() 17 | private val vPin = PointF() 18 | private var sPin: PointF? = null 19 | private var pin: Bitmap? = null 20 | 21 | init { 22 | initialise() 23 | } 24 | 25 | fun setPin(sPin: PointF?) { 26 | this.sPin = sPin 27 | initialise() 28 | invalidate() 29 | } 30 | 31 | private fun initialise() { 32 | val density = resources.displayMetrics.densityDpi.toFloat() 33 | pin = BitmapFactory.decodeResource(this.resources, drawable.pushpin_blue) 34 | val w = density / 420f * pin!!.width 35 | val h = density / 420f * pin!!.height 36 | pin = Bitmap.createScaledBitmap(pin!!, w.toInt(), h.toInt(), true) 37 | } 38 | 39 | override fun onDraw(canvas: Canvas) { 40 | super.onDraw(canvas) 41 | 42 | // Don't draw pin before image is ready so it doesn't move around during setup. 43 | if (!isReady) { 44 | return 45 | } 46 | 47 | paint.isAntiAlias = true 48 | if (sPin != null && pin != null) { 49 | sourceToViewCoord(sPin, vPin) 50 | val vX = vPin.x - pin!!.width / 2 51 | val vY = vPin.y - pin!!.height 52 | canvas.drawBitmap(pin!!, vX, vY, paint) 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/imagedisplay/ImageDisplayActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.imagedisplay 2 | 3 | import android.util.Log 4 | import com.davemorrissey.labs.subscaleview.test.AbstractFragmentsActivity 5 | import com.davemorrissey.labs.subscaleview.test.Page 6 | import com.davemorrissey.labs.subscaleview.test.R.id 7 | import com.davemorrissey.labs.subscaleview.test.R.layout 8 | import com.davemorrissey.labs.subscaleview.test.R.string 9 | 10 | class ImageDisplayActivity : AbstractFragmentsActivity( 11 | string.display_title, layout.fragments_activity, listOf( 12 | Page(string.display_p1_subtitle, string.display_p1_text), 13 | Page(string.display_p2_subtitle, string.display_p2_text), 14 | Page(string.display_p3_subtitle, string.display_p3_text) 15 | ) 16 | ) { 17 | override fun onPageChanged(page: Int) { 18 | try { 19 | supportFragmentManager 20 | .beginTransaction() 21 | .replace(id.frame, fragments[page].getDeclaredConstructor().newInstance()) 22 | .commit() 23 | } catch (e: Exception) { 24 | Log.e(ImageDisplayActivity::class.java.name, "Failed to load fragment", e) 25 | } 26 | } 27 | } 28 | 29 | private val fragments = listOf( 30 | ImageDisplayLargeFragment::class.java, 31 | ImageDisplayRotateFragment::class.java, 32 | ImageDisplayRegionFragment::class.java 33 | ) -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/imagedisplay/ImageDisplayLargeFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.imagedisplay 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.davemorrissey.labs.subscaleview.ImageSource 9 | import com.davemorrissey.labs.subscaleview.test.databinding.ImagedisplayLargeFragmentBinding 10 | 11 | class ImageDisplayLargeFragment : Fragment() { 12 | 13 | private lateinit var binding: ImagedisplayLargeFragmentBinding 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View { 20 | binding = ImagedisplayLargeFragmentBinding.inflate(inflater, container, false) 21 | 22 | val activity = activity as? ImageDisplayActivity 23 | binding.next.setOnClickListener { activity?.next() } 24 | 25 | binding.imageView.setImage(ImageSource.asset(requireContext(), "card.png")) 26 | 27 | return binding.root 28 | } 29 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/imagedisplay/ImageDisplayRegionFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.imagedisplay 2 | 3 | import android.graphics.Rect 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import com.davemorrissey.labs.subscaleview.ImageSource 10 | import com.davemorrissey.labs.subscaleview.test.databinding.ImagedisplayRegionFragmentBinding 11 | 12 | class ImageDisplayRegionFragment : Fragment() { 13 | 14 | private lateinit var binding: ImagedisplayRegionFragmentBinding 15 | 16 | override fun onCreateView( 17 | inflater: LayoutInflater, 18 | container: ViewGroup?, 19 | savedInstanceState: Bundle? 20 | ): View { 21 | binding = ImagedisplayRegionFragmentBinding.inflate(inflater, container, false) 22 | 23 | val activity = activity as? ImageDisplayActivity 24 | binding.previous.setOnClickListener { activity?.previous() } 25 | 26 | binding.imageView.setImage( 27 | ImageSource.asset(requireContext(), "card.png").region(Rect(5200, 651, 8200, 3250)) 28 | ) 29 | 30 | return binding.root 31 | } 32 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/imagedisplay/ImageDisplayRotateFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.imagedisplay 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.davemorrissey.labs.subscaleview.ImageSource 9 | import com.davemorrissey.labs.subscaleview.test.databinding.ImagedisplayRotateFragmentBinding 10 | 11 | class ImageDisplayRotateFragment : Fragment() { 12 | 13 | private lateinit var binding: ImagedisplayRotateFragmentBinding 14 | 15 | override fun onCreateView( 16 | inflater: LayoutInflater, 17 | container: ViewGroup?, 18 | savedInstanceState: Bundle? 19 | ): View { 20 | binding = ImagedisplayRotateFragmentBinding.inflate(inflater, container, false) 21 | 22 | val activity = activity as? ImageDisplayActivity 23 | binding.previous.setOnClickListener { activity?.previous() } 24 | binding.next.setOnClickListener { activity?.next() } 25 | 26 | binding.imageView.setImage( 27 | ImageSource.asset(requireContext(), "swissroad.jpg") 28 | ) 29 | 30 | binding.rotate.setOnClickListener { 31 | binding.imageView.imageRotation = binding.imageView.imageRotation.rotateBy90Degrees() 32 | } 33 | 34 | return binding.root 35 | } 36 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/viewpager/VerticalViewPager.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.viewpager 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.MotionEvent 6 | import android.view.View 7 | import androidx.viewpager.widget.ViewPager 8 | 9 | /** 10 | * From http://stackoverflow.com/a/22797619/2719186 11 | */ 12 | class VerticalViewPager : ViewPager { 13 | 14 | constructor(context: Context?) : super(context!!) { 15 | init() 16 | } 17 | 18 | constructor(context: Context?, attrs: AttributeSet?) : super( 19 | context!!, attrs 20 | ) { 21 | init() 22 | } 23 | 24 | private fun init() { 25 | setPageTransformer(true, VerticalPageTransformer()) 26 | overScrollMode = OVER_SCROLL_NEVER 27 | } 28 | 29 | private fun swapXY(ev: MotionEvent): MotionEvent { 30 | val width = width.toFloat() 31 | val height = height.toFloat() 32 | val newX = ev.y / height * width 33 | val newY = ev.x / width * height 34 | ev.setLocation(newX, newY) 35 | return ev 36 | } 37 | 38 | override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { 39 | val intercepted = super.onInterceptTouchEvent(swapXY(ev)) 40 | swapXY(ev) 41 | return intercepted 42 | } 43 | 44 | override fun onTouchEvent(ev: MotionEvent): Boolean { 45 | return super.onTouchEvent(swapXY(ev)) 46 | } 47 | 48 | private class VerticalPageTransformer : PageTransformer { 49 | override fun transformPage(view: View, position: Float) { 50 | if (position < -1) { 51 | view.alpha = 0f 52 | } else if (position <= 1) { 53 | view.alpha = 1f 54 | view.translationX = view.width * -position 55 | val yPosition = position * view.height 56 | view.translationY = yPosition 57 | } else { 58 | view.alpha = 0f 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/viewpager/ViewPagerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.viewpager 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import androidx.fragment.app.Fragment 6 | import androidx.fragment.app.FragmentManager 7 | import androidx.fragment.app.FragmentStatePagerAdapter 8 | import androidx.viewpager.widget.ViewPager 9 | import com.davemorrissey.labs.subscaleview.test.AbstractPagesActivity 10 | import com.davemorrissey.labs.subscaleview.test.Page 11 | import com.davemorrissey.labs.subscaleview.test.R.id 12 | import com.davemorrissey.labs.subscaleview.test.R.layout 13 | import com.davemorrissey.labs.subscaleview.test.R.string 14 | 15 | class ViewPagerActivity : AbstractPagesActivity( 16 | string.pager_title, layout.view_pager, listOf( 17 | Page(string.pager_p1_subtitle, string.pager_p1_text), 18 | Page(string.pager_p2_subtitle, string.pager_p2_text) 19 | ) 20 | ) { 21 | override fun onCreate(savedInstanceState: Bundle?) { 22 | super.onCreate(savedInstanceState) 23 | 24 | val horizontalPager = findViewById(id.horizontal_pager) 25 | horizontalPager.adapter = ScreenSlidePagerAdapter(supportFragmentManager) 26 | 27 | val verticalPager = findViewById(id.vertical_pager) 28 | verticalPager.adapter = ScreenSlidePagerAdapter(supportFragmentManager) 29 | } 30 | 31 | override fun onBackPressed() { 32 | val viewPager = 33 | findViewById(if (page == 0) id.horizontal_pager else id.vertical_pager) 34 | if (viewPager.currentItem == 0) { 35 | super.onBackPressed() 36 | } else { 37 | viewPager.currentItem -= 1 38 | } 39 | } 40 | 41 | override fun onPageChanged(page: Int) { 42 | if (page == 0) { 43 | findViewById(id.horizontal_pager).visibility = View.VISIBLE 44 | findViewById(id.vertical_pager).visibility = View.GONE 45 | } else { 46 | findViewById(id.horizontal_pager).visibility = View.GONE 47 | findViewById(id.vertical_pager).visibility = View.VISIBLE 48 | } 49 | } 50 | } 51 | 52 | private class ScreenSlidePagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { 53 | override fun getItem(position: Int): Fragment { 54 | return ViewPagerFragment().apply { 55 | asset = IMAGES[position] 56 | } 57 | } 58 | 59 | override fun getCount(): Int { 60 | return IMAGES.size 61 | } 62 | } 63 | 64 | private val IMAGES = arrayOf("sanmartino.jpg", "swissroad.jpg") 65 | -------------------------------------------------------------------------------- /sample/src/main/kotlin/com/davemorrissey/labs/subscaleview/test/viewpager/ViewPagerFragment.kt: -------------------------------------------------------------------------------- 1 | package com.davemorrissey.labs.subscaleview.test.viewpager 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import com.davemorrissey.labs.subscaleview.ImageSource 9 | import com.davemorrissey.labs.subscaleview.test.databinding.ViewPagerPageBinding 10 | 11 | class ViewPagerFragment : Fragment() { 12 | 13 | private lateinit var binding: ViewPagerPageBinding 14 | 15 | var asset: String? = null 16 | 17 | override fun onCreateView( 18 | inflater: LayoutInflater, 19 | container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View { 22 | binding = ViewPagerPageBinding.inflate(inflater, container, false) 23 | 24 | if (savedInstanceState != null) { 25 | if (asset == null && savedInstanceState.containsKey(BUNDLE_ASSET)) { 26 | asset = savedInstanceState.getString(BUNDLE_ASSET) 27 | } 28 | } 29 | asset?.let { 30 | binding.imageView.setImage(ImageSource.asset(requireContext(), it)) 31 | } 32 | 33 | return binding.root 34 | } 35 | 36 | override fun onSaveInstanceState(outState: Bundle) { 37 | super.onSaveInstanceState(outState) 38 | 39 | if (view != null) { 40 | outState.putString(BUNDLE_ASSET, asset) 41 | } 42 | } 43 | } 44 | 45 | private const val BUNDLE_ASSET = "asset" 46 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/button_standout_inactive.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/button_standout_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/button_transparent_pressed.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/buttonstate_standout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/buttonstate_transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/pushpin_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-nodpi/pushpin_blue.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-nodpi/transparent.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-xhdpi/next.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-xhdpi/play.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/previous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-xhdpi/previous.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-xhdpi/reset.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tachiyomiorg/subsampling-scale-image-view/66e0db195d1e41436b8bc3a22fea551e5d457db8/sample/src/main/res/drawable-xhdpi/rotate.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/animation_activity.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 17 | 18 | 24 | 25 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/extension_circle_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 17 | 18 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/extension_freehand_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 18 | 19 | 25 | 26 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/extension_pin_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 13 | 14 | 18 | 19 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/fragments_activity.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/imagedisplay_large_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 13 | 14 | 18 | 19 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/imagedisplay_region_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 18 | 19 | 25 | 26 | 32 | 33 | 34 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/imagedisplay_rotate_fragment.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 12 | 13 | 17 | 18 | 24 | 25 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 59 | 60 | 61 | 62 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | 77 | 78 |