├── .github ├── dependabot.yml ├── release.yml ├── stale.yml └── workflows │ ├── Android-CI-release.yml │ ├── Android-CI.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── .gitmodules ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── LICENSE.txt ├── README.md ├── app ├── build.gradle └── src │ ├── androidTest │ └── java │ │ └── info │ │ └── touchimage │ │ └── demo │ │ ├── MainSmokeTest.kt │ │ ├── TouchTest.kt │ │ ├── ZoomTest.kt │ │ └── utils │ │ ├── MultiTouchDownEvent.kt │ │ └── TouchAction.kt │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── info │ │ └── touchimage │ │ └── demo │ │ ├── AnimateZoomActivity.kt │ │ ├── ChangeSizeExampleActivity.kt │ │ ├── GlideExampleActivity.kt │ │ ├── MainActivity.kt │ │ ├── MirroringExampleActivity.kt │ │ ├── RecyclerExampleActivity.kt │ │ ├── ShapedExampleActivity.kt │ │ ├── SingleTouchImageViewActivity.kt │ │ ├── SwitchImageExampleActivity.kt │ │ ├── SwitchScaleTypeExampleActivity.kt │ │ ├── ViewPager2ExampleActivity.kt │ │ ├── ViewPagerExampleActivity.kt │ │ └── custom │ │ ├── AdapterImages.kt │ │ └── ExtendedViewPager.kt │ └── res │ ├── drawable-hdpi │ └── icon.png │ ├── drawable-ldpi │ └── icon.png │ ├── drawable-mdpi │ └── icon.png │ ├── drawable │ ├── nature_1.jpg │ ├── nature_2.jpg │ ├── nature_3.jpg │ ├── nature_4.jpg │ ├── nature_5.jpg │ ├── nature_6.jpg │ ├── nature_7.jpg │ ├── nature_8.jpg │ └── numbers.png │ ├── layout │ ├── activity_change_size.xml │ ├── activity_glide.xml │ ├── activity_main.xml │ ├── activity_mirroring_example.xml │ ├── activity_recyclerview.xml │ ├── activity_shaped_example.xml │ ├── activity_single_touchimageview.xml │ ├── activity_switch_image_example.xml │ ├── activity_switch_scaletype_example.xml │ ├── activity_viewpager2_example.xml │ └── activity_viewpager_example.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── screenShotCompare.sh ├── screenshotsToCompare ├── MainSmokeTest_makeScreenshotOfShapedImage.png ├── MainSmokeTest_smokeTestSimplyStart.png ├── MainSmokeTest_testAnimateZoom.png ├── MainSmokeTest_testChangeSize.png ├── MainSmokeTest_testGlide.png ├── MainSmokeTest_testMirroring.png ├── MainSmokeTest_testRecycler.png ├── MainSmokeTest_testSingleTouch.png ├── MainSmokeTest_testSwitchImage.png ├── MainSmokeTest_testSwitchScale.png ├── MainSmokeTest_testView2Pager.png ├── MainSmokeTest_testViewPager.png ├── TouchTest_testSingleTouch-touch1.png ├── TouchTest_testSingleTouch-touch2.png ├── ZoomTest_zoom-1-init.png ├── ZoomTest_zoom-2-reset.png ├── ZoomTest_zoom-3-zoom.png └── ZoomTest_zoom-4-end.png ├── settings.gradle └── touchview ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── ortiz │ └── touchview │ ├── FixedPixel.kt │ ├── ImageActionState.kt │ ├── OnTouchCoordinatesListener.kt │ ├── OnTouchImageViewListener.kt │ ├── OnZoomFinishedListener.kt │ ├── TouchImageView.kt │ └── ZoomVariables.kt └── res └── values └── attrs_touchimageview.xml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "github-actions" 13 | directory: "/" # Location of package manifests 14 | schedule: 15 | interval: "weekly" -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - someuser 7 | categories: 8 | - title: Breaking Changes 🛠 9 | labels: 10 | - breaking-change 11 | - title: Exciting New Features 🎉 12 | labels: 13 | - enhancement 14 | - title: Dependencies 15 | labels: 16 | - dependencies 17 | - title: Espresso test 18 | labels: 19 | - Espresso 20 | - title: Other Changes 21 | labels: 22 | - "*" -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 360 5 | 6 | # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. 7 | # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. 8 | daysUntilClose: 90 9 | 10 | # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) 11 | onlyLabels: [] 12 | 13 | # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable 14 | exemptLabels: 15 | - pinned 16 | 17 | # Set to true to ignore issues in a project (defaults to false) 18 | exemptProjects: false 19 | 20 | # Set to true to ignore issues in a milestone (defaults to false) 21 | exemptMilestones: false 22 | 23 | # Set to true to ignore issues with an assignee (defaults to false) 24 | exemptAssignees: false 25 | 26 | # Label to use when marking as stale 27 | staleLabel: "stale" 28 | 29 | # Comment to post when marking as stale. Set to `false` to disable 30 | markComment: > 31 | This issue has been automatically marked as stale because it has not had 32 | recent activity. Please comment here if it is still valid so that we can 33 | reprioritize. Thank you! 34 | 35 | # Comment to post when removing the stale label. 36 | # unmarkComment: > 37 | # Your comment here. 38 | 39 | # Comment to post when closing a stale Issue or Pull Request. 40 | closeComment: > 41 | Closing this. Please reopen if you believe it should be addressed. Thank you for your contribution. 42 | 43 | # Limit the number of actions per hour, from 1-30. Default is 30 44 | limitPerRun: 20 45 | 46 | # Limit to only `issues` or `pulls` 47 | # only: issues 48 | 49 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 50 | # pulls: 51 | # daysUntilStale: 30 52 | # markComment: > 53 | # This pull request has been automatically marked as stale because it has not had 54 | # recent activity. It will be closed if no further activity occurs. Thank you 55 | # for your contributions. 56 | 57 | # issues: 58 | # exemptLabels: 59 | # - confirmed 60 | -------------------------------------------------------------------------------- /.github/workflows/Android-CI-release.yml: -------------------------------------------------------------------------------- 1 | name: Release with changelog 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - name: Install JDK ${{ matrix.java_version }} 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'adopt' 21 | java-version: 17 22 | 23 | - name: Install Android SDK 24 | uses: hannesa2/action-android/install-sdk@0.1.16.7 25 | 26 | - name: Build project 27 | run: ./gradlew assembleRelease 28 | env: 29 | VERSION: ${{ github.ref }} 30 | 31 | - name: Get the version 32 | id: tagger 33 | uses: jimschubert/query-tag-action@v2 34 | with: 35 | skip-unshallow: 'true' 36 | abbrev: false 37 | commit-ish: HEAD 38 | 39 | - name: Create Release 40 | uses: softprops/action-gh-release@v2 41 | with: 42 | tag_name: ${{steps.tagger.outputs.tag}} 43 | name: ${{steps.tagger.outputs.tag}} 44 | generate_release_notes: true 45 | files: touchview/build/outputs/aar/touchview-release.aar 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | -------------------------------------------------------------------------------- /.github/workflows/Android-CI.yml: -------------------------------------------------------------------------------- 1 | name: PullRequest 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | buildTest: 11 | name: Espresso 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ ubuntu-22.04 ] 16 | api: [ 28 ] 17 | tag: [ default ] 18 | abi: [ x86_64 ] 19 | java_version: [ 17 ] 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | submodules: true 26 | - name: kvm support 27 | run: | 28 | egrep -c '(vmx|svm)' /proc/cpuinfo 29 | id 30 | sudo adduser $USER kvm 31 | sudo chown -R $USER /dev/kvm 32 | id 33 | - name: prepare 34 | run: | 35 | sudo apt-get update && sudo apt-get install -y exiftool imagemagick xdg-utils libimage-exiftool-perl zsh jq xorg 36 | # brew install exiftool imagemagick 37 | - name: Install JDK ${{ matrix.java_version }} 38 | uses: actions/setup-java@v4 39 | with: 40 | distribution: 'adopt' 41 | java-version: ${{ matrix.java_version }} 42 | - name: Install Android SDK 43 | uses: hannesa2/action-android/install-sdk@0.1.16.7 44 | - name: Build project 45 | run: ./gradlew assembleDebug 46 | - name: Run tests 47 | run: ./gradlew test 48 | - name: Run instrumentation tests 49 | uses: hannesa2/action-android/emulator-run-cmd@0.1.16.7 50 | with: 51 | cmd: ./gradlew :app:cAT 52 | api: ${{ matrix.api }} 53 | tag: ${{ matrix.tag }} 54 | abi: ${{ matrix.abi }} 55 | cmdOptions: -noaudio -no-boot-anim -no-window -metrics-to-console 56 | bootTimeout: 720 57 | disableAnimations: true 58 | - name: Archive Espresso results 59 | uses: actions/upload-artifact@v4 60 | if: ${{ always() }} 61 | with: 62 | name: Touch-Espresso-report 63 | path: app/build/reports/androidTests/connected 64 | if-no-files-found: error 65 | - name: Archive screenshots 66 | if: ${{ always() }} 67 | uses: actions/upload-artifact@v4 68 | with: 69 | name: Touch-Screenshots 70 | path: | 71 | app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected 72 | app/build/outputs/androidTest-results/connected 73 | - name: Compare screenshots 74 | run: | 75 | ls -ls app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected 76 | cp app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/emulator-5554\ -\ 9/* screenshotsToCompare 77 | export DISPLAY=:99 78 | sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & 79 | ./screenShotCompare.sh 80 | - name: Archive screenshots diffs 81 | if: ${{ always() }} 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: Touch-Screenshots-diffs 85 | path: | 86 | screenshotDiffs 87 | - name: Show git status 88 | if: ${{ always() }} 89 | run: | 90 | git add screenshotsToCompare 91 | git status 92 | [ "$(git status -s -uno)" ] && exit 1 || exit 0 93 | Check: 94 | name: Check 95 | runs-on: ${{ matrix.os }} 96 | strategy: 97 | matrix: 98 | os: [ ubuntu-latest ] 99 | java_version: [ 17 ] 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@v4 103 | with: 104 | fetch-depth: 0 105 | submodules: true 106 | - name: Install JDK ${{ matrix.java_version }} 107 | uses: actions/setup-java@v4 108 | with: 109 | distribution: 'adopt' 110 | java-version: ${{ matrix.java_version }} 111 | - uses: gradle/actions/wrapper-validation@v4 112 | - name: Install Android SDK 113 | uses: hannesa2/action-android/install-sdk@0.1.16.7 114 | - name: Code checks 115 | run: ./gradlew check 116 | - name: Archive Lint report 117 | uses: actions/upload-artifact@v4 118 | if: ${{ always() }} 119 | with: 120 | name: TouchImage-Lint-report 121 | path: app/build/reports/lint-results*.html 122 | -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "36 6 * * MON" 6 | 7 | jobs: 8 | update-gradle-wrapper: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Install JDK 14 | uses: actions/setup-java@v4 15 | with: 16 | distribution: 'adopt' 17 | java-version: 17 18 | - name: Update Gradle Wrapper 19 | uses: gradle-update/update-gradle-wrapper-action@v2 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | set-distribution-checksum: false 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OSX 2 | .DS_Store 3 | 4 | # Built application files 5 | *.apk 6 | *.ap_ 7 | 8 | # Files for the Dalvik VM 9 | *.dex 10 | 11 | # Java class files 12 | *.class 13 | 14 | # Generated files 15 | bin/ 16 | gen/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | 25 | # Proguard folder generated by Eclipse 26 | proguard/ 27 | 28 | # Log Files 29 | *.log 30 | 31 | # Android Studio 32 | .idea/* 33 | !.idea/codeStyles/ 34 | *.iml 35 | 36 | screenshots 37 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "git-diff-image"] 2 | path = git-diff-image 3 | url = git@github.com:ewanmellor/git-diff-image.git 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 30 | 153 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2021 The authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/MikeOrtiz/TouchImageView.svg)](https://jitpack.io/#hannesa2/TouchImageView) 2 | 3 | # TouchImageView for Android 4 | 5 | ## Capabilities 6 | 7 | TouchImageView extends ImageView and supports all of ImageView’s functionality. In addition, TouchImageView adds pinch zoom, dragging, fling, double tap zoom functionality and other animation polish. The intention is for TouchImageView to mirror as closely as possible the functionality of zoomable images in Gallery apps. 8 | 9 | ## Project status: maintenance mode 10 | Issues are ignored, but pull requests are not. If you need to get something done, submit a PR! 11 | 12 | ## Download 13 | Repository available on https://jitpack.io/#MikeOrtiz/TouchImageView 14 | 15 | ```Gradle 16 | allprojects { 17 | repositories { 18 | ... 19 | maven { url 'https://jitpack.io' } 20 | } 21 | } 22 | ``` 23 | ```Gradle 24 | dependencies { 25 | implementation 'com.github.MikeOrtiz:TouchImageView:1.4.1' // last SupportLib version 26 | // or 27 | implementation 'com.github.MikeOrtiz:TouchImageView:$LAST_VERSION' // Android X 28 | } 29 | 30 | ``` 31 | 32 | ## Examples 33 | 34 | Please view the sample app which includes examples of the following functionality: 35 | 36 | #### Single TouchImageView 37 | 38 | Basic use of a single TouchImageView. Includes usage of `OnTouchImageViewListener`, `getScrollPosition()`, `getZoomedRect()`, `isZoomed()`, and `getCurrentZoom()`. 39 | 40 | #### ViewPager Example 41 | 42 | TouchImageViews placed in a ViewPager like the Gallery app. 43 | 44 | #### Mirroring Example 45 | 46 | Mirror two TouchImageViews using `onTouchImageViewListener` and `setZoom()`. 47 | 48 | #### Switch Image Example 49 | 50 | Click on TouchImageView to cycle through images. Note that the zoom state is maintained though the images are switched. 51 | 52 | #### Switch ScaleType Example 53 | 54 | Click on TouchImageView to cycle through supported ScaleTypes. 55 | 56 | #### Resize Example 57 | 58 | Click on the arrow buttons to change the shape and size of the TouchImageView. See how the view looks when it shrinks with various "resize" settings. Read ChangeSizeExampleActivity.java's comment for advice on how to set up a TouchImageView that's going to be resized. 59 | 60 | ## Limitations 61 | 62 | TouchImageView does not yet support pinch image rotation. Also, `FIT_START` and `FIT_END` scaleTypes are not yet supported. 63 | 64 | ## API 65 | 66 | Get the current zoom. This is the zoom relative to the initial scale, not the original resource. 67 | 68 | float getCurrentZoom(); 69 | 70 | Get the max zoom multiplier. 71 | 72 | float getMaxZoom(); 73 | 74 | Get the min zoom multiplier. 75 | 76 | float getMinZoom(); 77 | 78 | Return the point at the center of the zoomable image. The `PointF` coordinates range in value between 0 and 1 and the focus point is denoted as a fraction from the left and top of the view. For example, the top left corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 79 | 80 | PointF getScrollPosition(); 81 | 82 | Return a `RectF` representing the zoomed image. 83 | 84 | RectF getZoomedRect(); 85 | 86 | Returns `false` if image is in initial, unzoomed state. `True`, otherwise. 87 | 88 | boolean isZoomed(); 89 | 90 | Reset zoom and translation to initial state. 91 | 92 | void resetZoom(); 93 | 94 | Set the max zoom multiplier. Default value is 3. 95 | 96 | void setMaxZoom(float max); 97 | 98 | Set the min zoom multiplier. Default value is 1. Set to `TouchImageView.AUTOMATIC_MIN_ZOOM` to make it possible to see the whole image. 99 | 100 | void setMinZoom(float min); 101 | 102 | Set the max zoom multiplier to stay at a fixed multiple of the min zoom multiplier. 103 | 104 | void setMaxZoomRatio(float max); 105 | 106 | Set the focus point of the zoomed image. The focus points are denoted as a fraction from the left and top of the view. The focus points can range in value between 0 and 1. 107 | 108 | void setScrollPosition(float focusX, float focusY); 109 | 110 | Set zoom to the specified scale. Image will be centered by default. 111 | 112 | void setZoom(float scale); 113 | 114 | Set zoom to the specified scale. Image will be centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the focus point as a fraction from the left and top of the view. For example, the top left corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 115 | 116 | void setZoom(float scale, float focusX, float focusY); 117 | 118 | Set zoom to the specified scale. Image will be centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the focus point as a fraction from the left and top of the view. For example, the top left corner of the image would be (0, 0). And the bottom right corner would be (1, 1). 119 | 120 | void setZoom(float scale, float focusX, float focusY, ScaleType scaleType); 121 | 122 | Set zoom parameters equal to another `TouchImageView`. Including scale, position, and `ScaleType`. 123 | 124 | void setZoom(TouchImageView img); 125 | 126 | Set which part of the image should remain fixed if the TouchImageView is resized. 127 | 128 | setViewSizeChangeFixedPixel(FixedPixel fixedPixel) 129 | 130 | Set which part of the image should remain fixed if the screen is rotated. 131 | 132 | setOrientationChangeFixedPixel(FixedPixel fixedPixel) 133 | 134 | ## License 135 | 136 | TouchImageView is available under the MIT license. See the LICENSE file for more info. 137 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | defaultConfig { 8 | applicationId "com.ortiz.touchdemo" 9 | versionCode getGitCommitCount() 10 | versionName getTag() 11 | 12 | minSdkVersion 21 // because of testLib Moka 13 | compileSdk defaultCompileSdkVersion 14 | targetSdkVersion defaultTargetSdkVersion 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | testInstrumentationRunnerArguments useTestStorageService: 'true' 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' 24 | } 25 | } 26 | 27 | buildFeatures { 28 | viewBinding true 29 | } 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_17 32 | targetCompatibility JavaVersion.VERSION_17 33 | } 34 | 35 | namespace 'info.touchimage.demo' 36 | } 37 | 38 | dependencies { 39 | implementation project(':touchview') 40 | implementation 'androidx.core:core-ktx:1.16.0' 41 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 42 | implementation 'androidx.recyclerview:recyclerview:1.4.0' 43 | implementation "androidx.viewpager2:viewpager2:1.1.0" 44 | implementation 'com.github.bumptech.glide:glide:4.16.0' 45 | 46 | androidTestImplementation 'com.github.AppDevNext:Moka:1.7' 47 | androidTestImplementation "androidx.test.ext:junit-ktx:1.2.1" 48 | androidTestImplementation "com.github.AppDevNext.Logcat:LogcatCoreLib:3.3.1" 49 | androidTestUtil "androidx.test.services:test-services:1.5.0" 50 | androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1" 51 | androidTestImplementation 'androidx.test.espresso:espresso-intents:3.6.1' 52 | } 53 | -------------------------------------------------------------------------------- /app/src/androidTest/java/info/touchimage/demo/MainSmokeTest.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.graphics.Bitmap 4 | import androidx.test.core.graphics.writeToTestStorage 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.action.ViewActions 7 | import androidx.test.espresso.action.ViewActions.captureToBitmap 8 | import androidx.test.espresso.assertion.ViewAssertions.matches 9 | import androidx.test.espresso.intent.Intents 10 | import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent 11 | import androidx.test.espresso.matcher.ViewMatchers.* 12 | import androidx.test.ext.junit.rules.activityScenarioRule 13 | import androidx.test.ext.junit.runners.AndroidJUnit4 14 | import com.moka.lib.assertions.WaitingAssertion 15 | import org.hamcrest.CoreMatchers.containsString 16 | import org.junit.* 17 | import org.junit.rules.TestName 18 | import org.junit.runner.RunWith 19 | 20 | @RunWith(AndroidJUnit4::class) 21 | class MainSmokeTest { 22 | 23 | @get:Rule 24 | val activityScenarioRule = activityScenarioRule() 25 | 26 | @get:Rule 27 | var nameRule = TestName() 28 | 29 | @Before 30 | fun setUp() { 31 | Intents.init() 32 | } 33 | 34 | @After 35 | fun cleanUp() { 36 | Intents.release() 37 | } 38 | 39 | @Test 40 | fun smokeTestSimplyStart() { 41 | onView(isRoot()) 42 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 43 | } 44 | 45 | @Test 46 | fun testSingleTouch() { 47 | onView(withId(R.id.single_touchimageview_button)).perform(ViewActions.click()) 48 | Intents.intended(hasComponent(SingleTouchImageViewActivity::class.java.name)) 49 | onView(isRoot()) 50 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 51 | } 52 | 53 | @Test 54 | fun testViewPager() { 55 | onView(withId(R.id.viewpager_example_button)).perform(ViewActions.click()) 56 | Intents.intended(hasComponent(ViewPagerExampleActivity::class.java.name)) 57 | onView(isRoot()) 58 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 59 | } 60 | 61 | @Test 62 | fun testView2Pager() { 63 | onView(withId(R.id.viewpager2_example_button)).perform(ViewActions.click()) 64 | Intents.intended(hasComponent(ViewPager2ExampleActivity::class.java.name)) 65 | onView(isRoot()) 66 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 67 | } 68 | 69 | @Test 70 | fun testMirroring() { 71 | onView(withId(R.id.mirror_touchimageview_button)).perform(ViewActions.click()) 72 | Intents.intended(hasComponent(MirroringExampleActivity::class.java.name)) 73 | onView(isRoot()) 74 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 75 | } 76 | 77 | @Test 78 | fun testSwitchImage() { 79 | onView(withId(R.id.switch_image_button)).perform(ViewActions.click()) 80 | Intents.intended(hasComponent(SwitchImageExampleActivity::class.java.name)) 81 | onView(isRoot()) 82 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 83 | } 84 | 85 | @Test 86 | fun testSwitchScale() { 87 | onView(withId(R.id.switch_scaletype_button)).perform(ViewActions.click()) 88 | Intents.intended(hasComponent(SwitchScaleTypeExampleActivity::class.java.name)) 89 | onView(isRoot()) 90 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 91 | } 92 | 93 | @Test 94 | fun testChangeSize() { 95 | onView(withId(R.id.resize_button)).perform(ViewActions.click()) 96 | Intents.intended(hasComponent(ChangeSizeExampleActivity::class.java.name)) 97 | Thread.sleep(500) 98 | onView(isRoot()) 99 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 100 | } 101 | 102 | @Test 103 | fun testRecycler() { 104 | onView(withId(R.id.recycler_button)).perform(ViewActions.click()) 105 | Intents.intended(hasComponent(RecyclerExampleActivity::class.java.name)) 106 | onView(isRoot()) 107 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 108 | } 109 | 110 | @Test 111 | fun testAnimateZoom() { 112 | onView(withId(R.id.animate_button)).perform(ViewActions.click()) 113 | Intents.intended(hasComponent(AnimateZoomActivity::class.java.name)) 114 | onView(isRoot()) 115 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 116 | } 117 | 118 | @Test 119 | fun testGlide() { 120 | onView(withId(R.id.glide_button)).perform(ViewActions.click()) 121 | Intents.intended(hasComponent(GlideExampleActivity::class.java.name)) 122 | 123 | WaitingAssertion.checkAssertion(R.id.textLoaded, isDisplayed(), 1500) 124 | onView(withId(R.id.textLoaded)).check( matches(withText(containsString(" ms")))) 125 | onView(isRoot()) 126 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 127 | } 128 | 129 | @Test 130 | fun makeScreenshotOfShapedImage() { 131 | onView(withId(R.id.shaped_image_button)).perform(ViewActions.click()) 132 | Intents.intended(hasComponent(ShapedExampleActivity::class.java.name)) 133 | onView(isRoot()) 134 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}") }) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/androidTest/java/info/touchimage/demo/TouchTest.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.graphics.Bitmap 4 | import androidx.test.core.graphics.writeToTestStorage 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.action.ViewActions.captureToBitmap 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import androidx.test.espresso.matcher.ViewMatchers.withId 9 | import androidx.test.ext.junit.rules.activityScenarioRule 10 | import androidx.test.ext.junit.runners.AndroidJUnit4 11 | import info.touchimage.demo.utils.MultiTouchDownEvent 12 | import info.touchimage.demo.utils.TouchAction 13 | import org.junit.Ignore 14 | import org.junit.Rule 15 | import org.junit.Test 16 | import org.junit.rules.TestName 17 | import org.junit.runner.RunWith 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | class TouchTest { 21 | 22 | @get:Rule 23 | val activityScenarioRule = activityScenarioRule() 24 | 25 | @get:Rule 26 | var nameRule = TestName() 27 | 28 | @Test 29 | fun testSingleTouch() { 30 | onView(withId(R.id.imageSingle)).perform(TouchAction(4f, 8f)) 31 | onView(ViewMatchers.isRoot()) 32 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-touch1") }) 33 | onView(withId(R.id.imageSingle)).perform(TouchAction(40f, 80f)) 34 | Thread.sleep(300) 35 | onView(ViewMatchers.isRoot()) 36 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-touch2") }) 37 | } 38 | 39 | @Test 40 | @Ignore("It is flaky") 41 | fun testMultiTouch() { 42 | onView(ViewMatchers.isRoot()) 43 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-before") }) 44 | val touchList: Array> = listOf( 45 | Pair(4f, 8f), 46 | Pair(40f, 80f), 47 | Pair(30f, 70f) 48 | ).toTypedArray() 49 | onView(withId(R.id.imageSingle)).perform(MultiTouchDownEvent(touchList)) 50 | onView(ViewMatchers.isRoot()) 51 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-after") }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/androidTest/java/info/touchimage/demo/ZoomTest.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.graphics.Bitmap 4 | import androidx.test.core.graphics.writeToTestStorage 5 | import androidx.test.espresso.Espresso.onView 6 | import androidx.test.espresso.action.ViewActions 7 | import androidx.test.espresso.action.ViewActions.captureToBitmap 8 | import androidx.test.espresso.matcher.ViewMatchers 9 | import androidx.test.espresso.matcher.ViewMatchers.withId 10 | import androidx.test.ext.junit.rules.activityScenarioRule 11 | import androidx.test.ext.junit.runners.AndroidJUnit4 12 | import org.junit.Rule 13 | import org.junit.Test 14 | import org.junit.rules.TestName 15 | import org.junit.runner.RunWith 16 | 17 | @RunWith(AndroidJUnit4::class) 18 | class ZoomTest { 19 | 20 | @get:Rule 21 | val activityScenarioRule = activityScenarioRule() 22 | 23 | @get:Rule 24 | var nameRule = TestName() 25 | 26 | @Test 27 | fun zoom() { 28 | Thread.sleep(WAIT) 29 | onView(ViewMatchers.isRoot()) 30 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-1-init") }) 31 | onView(withId(R.id.current_zoom)).perform(ViewActions.click()) 32 | Thread.sleep(WAIT) 33 | onView(ViewMatchers.isRoot()) 34 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-2-reset") }) 35 | onView(withId(R.id.current_zoom)).perform(ViewActions.click()) 36 | Thread.sleep(WAIT) 37 | onView(ViewMatchers.isRoot()) 38 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-3-zoom") }) 39 | onView(withId(R.id.current_zoom)).perform(ViewActions.click()) 40 | Thread.sleep(WAIT) 41 | onView(ViewMatchers.isRoot()) 42 | .perform(captureToBitmap { bitmap: Bitmap -> bitmap.writeToTestStorage("${javaClass.simpleName}_${nameRule.methodName}-4-end") }) 43 | } 44 | 45 | companion object { 46 | const val WAIT = 600L 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/androidTest/java/info/touchimage/demo/utils/MultiTouchDownEvent.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo.utils 2 | 3 | import android.os.SystemClock 4 | import android.view.InputDevice 5 | import android.view.MotionEvent 6 | import android.view.View 7 | import androidx.test.espresso.UiController 8 | import androidx.test.espresso.ViewAction 9 | import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 10 | import org.hamcrest.Matcher 11 | 12 | class MultiTouchDownEvent(private val locations: Array>) : ViewAction { 13 | 14 | override fun getDescription() = "Multi Touch Event" 15 | 16 | override fun getConstraints(): Matcher = isDisplayed() 17 | 18 | override fun perform(uiController: UiController, view: View) { 19 | 20 | val screenPos = IntArray(2) 21 | view.getLocationOnScreen(screenPos) 22 | 23 | val coordinatesList = buildCoordinatesList(screenPos) 24 | val pointerProperties = buildPointerPropertiesList() 25 | println(coordinatesList) 26 | val downTime = SystemClock.uptimeMillis() 27 | val eventTime = SystemClock.uptimeMillis() 28 | 29 | for (i in coordinatesList.indices) { 30 | val pointerCount = i + 1 31 | 32 | val coordinatesSlice = coordinatesList.subList(0, pointerCount) 33 | val propertiesSlice = pointerProperties.subList(0, pointerCount) 34 | 35 | val eventType = pointerDownEventType(pointerCount) 36 | 37 | val event = MotionEvent.obtain( 38 | downTime, 39 | eventTime, 40 | eventType, 41 | pointerCount, 42 | propertiesSlice.toTypedArray(), 43 | coordinatesSlice.toTypedArray(), 44 | 0, 45 | 0, 46 | 1f, 47 | 1f, 48 | 0, 49 | 0, 50 | InputDevice.SOURCE_UNKNOWN, 51 | 0 52 | ) 53 | 54 | uiController.injectMotionEvent(event) 55 | 56 | event.recycle() 57 | } 58 | } 59 | 60 | private fun buildCoordinatesList(screenPosition: IntArray): List { 61 | return locations.map { 62 | val coordinate = MotionEvent.PointerCoords() 63 | coordinate.x = it.first + screenPosition[0] 64 | coordinate.y = it.second + screenPosition[1] 65 | 66 | 67 | coordinate.pressure = 1f 68 | coordinate.size = 1f 69 | coordinate 70 | } 71 | } 72 | 73 | private fun buildPointerPropertiesList(): List { 74 | return IntArray(locations.count()) { it }.map { 75 | val pointer = MotionEvent.PointerProperties() 76 | pointer.id = it 77 | pointer 78 | } 79 | } 80 | 81 | private fun pointerDownEventType(numberOfPointers: Int): Int { 82 | if (numberOfPointers < 1) return -1 83 | 84 | var eventType = if (numberOfPointers == 1) MotionEvent.ACTION_DOWN else MotionEvent.ACTION_POINTER_DOWN 85 | eventType += (numberOfPointers shl MotionEvent.ACTION_POINTER_INDEX_SHIFT) 86 | 87 | return eventType 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/androidTest/java/info/touchimage/demo/utils/TouchAction.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo.utils 2 | 3 | import android.view.View 4 | import androidx.test.espresso.UiController 5 | import androidx.test.espresso.ViewAction 6 | import androidx.test.espresso.action.MotionEvents 7 | import androidx.test.espresso.matcher.ViewMatchers 8 | import org.hamcrest.Matcher 9 | 10 | class TouchAction(private val x: Float, private val y: Float) : ViewAction { 11 | 12 | override fun getConstraints(): Matcher = ViewMatchers.isDisplayed() 13 | 14 | override fun getDescription() = "Send touch events" 15 | 16 | override fun perform(uiController: UiController, view: View) { 17 | // Get view absolute position 18 | val location = IntArray(2) 19 | view.getLocationOnScreen(location) 20 | 21 | // Offset coordinates by view position 22 | val coordinates = floatArrayOf(x + location[0], y + location[1]) 23 | val precision = floatArrayOf(1f, 1f) 24 | 25 | // Send down event, pause, and send up 26 | val down = MotionEvents.sendDown(uiController, coordinates, precision).down 27 | uiController.loopMainThreadForAtLeast(200) 28 | MotionEvents.sendUp(uiController, down, coordinates) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/AnimateZoomActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import com.ortiz.touchview.OnZoomFinishedListener 7 | import info.touchimage.demo.databinding.ActivitySingleTouchimageviewBinding 8 | 9 | 10 | class AnimateZoomActivity : AppCompatActivity(), OnZoomFinishedListener { 11 | 12 | private lateinit var binding: ActivitySingleTouchimageviewBinding 13 | 14 | @SuppressLint("SetTextI18n") 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | 18 | // https://developer.android.com/topic/libraries/view-binding 19 | binding = ActivitySingleTouchimageviewBinding.inflate(layoutInflater) 20 | val view = binding.root 21 | setContentView(view) 22 | 23 | binding.imageSingle.setImageResource(R.drawable.numbers) 24 | 25 | binding.currentZoom.setOnClickListener { 26 | when { 27 | binding.imageSingle.isZoomed -> binding.imageSingle.resetZoomAnimated() 28 | binding.imageSingle.isZoomed.not() -> binding.imageSingle.setZoomAnimated(3f, 0.75f, 0.75f, this) 29 | } 30 | } 31 | 32 | // It's not needed, but help to see if it proper centers on bigger screens (or smaller images) 33 | binding.imageSingle.setZoom(1.1f, 0f, 0f) 34 | } 35 | 36 | @SuppressLint("SetTextI18n") 37 | override fun onZoomFinished() { 38 | binding.scrollPosition.text = "Zoom done" 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/ChangeSizeExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.animation.ValueAnimator 4 | import android.annotation.SuppressLint 5 | import android.graphics.Color 6 | import android.os.Bundle 7 | import android.view.View 8 | import android.widget.Button 9 | import android.widget.ImageView 10 | import androidx.appcompat.app.AppCompatActivity 11 | import androidx.core.view.updateLayoutParams 12 | import com.ortiz.touchview.FixedPixel 13 | import com.ortiz.touchview.TouchImageView 14 | import info.touchimage.demo.databinding.ActivityChangeSizeBinding 15 | import kotlin.math.max 16 | import kotlin.math.min 17 | import kotlin.math.pow 18 | 19 | /** 20 | * An example Activity for how to handle a TouchImageView that might be resized. 21 | * 22 | * If you want your image to look like it's being cropped or sliding when you resize it, instead of 23 | * changing its zoom level, you probably want ScaleType.CENTER. Here's an example of how to use it: 24 | * 25 | * imageChangeSize.setScaleType(CENTER); 26 | * imageChangeSize.setMinZoom(TouchImageView.AUTOMATIC_MIN_ZOOM); 27 | * imageChangeSize.setMaxZoomRatio(3.0f); 28 | * float widthRatio = (float) imageChangeSize.getMeasuredWidth() / imageChangeSize.getDrawable().getIntrinsicWidth(); 29 | * float heightRatio = (float) imageChangeSize.getMeasuredHeight() / imageChangeSize.getDrawable().getIntrinsicHeight(); 30 | * imageChangeSize.setZoom(Math.max(widthRatio, heightRatio)); // For an initial view that looks like CENTER_CROP 31 | * imageChangeSize.setZoom(Math.min(widthRatio, heightRatio)); // For an initial view that looks like FIT_CENTER 32 | * 33 | * That code is run when the button displays "CENTER (with X zoom)". 34 | * 35 | * You can use other ScaleTypes, but for all of them, the size of the image depends somehow on the 36 | * size of the TouchImageView, just like it does in ImageView. You can thus expect your image to 37 | * change magnification as its View changes sizes. 38 | */ 39 | class ChangeSizeExampleActivity : AppCompatActivity() { 40 | 41 | private lateinit var binding: ActivityChangeSizeBinding 42 | 43 | private var xSizeAnimator = ValueAnimator() 44 | private var ySizeAnimator = ValueAnimator() 45 | private var xSizeAdjustment = 0 46 | private var ySizeAdjustment = 0 47 | private var scaleTypeIndex = 0 48 | private var imageIndex = 0 49 | 50 | private lateinit var resizeAdjuster: SizeBehaviorAdjuster 51 | private lateinit var rotateAdjuster: SizeBehaviorAdjuster 52 | 53 | override fun onCreate(savedInstanceState: Bundle?) { 54 | super.onCreate(savedInstanceState) 55 | 56 | // https://developer.android.com/topic/libraries/view-binding 57 | binding = ActivityChangeSizeBinding.inflate(layoutInflater) 58 | val view = binding.root 59 | setContentView(view) 60 | 61 | binding.imageChangeSize.setBackgroundColor(Color.LTGRAY) 62 | binding.imageChangeSize.minZoom = TouchImageView.AUTOMATIC_MIN_ZOOM 63 | binding.imageChangeSize.setMaxZoomRatio(6.0f) 64 | 65 | binding.left.setOnClickListener(SizeAdjuster(-1, 0)) 66 | binding.right.setOnClickListener(SizeAdjuster(1, 0)) 67 | binding.up.setOnClickListener(SizeAdjuster(0, -1)) 68 | binding.down.setOnClickListener(SizeAdjuster(0, 1)) 69 | 70 | resizeAdjuster = SizeBehaviorAdjuster(false, "resize: ") 71 | rotateAdjuster = SizeBehaviorAdjuster(true, "rotate: ") 72 | binding.resize.setOnClickListener(resizeAdjuster) 73 | binding.rotate.setOnClickListener(rotateAdjuster) 74 | 75 | binding.switchScaletypeButton.setOnClickListener { 76 | scaleTypeIndex = (scaleTypeIndex + 1) % scaleTypes.size 77 | processScaleType(scaleTypes[scaleTypeIndex], true) 78 | } 79 | 80 | findViewById(R.id.switch_image_button).setOnClickListener { 81 | imageIndex = (imageIndex + 1) % images.size 82 | binding.imageChangeSize.setImageResource(images[imageIndex]) 83 | } 84 | 85 | savedInstanceState?.let { savedState -> 86 | scaleTypeIndex = savedState.getInt("scaleTypeIndex") 87 | resizeAdjuster.setIndex(findViewById(R.id.resize) as Button, savedState.getInt("resizeAdjusterIndex")) 88 | rotateAdjuster.setIndex(findViewById(R.id.rotate) as Button, savedState.getInt("rotateAdjusterIndex")) 89 | imageIndex = savedState.getInt("imageIndex") 90 | binding.imageChangeSize.setImageResource(images[imageIndex]) 91 | } 92 | 93 | binding.imageChangeSize.post { processScaleType(scaleTypes[scaleTypeIndex], false) } 94 | } 95 | 96 | override fun onSaveInstanceState(outState: Bundle) { 97 | super.onSaveInstanceState(outState) 98 | with(outState) { 99 | putInt("scaleTypeIndex", scaleTypeIndex) 100 | putInt("resizeAdjusterIndex", resizeAdjuster.index) 101 | putInt("rotateAdjusterIndex", rotateAdjuster.index) 102 | putInt("imageIndex", imageIndex) 103 | } 104 | } 105 | 106 | @SuppressLint("SetTextI18n") 107 | private fun processScaleType(scaleType: ImageView.ScaleType, resetZoom: Boolean) { 108 | when (scaleType) { 109 | ImageView.ScaleType.FIT_END -> { 110 | binding.switchScaletypeButton.text = ImageView.ScaleType.CENTER.name + " (with " + ImageView.ScaleType.CENTER_CROP.name + " zoom)" 111 | binding.imageChangeSize.scaleType = ImageView.ScaleType.CENTER 112 | val widthRatio = binding.imageChangeSize.measuredWidth.toFloat() / binding.imageChangeSize.drawable.intrinsicWidth 113 | val heightRatio = binding.imageChangeSize.measuredHeight.toFloat() / binding.imageChangeSize.drawable.intrinsicHeight 114 | if (resetZoom) { 115 | binding.imageChangeSize.setZoom(max(widthRatio, heightRatio)) 116 | } 117 | } 118 | ImageView.ScaleType.FIT_START -> { 119 | binding.switchScaletypeButton.text = ImageView.ScaleType.CENTER.name + " (with " + ImageView.ScaleType.FIT_CENTER.name + " zoom)" 120 | binding.imageChangeSize.scaleType = ImageView.ScaleType.CENTER 121 | val widthRatio = binding.imageChangeSize.measuredWidth.toFloat() / binding.imageChangeSize.drawable.intrinsicWidth 122 | val heightRatio = binding.imageChangeSize.measuredHeight.toFloat() / binding.imageChangeSize.drawable.intrinsicHeight 123 | if (resetZoom) { 124 | binding.imageChangeSize.setZoom(min(widthRatio, heightRatio)) 125 | } 126 | } 127 | else -> { 128 | binding.switchScaletypeButton.text = scaleType.name 129 | binding.imageChangeSize.scaleType = scaleType 130 | if (resetZoom) { 131 | binding.imageChangeSize.resetZoom() 132 | } 133 | } 134 | } 135 | } 136 | 137 | private fun adjustImageSize() { 138 | val width = binding.imageContainer.measuredWidth * 1.1.pow(xSizeAdjustment.toDouble()) 139 | val height = binding.imageContainer.measuredHeight * 1.1.pow(ySizeAdjustment.toDouble()) 140 | xSizeAnimator.cancel() 141 | ySizeAnimator.cancel() 142 | xSizeAnimator = ValueAnimator.ofInt(binding.imageChangeSize.width, width.toInt()) 143 | ySizeAnimator = ValueAnimator.ofInt(binding.imageChangeSize.height, height.toInt()) 144 | xSizeAnimator.addUpdateListener { animation -> 145 | binding.imageChangeSize.updateLayoutParams { 146 | this.width = animation.animatedValue as Int 147 | } 148 | } 149 | ySizeAnimator.addUpdateListener { animation -> 150 | binding.imageChangeSize.updateLayoutParams { 151 | this.height = animation.animatedValue as Int 152 | } 153 | } 154 | xSizeAnimator.duration = 200 155 | ySizeAnimator.duration = 200 156 | xSizeAnimator.start() 157 | ySizeAnimator.start() 158 | } 159 | 160 | private inner class SizeAdjuster constructor(var dx: Int, var dy: Int) : View.OnClickListener { 161 | 162 | override fun onClick(v: View) { 163 | val newXScale = min(0, xSizeAdjustment + dx) 164 | val newYScale = min(0, ySizeAdjustment + dy) 165 | if (newXScale == xSizeAdjustment && newYScale == ySizeAdjustment) { 166 | return 167 | } 168 | xSizeAdjustment = newXScale 169 | ySizeAdjustment = newYScale 170 | adjustImageSize() 171 | } 172 | } 173 | 174 | private inner class SizeBehaviorAdjuster(private val forOrientationChanges: Boolean, private val buttonPrefix: String) : View.OnClickListener { 175 | private val values = FixedPixel.values() 176 | var index = 0 177 | private set 178 | 179 | override fun onClick(v: View) { 180 | setIndex(v as Button, (index + 1) % values.size) 181 | } 182 | 183 | @SuppressLint("SetTextI18n") 184 | fun setIndex(b: Button, index: Int) { 185 | this.index = index 186 | if (forOrientationChanges) { 187 | binding.imageChangeSize.orientationChangeFixedPixel = values[index] 188 | } else { 189 | binding.imageChangeSize.viewSizeChangeFixedPixel = values[index] 190 | } 191 | b.text = buttonPrefix + values[index].name 192 | } 193 | } 194 | 195 | companion object { 196 | 197 | // 198 | // Two of the ScaleTypes are stand-ins for CENTER with different initial zoom levels. This is 199 | // special-cased in processScaleType. 200 | // 201 | private val scaleTypes = arrayOf(ImageView.ScaleType.CENTER, ImageView.ScaleType.CENTER_CROP, ImageView.ScaleType.FIT_START, // stand-in for CENTER with initial zoom that looks like FIT_CENTER 202 | ImageView.ScaleType.FIT_END, // stand-in for CENTER with initial zoom that looks like CENTER_CROP 203 | ImageView.ScaleType.CENTER_INSIDE, ImageView.ScaleType.FIT_XY, ImageView.ScaleType.FIT_CENTER) 204 | 205 | private val images = intArrayOf(R.drawable.nature_1, R.drawable.nature_2, R.drawable.nature_6, R.drawable.nature_7, R.drawable.nature_8) 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/GlideExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.drawable.Drawable 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.view.View 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.bumptech.glide.Glide 10 | import com.bumptech.glide.request.target.CustomTarget 11 | import com.bumptech.glide.request.transition.Transition 12 | import info.touchimage.demo.databinding.ActivityGlideBinding 13 | 14 | 15 | class GlideExampleActivity : AppCompatActivity() { 16 | 17 | private lateinit var binding: ActivityGlideBinding 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | // https://developer.android.com/topic/libraries/view-binding 22 | binding = ActivityGlideBinding.inflate(layoutInflater) 23 | val view = binding.root 24 | setContentView(view) 25 | 26 | val start = System.currentTimeMillis() 27 | 28 | Glide.with(this) 29 | .load(GLIDE_IMAGE_URL) 30 | .into(object : CustomTarget() { 31 | @SuppressLint("SetTextI18n") 32 | override fun onResourceReady(resource: Drawable, transition: Transition?) { 33 | binding.imageGlide.setImageDrawable(resource) 34 | binding.textLoaded.visibility = View.VISIBLE 35 | val loadTime = System.currentTimeMillis() - start 36 | binding.textLoaded.text = getString(R.string.loaded) + " within ms" 37 | Log.d("GlideExampleActivity", loadTime.toString() + "ms") 38 | } 39 | 40 | override fun onLoadCleared(placeholder: Drawable?) = Unit 41 | 42 | }) 43 | } 44 | 45 | companion object { 46 | const val GLIDE_IMAGE_URL = "https://raw.githubusercontent.com/bumptech/glide/master/static/glide_logo.png" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.appcompat.app.AppCompatActivity 6 | import info.touchimage.demo.databinding.ActivityMainBinding 7 | 8 | 9 | class MainActivity : AppCompatActivity() { 10 | 11 | private lateinit var binding: ActivityMainBinding 12 | 13 | public override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | 16 | // https://developer.android.com/topic/libraries/view-binding 17 | binding = ActivityMainBinding.inflate(layoutInflater) 18 | val view = binding.root 19 | setContentView(view) 20 | 21 | binding.singleTouchimageviewButton.setOnClickListener { startActivity(Intent(this@MainActivity, SingleTouchImageViewActivity::class.java)) } 22 | binding.viewpagerExampleButton.setOnClickListener { startActivity(Intent(this@MainActivity, ViewPagerExampleActivity::class.java)) } 23 | binding.viewpager2ExampleButton.setOnClickListener { startActivity(Intent(this@MainActivity, ViewPager2ExampleActivity::class.java)) } 24 | binding.mirrorTouchimageviewButton.setOnClickListener { startActivity(Intent(this@MainActivity, MirroringExampleActivity::class.java)) } 25 | binding.switchImageButton.setOnClickListener { startActivity(Intent(this@MainActivity, SwitchImageExampleActivity::class.java)) } 26 | binding.switchScaletypeButton.setOnClickListener { startActivity(Intent(this@MainActivity, SwitchScaleTypeExampleActivity::class.java)) } 27 | binding.resizeButton.setOnClickListener { startActivity(Intent(this@MainActivity, ChangeSizeExampleActivity::class.java)) } 28 | binding.recyclerButton.setOnClickListener { startActivity(Intent(this@MainActivity, RecyclerExampleActivity::class.java)) } 29 | binding.animateButton.setOnClickListener { startActivity(Intent(this@MainActivity, AnimateZoomActivity::class.java)) } 30 | binding.glideButton.setOnClickListener { startActivity(Intent(this@MainActivity, GlideExampleActivity::class.java)) } 31 | binding.shapedImageButton.setOnClickListener { startActivity(Intent(this@MainActivity, ShapedExampleActivity::class.java)) } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/MirroringExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import com.ortiz.touchview.OnTouchImageViewListener 6 | import info.touchimage.demo.databinding.ActivityMirroringExampleBinding 7 | 8 | class MirroringExampleActivity : AppCompatActivity() { 9 | 10 | private lateinit var binding: ActivityMirroringExampleBinding 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | // https://developer.android.com/topic/libraries/view-binding 16 | binding = ActivityMirroringExampleBinding.inflate(layoutInflater) 17 | val view = binding.root 18 | setContentView(view) 19 | 20 | // Each image has an OnTouchImageViewListener which uses its own TouchImageView 21 | // to set the other TIV with the same zoom variables. 22 | 23 | binding.topImage.setOnTouchImageViewListener(object : OnTouchImageViewListener { 24 | override fun onMove() { 25 | binding.bottomImage.setZoom(binding.topImage) 26 | } 27 | }) 28 | binding.bottomImage.setOnTouchImageViewListener(object : OnTouchImageViewListener { 29 | override fun onMove() { 30 | binding.topImage.setZoom(binding.bottomImage) 31 | } 32 | }) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/RecyclerExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.recyclerview.widget.PagerSnapHelper 6 | import info.touchimage.demo.custom.AdapterImages 7 | import info.touchimage.demo.databinding.ActivityRecyclerviewBinding 8 | 9 | 10 | class RecyclerExampleActivity : AppCompatActivity() { 11 | 12 | private lateinit var binding: ActivityRecyclerviewBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | // https://developer.android.com/topic/libraries/view-binding 18 | binding = ActivityRecyclerviewBinding.inflate(layoutInflater) 19 | val view = binding.root 20 | setContentView(view) 21 | 22 | with(binding.recycler) { 23 | adapter = AdapterImages(images) 24 | PagerSnapHelper().attachToRecyclerView(this) 25 | } 26 | } 27 | 28 | companion object { 29 | 30 | private val images = intArrayOf(R.drawable.nature_1, R.drawable.nature_2, R.drawable.nature_3, R.drawable.nature_4, R.drawable.nature_5, R.drawable.nature_6, R.drawable.nature_7, R.drawable.nature_8) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/ShapedExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.graphics.Outline 4 | import android.os.Bundle 5 | import android.view.View 6 | import android.view.ViewOutlineProvider 7 | import androidx.appcompat.app.AppCompatActivity 8 | import info.touchimage.demo.databinding.ActivityShapedExampleBinding 9 | 10 | internal class ShapedExampleActivity : AppCompatActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | ActivityShapedExampleBinding.inflate(layoutInflater).apply { 15 | val view = root 16 | 17 | val outlineProvider = object : ViewOutlineProvider() { 18 | override fun getOutline(view: View, outline: Outline) { 19 | outline.setRoundRect( 20 | 0, 21 | 0, 22 | view.width, 23 | view.height, 24 | view.resources.getDimension(R.dimen.grid_2) 25 | ) 26 | } 27 | } 28 | imageView.outlineProvider = outlineProvider 29 | imageView.clipToOutline = true 30 | 31 | setContentView(view) 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/SingleTouchImageViewActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.annotation.SuppressLint 4 | import android.graphics.PointF 5 | import android.os.Bundle 6 | import android.view.MotionEvent 7 | import android.view.View 8 | import androidx.appcompat.app.AppCompatActivity 9 | import com.ortiz.touchview.OnTouchCoordinatesListener 10 | import com.ortiz.touchview.OnTouchImageViewListener 11 | import info.touchimage.demo.databinding.ActivitySingleTouchimageviewBinding 12 | import java.text.DecimalFormat 13 | 14 | 15 | class SingleTouchImageViewActivity : AppCompatActivity() { 16 | 17 | private lateinit var binding: ActivitySingleTouchimageviewBinding 18 | 19 | @SuppressLint("SetTextI18n") 20 | override fun onCreate(savedInstanceState: Bundle?) { 21 | super.onCreate(savedInstanceState) 22 | 23 | // https://developer.android.com/topic/libraries/view-binding 24 | binding = ActivitySingleTouchimageviewBinding.inflate(layoutInflater) 25 | val view = binding.root 26 | setContentView(view) 27 | 28 | // DecimalFormat rounds to 2 decimal places. 29 | val df = DecimalFormat("#.##") 30 | 31 | // Set the OnTouchImageViewListener which updates edit texts with zoom and scroll diagnostics. 32 | binding.imageSingle.setOnTouchImageViewListener(object : OnTouchImageViewListener { 33 | override fun onMove() { 34 | val point = binding.imageSingle.scrollPosition 35 | val rect = binding.imageSingle.zoomedRect 36 | val currentZoom = binding.imageSingle.currentZoom 37 | val isZoomed = binding.imageSingle.isZoomed 38 | binding.scrollPosition.text = "x: " + df.format(point.x.toDouble()) + " y: " + df.format(point.y.toDouble()) 39 | binding.zoomedRect.text = ("left: " + df.format(rect.left.toDouble()) + " top: " + df.format(rect.top.toDouble()) 40 | + "\nright: " + df.format(rect.right.toDouble()) + " bottom: " + df.format(rect.bottom.toDouble())) 41 | binding.currentZoom.text = "getCurrentZoom(): $currentZoom isZoomed(): $isZoomed" 42 | } 43 | } 44 | ) 45 | 46 | binding.imageSingle.setOnTouchCoordinatesListener(object: OnTouchCoordinatesListener { 47 | override fun onTouchCoordinate(view: View, event: MotionEvent, bitmapPoint: PointF) { 48 | binding.touchCoordinates.text = "touch coordinates x=${bitmapPoint.x} y=${bitmapPoint.y}" 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/SwitchImageExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import info.touchimage.demo.databinding.ActivitySwitchImageExampleBinding 6 | 7 | class SwitchImageExampleActivity : AppCompatActivity() { 8 | 9 | private var index = 0 10 | private lateinit var binding: ActivitySwitchImageExampleBinding 11 | 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | // https://developer.android.com/topic/libraries/view-binding 16 | binding = ActivitySwitchImageExampleBinding.inflate(layoutInflater) 17 | val view = binding.root 18 | setContentView(view) 19 | 20 | // Set first image 21 | savedInstanceState?.getInt("index")?.let(this::index::set) 22 | binding.imageSwitch.setImageResource(images[index]) 23 | 24 | // Set next image with each button click 25 | binding.imageSwitch.setOnClickListener { 26 | index = (index + 1) % images.size 27 | binding.imageSwitch.setImageResource(images[index]) 28 | } 29 | } 30 | 31 | override fun onSaveInstanceState(outState: Bundle) { 32 | super.onSaveInstanceState(outState) 33 | outState.putInt("index", index) 34 | } 35 | 36 | companion object { 37 | private val images = intArrayOf(R.drawable.nature_1, R.drawable.nature_4, R.drawable.nature_6, R.drawable.nature_7, R.drawable.nature_8) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/SwitchScaleTypeExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import android.widget.ImageView.ScaleType 5 | import android.widget.Toast 6 | import androidx.appcompat.app.AppCompatActivity 7 | import info.touchimage.demo.databinding.ActivitySwitchScaletypeExampleBinding 8 | 9 | class SwitchScaleTypeExampleActivity : AppCompatActivity() { 10 | 11 | private var index = 0 12 | private lateinit var binding: ActivitySwitchScaletypeExampleBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | // https://developer.android.com/topic/libraries/view-binding 18 | binding = ActivitySwitchScaletypeExampleBinding.inflate(layoutInflater) 19 | val view = binding.root 20 | setContentView(view) 21 | 22 | 23 | // Set next scaleType with each button click 24 | binding.imageScale.setOnClickListener { 25 | index = ++index % scaleTypes.size 26 | val currScaleType = scaleTypes[index] 27 | binding.imageScale.scaleType = currScaleType 28 | Toast.makeText(this, "ScaleType: $currScaleType", Toast.LENGTH_SHORT).show() 29 | } 30 | } 31 | 32 | companion object { 33 | private val scaleTypes = arrayOf(ScaleType.CENTER, ScaleType.CENTER_CROP, ScaleType.CENTER_INSIDE, ScaleType.FIT_XY, ScaleType.FIT_CENTER) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/ViewPager2ExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import info.touchimage.demo.custom.AdapterImages 6 | import info.touchimage.demo.databinding.ActivityViewpager2ExampleBinding 7 | 8 | 9 | class ViewPager2ExampleActivity : AppCompatActivity() { 10 | 11 | private lateinit var binding: ActivityViewpager2ExampleBinding 12 | 13 | public override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | 16 | // https://developer.android.com/topic/libraries/view-binding 17 | binding = ActivityViewpager2ExampleBinding.inflate(layoutInflater) 18 | val view = binding.root 19 | setContentView(view) 20 | 21 | binding.viewPager2.adapter = AdapterImages(images) 22 | } 23 | 24 | companion object { 25 | 26 | private val images = intArrayOf(R.drawable.nature_1, R.drawable.nature_2, R.drawable.nature_3, R.drawable.nature_4, R.drawable.nature_5, R.drawable.nature_6, R.drawable.nature_7, R.drawable.nature_8) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/ViewPagerExampleActivity.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo 2 | 3 | import android.os.Bundle 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.LinearLayout 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.viewpager.widget.PagerAdapter 9 | import com.ortiz.touchview.TouchImageView 10 | import info.touchimage.demo.databinding.ActivityViewpagerExampleBinding 11 | 12 | 13 | class ViewPagerExampleActivity : AppCompatActivity() { 14 | 15 | private lateinit var binding: ActivityViewpagerExampleBinding 16 | 17 | public override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | // https://developer.android.com/topic/libraries/view-binding 21 | binding = ActivityViewpagerExampleBinding.inflate(layoutInflater) 22 | val view = binding.root 23 | setContentView(view) 24 | 25 | binding.viewPager.adapter = TouchImageAdapter() 26 | } 27 | 28 | private class TouchImageAdapter : PagerAdapter() { 29 | 30 | override fun getCount(): Int { 31 | return images.size 32 | } 33 | 34 | override fun instantiateItem(container: ViewGroup, position: Int): View { 35 | return TouchImageView(container.context).apply { 36 | setImageResource(images[position]) 37 | container.addView(this, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT) 38 | } 39 | } 40 | 41 | override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) { 42 | container.removeView(`object` as View) 43 | } 44 | 45 | override fun isViewFromObject(view: View, `object`: Any): Boolean { 46 | return view === `object` 47 | } 48 | 49 | companion object { 50 | 51 | private val images = intArrayOf(R.drawable.nature_1, R.drawable.nature_2, R.drawable.nature_3, R.drawable.nature_4, R.drawable.nature_5) 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/custom/AdapterImages.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo.custom 2 | 3 | import android.view.MotionEvent 4 | import android.view.ViewGroup 5 | import android.view.ViewGroup.LayoutParams.MATCH_PARENT 6 | import androidx.recyclerview.widget.RecyclerView 7 | import com.ortiz.touchview.TouchImageView 8 | 9 | class AdapterImages(private val photoList: IntArray) : RecyclerView.Adapter() { 10 | 11 | override fun getItemCount(): Int { 12 | return photoList.size 13 | } 14 | 15 | class ViewHolder(view: TouchImageView) : RecyclerView.ViewHolder(view) { 16 | val imagePlace = view 17 | } 18 | 19 | override fun getItemId(i: Int): Long { 20 | return i.toLong() 21 | } 22 | 23 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 24 | return ViewHolder(TouchImageView(parent.context).apply { 25 | layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) 26 | 27 | setOnTouchListener { view, event -> 28 | var result = true 29 | //can scroll horizontally checks if there's still a part of the image 30 | //that can be scrolled until you reach the edge 31 | if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && canScrollHorizontally(-1)) { 32 | //multi-touch event 33 | result = when (event.action) { 34 | MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { 35 | // Disallow RecyclerView to intercept touch events. 36 | parent.requestDisallowInterceptTouchEvent(true) 37 | // Disable touch on view 38 | false 39 | } 40 | MotionEvent.ACTION_UP -> { 41 | // Allow RecyclerView to intercept touch events. 42 | parent.requestDisallowInterceptTouchEvent(false) 43 | true 44 | } 45 | else -> true 46 | } 47 | } 48 | result 49 | } 50 | }) 51 | } 52 | 53 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 54 | holder.imagePlace.setImageResource(photoList[position]) 55 | } 56 | 57 | override fun getItemViewType(i: Int): Int { 58 | return 0 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/info/touchimage/demo/custom/ExtendedViewPager.kt: -------------------------------------------------------------------------------- 1 | package info.touchimage.demo.custom 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.View 6 | import androidx.viewpager.widget.ViewPager 7 | import com.ortiz.touchview.TouchImageView 8 | 9 | 10 | class ExtendedViewPager : ViewPager { 11 | 12 | constructor(context: Context) : super(context) 13 | 14 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) 15 | 16 | override fun canScroll(view: View, checkV: Boolean, dx: Int, x: Int, y: Int): Boolean { 17 | return if (view is TouchImageView) { 18 | view.canScrollHorizontally(-dx) 19 | } else { 20 | super.canScroll(view, checkV, dx, x, y) 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_1.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_2.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_3.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_4.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_5.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_6.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_7.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/nature_8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/nature_8.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/numbers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/app/src/main/res/drawable/numbers.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_change_size.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 23 | 24 | 25 | 33 | 34 | 38 | 39 |