├── .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 |
4 |
5 |
8 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | xmlns:android
48 |
49 | ^$
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | xmlns:.*
59 |
60 | ^$
61 |
62 |
63 | BY_NAME
64 |
65 |
66 |
67 |
68 |
69 |
70 | .*:id
71 |
72 | http://schemas.android.com/apk/res/android
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | .*:name
82 |
83 | http://schemas.android.com/apk/res/android
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | name
93 |
94 | ^$
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | style
104 |
105 | ^$
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | .*
115 |
116 | ^$
117 |
118 |
119 | BY_NAME
120 |
121 |
122 |
123 |
124 |
125 |
126 | .*
127 |
128 | http://schemas.android.com/apk/res/android
129 |
130 |
131 | ANDROID_ATTRIBUTE_ORDER
132 |
133 |
134 |
135 |
136 |
137 |
138 | .*
139 |
140 | .*
141 |
142 |
143 | BY_NAME
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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/#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 |
45 |
46 |
52 |
53 |
59 |
60 |
66 |
67 |
68 |
72 |
73 |
78 |
79 |
84 |
85 |
86 |
90 |
91 |
97 |
98 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_glide.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
10 |
11 |
16 |
17 |
22 |
23 |
28 |
29 |
34 |
35 |
40 |
41 |
46 |
47 |
52 |
53 |
58 |
59 |
64 |
65 |
70 |
71 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_mirroring_example.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
13 |
14 |
18 |
19 |
25 |
26 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_recyclerview.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_shaped_example.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_single_touchimageview.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
12 |
13 |
21 |
22 |
28 |
29 |
34 |
35 |
36 |
37 |
44 |
45 |
51 |
52 |
56 |
57 |
58 |
59 |
66 |
67 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_switch_image_example.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
11 |
12 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_switch_scaletype_example.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
12 |
13 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_viewpager2_example.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
9 |
10 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_viewpager_example.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
10 |
11 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/brown_light
4 | @color/brown
5 | #FF4081
6 | #cc7b35
7 | #945824
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello World, TouchImageViewActivity!
4 | TouchImageView
5 |
6 |
7 | Single TouchImageView
8 | Mirroring Example
9 | ViewPager Example
10 | ViewPager2 Example
11 | Switch Image Example
12 | Switch ScaleType Example
13 | Resize Example
14 | RecyclerView and zoom
15 | Animate zoom
16 | Glide
17 | Rounded corners
18 |
19 | getScrollPosition():
20 | getZoomedRect():
21 | getCurrentZoom():
22 | All touch gestures applied to one TouchImageView will be mirrored on the other.
23 | Swipe left or right to test ViewPager.
24 | Click TouchImageView to cycle through image resources. Note that the zoom state is maintained.
25 | Click TouchImageView to cycle through supported ScaleTypes.
26 | loaded
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | import org.gradle.internal.jvm.Jvm
2 |
3 | buildscript {
4 | ext.kotlin_version = '2.1.20'
5 | repositories {
6 | google()
7 | mavenCentral()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:8.10.0'
12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
13 | }
14 | }
15 |
16 | println "Gradle uses Java ${Jvm.current()}"
17 |
18 | ext {
19 | defaultTargetSdkVersion = 35
20 | defaultCompileSdkVersion = 35
21 | }
22 |
23 | allprojects {
24 | repositories {
25 | google()
26 | mavenCentral()
27 | maven { url 'https://jitpack.io' }
28 | }
29 | }
30 |
31 | @SuppressWarnings('unused')
32 | static def getTag() {
33 | def tagVersion = "$System.env.VERSION"
34 | if (tagVersion == "null") {
35 | // with local un-committed changes a -DIRTY is added
36 | def processChanges = "git diff-index --name-only HEAD --".execute()
37 | def dirty = ""
38 | if (!processChanges.text.toString().trim().isEmpty())
39 | dirty = "-DIRTY"
40 |
41 | def process = "git describe --tags".execute()
42 | tagVersion = process.text.toString().trim() + dirty
43 | } else {
44 | def tagVersionToken = tagVersion.split("/")
45 | if (tagVersionToken.size() > 2)
46 | tagVersion = tagVersionToken[2]
47 | else
48 | tagVersion = tagVersionToken[0]
49 | }
50 | return tagVersion
51 | }
52 |
53 | @SuppressWarnings('unused')
54 | static def getGitCommitCount() {
55 | def process = "git rev-list HEAD --count".execute()
56 | return process.text.toInteger()
57 | }
58 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.enableJetifier=true
2 | android.useAndroidX=true
3 |
4 | android.defaults.buildfeatures.buildconfig=true
5 | android.nonTransitiveRClass=false
6 | android.nonFinalResIds=false
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/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.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/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 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
3 | install:
4 | - ./gradlew :touchview:build :touchview:publishToMavenLocal -x :touchview:test
5 |
--------------------------------------------------------------------------------
/screenShotCompare.sh:
--------------------------------------------------------------------------------
1 | diffFiles=./screenshotDiffs
2 | mkdir $diffFiles
3 | #cp app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/emulator\(AVD\)\ -\ 9/* screenshotsToCompare
4 | set -x
5 | ./git-diff-image/install.sh
6 | GIT_DIFF_IMAGE_OUTPUT_DIR=$diffFiles git diff-image
7 |
8 | ls -la $diffFiles
9 |
10 | # set error when diffs are there
11 | [ "$(ls -A $diffFiles)" ] && exit 1 || exit 0
12 |
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_makeScreenshotOfShapedImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_makeScreenshotOfShapedImage.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_smokeTestSimplyStart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_smokeTestSimplyStart.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testAnimateZoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testAnimateZoom.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testChangeSize.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testChangeSize.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testGlide.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testGlide.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testMirroring.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testMirroring.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testRecycler.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testRecycler.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testSingleTouch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testSingleTouch.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testSwitchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testSwitchImage.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testSwitchScale.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testSwitchScale.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testView2Pager.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testView2Pager.png
--------------------------------------------------------------------------------
/screenshotsToCompare/MainSmokeTest_testViewPager.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/MainSmokeTest_testViewPager.png
--------------------------------------------------------------------------------
/screenshotsToCompare/TouchTest_testSingleTouch-touch1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/TouchTest_testSingleTouch-touch1.png
--------------------------------------------------------------------------------
/screenshotsToCompare/TouchTest_testSingleTouch-touch2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/TouchTest_testSingleTouch-touch2.png
--------------------------------------------------------------------------------
/screenshotsToCompare/ZoomTest_zoom-1-init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/ZoomTest_zoom-1-init.png
--------------------------------------------------------------------------------
/screenshotsToCompare/ZoomTest_zoom-2-reset.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/ZoomTest_zoom-2-reset.png
--------------------------------------------------------------------------------
/screenshotsToCompare/ZoomTest_zoom-3-zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/ZoomTest_zoom-3-zoom.png
--------------------------------------------------------------------------------
/screenshotsToCompare/ZoomTest_zoom-4-end.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikeOrtiz/TouchImageView/ef8d7fa0ae354bb69eb42e0178f7bf8a7521dca4/screenshotsToCompare/ZoomTest_zoom-4-end.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':touchview'
2 |
--------------------------------------------------------------------------------
/touchview/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | defaultConfig {
9 | minSdkVersion 16
10 | aarMetadata {
11 | minCompileSdk = 28
12 | }
13 | testFixtures {
14 | enable = true
15 | }
16 |
17 | compileSdk defaultCompileSdkVersion
18 | targetSdkVersion defaultTargetSdkVersion
19 | buildConfigField "String", 'VERSION', "\"" + versionName + "\""
20 | }
21 |
22 | buildTypes {
23 | release {
24 | minifyEnabled false
25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
26 | }
27 | }
28 | namespace 'com.ortiz.touchview'
29 |
30 | compileOptions {
31 | sourceCompatibility JavaVersion.VERSION_17
32 | targetCompatibility JavaVersion.VERSION_17
33 | }
34 | }
35 |
36 | dependencies {
37 | api 'androidx.appcompat:appcompat:1.7.0'
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
39 | }
40 |
41 | publishing {
42 | publications {
43 | release(MavenPublication) {
44 | afterEvaluate {
45 | from components.release
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/touchview/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/JVillella/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/touchview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/FixedPixel.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | enum class FixedPixel {
4 | CENTER, TOP_LEFT, BOTTOM_RIGHT
5 | }
6 |
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/ImageActionState.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | internal enum class ImageActionState {
4 | NONE, DRAG, ZOOM, FLING, ANIMATE_ZOOM
5 | }
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/OnTouchCoordinatesListener.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | import android.graphics.PointF
4 | import android.view.MotionEvent
5 | import android.view.View
6 |
7 | fun interface OnTouchCoordinatesListener {
8 | fun onTouchCoordinate(view: View, event: MotionEvent, bitmapPoint: PointF)
9 | }
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/OnTouchImageViewListener.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | fun interface OnTouchImageViewListener {
4 | fun onMove()
5 | }
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/OnZoomFinishedListener.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | fun interface OnZoomFinishedListener {
4 | fun onZoomFinished()
5 | }
6 |
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/TouchImageView.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | import android.content.Context
4 | import android.content.res.Configuration
5 | import android.graphics.*
6 | import android.graphics.drawable.Drawable
7 | import android.net.Uri
8 | import android.os.Bundle
9 | import android.os.Parcelable
10 | import android.util.AttributeSet
11 | import android.view.GestureDetector
12 | import android.view.GestureDetector.OnDoubleTapListener
13 | import android.view.GestureDetector.SimpleOnGestureListener
14 | import android.view.MotionEvent
15 | import android.view.ScaleGestureDetector
16 | import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
17 | import android.view.View
18 | import android.view.animation.AccelerateDecelerateInterpolator
19 | import android.view.animation.LinearInterpolator
20 | import android.widget.OverScroller
21 | import androidx.appcompat.widget.AppCompatImageView
22 | import androidx.core.os.BundleCompat
23 | import androidx.core.os.bundleOf
24 | import kotlin.math.abs
25 | import kotlin.math.max
26 | import kotlin.math.min
27 |
28 | @Suppress("unused")
29 | open class TouchImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) :
30 | AppCompatImageView(context, attrs, defStyle) {
31 | /**
32 | * Get the current zoom. This is the zoom relative to the initial
33 | * scale, not the original resource.
34 | *
35 | * @return current zoom multiplier.
36 | */
37 | // Scale of image ranges from minScale to maxScale, where minScale == 1
38 | // when the image is stretched to fit view.
39 | var currentZoom = 0f
40 | private set
41 |
42 | // Matrix applied to image. MSCALE_X and MSCALE_Y should always be equal.
43 | // MTRANS_X and MTRANS_Y are the other values used. prevMatrix is the matrix saved prior to the screen rotating.
44 | private var touchMatrix: Matrix
45 | private var prevMatrix: Matrix
46 | var isZoomEnabled = false
47 | var isSuperZoomEnabled = true
48 | private var isRotateImageToFitScreen = false
49 |
50 | var orientationChangeFixedPixel: FixedPixel? = FixedPixel.CENTER
51 | var viewSizeChangeFixedPixel: FixedPixel? = FixedPixel.CENTER
52 | private var orientationJustChanged = false
53 |
54 | private var imageActionState: ImageActionState? = null
55 | private var userSpecifiedMinScale = 0f
56 | private var minScale = 0f
57 | private var maxScaleIsSetByMultiplier = false
58 | private var maxScaleMultiplier = 0f
59 | private var maxScale = 0f
60 | private var superMinScale = 0f
61 | private var superMaxScale = 0f
62 | private var floatMatrix: FloatArray
63 |
64 | /**
65 | * Set custom zoom multiplier for double tap.
66 | * By default maxScale will be used as value for double tap zoom multiplier.
67 | *
68 | */
69 | var doubleTapScale = 0f
70 | private var fling: Fling? = null
71 | private var orientation = 0
72 | private var touchScaleType: ScaleType? = null
73 | private var imageRenderedAtLeastOnce = false
74 | private var onDrawReady = false
75 | private var delayedZoomVariables: ZoomVariables? = null
76 |
77 | // Size of view and previous view size (ie before rotation)
78 | private var viewWidth = 0
79 | private var viewHeight = 0
80 | private var prevViewWidth = 0
81 | private var prevViewHeight = 0
82 |
83 | // Size of image when it is stretched to fit view. Before and After rotation.
84 | private var matchViewWidth = 0f
85 | private var matchViewHeight = 0f
86 | private var prevMatchViewWidth = 0f
87 | private var prevMatchViewHeight = 0f
88 | private var scaleDetector: ScaleGestureDetector
89 | private var gestureDetector: GestureDetector
90 | private var touchCoordinatesListener: OnTouchCoordinatesListener? = null
91 | private var doubleTapListener: OnDoubleTapListener? = null
92 | private var userTouchListener: OnTouchListener? = null
93 | private var touchImageViewListener: OnTouchImageViewListener? = null
94 |
95 | init {
96 | super.setClickable(true)
97 | orientation = resources.configuration.orientation
98 | scaleDetector = ScaleGestureDetector(context, ScaleListener())
99 | gestureDetector = GestureDetector(context, GestureListener())
100 | touchMatrix = Matrix()
101 | prevMatrix = Matrix()
102 | floatMatrix = FloatArray(9)
103 | currentZoom = 1f
104 | if (touchScaleType == null) {
105 | touchScaleType = ScaleType.FIT_CENTER
106 | }
107 | minScale = 1f
108 | maxScale = 3f
109 | superMinScale = SUPER_MIN_MULTIPLIER * minScale
110 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale
111 | imageMatrix = touchMatrix
112 | scaleType = ScaleType.MATRIX
113 | setState(ImageActionState.NONE)
114 | onDrawReady = false
115 | super.setOnTouchListener(PrivateOnTouchListener())
116 | val attributes = context.theme.obtainStyledAttributes(attrs, R.styleable.TouchImageView, defStyle, 0)
117 | try {
118 | if (!isInEditMode) {
119 | isZoomEnabled = attributes.getBoolean(R.styleable.TouchImageView_zoom_enabled, true)
120 | }
121 | } finally {
122 | // release the TypedArray so that it can be reused.
123 | attributes.recycle()
124 | }
125 | }
126 |
127 | fun setRotateImageToFitScreen(rotateImageToFitScreen: Boolean) {
128 | isRotateImageToFitScreen = rotateImageToFitScreen
129 | }
130 |
131 | override fun setOnTouchListener(onTouchListener: OnTouchListener?) {
132 | userTouchListener = onTouchListener
133 | }
134 |
135 | fun setOnTouchImageViewListener(onTouchImageViewListener: OnTouchImageViewListener) {
136 | touchImageViewListener = onTouchImageViewListener
137 | }
138 |
139 | fun setOnDoubleTapListener(onDoubleTapListener: OnDoubleTapListener) {
140 | doubleTapListener = onDoubleTapListener
141 | }
142 |
143 | fun setOnTouchCoordinatesListener(onTouchCoordinatesListener: OnTouchCoordinatesListener) {
144 | touchCoordinatesListener = onTouchCoordinatesListener
145 | }
146 |
147 | override fun setImageResource(resId: Int) {
148 | imageRenderedAtLeastOnce = false
149 | super.setImageResource(resId)
150 | savePreviousImageValues()
151 | fitImageToView()
152 | }
153 |
154 | override fun setImageBitmap(bm: Bitmap?) {
155 | imageRenderedAtLeastOnce = false
156 | super.setImageBitmap(bm)
157 | savePreviousImageValues()
158 | fitImageToView()
159 | }
160 |
161 | override fun setImageDrawable(drawable: Drawable?) {
162 | imageRenderedAtLeastOnce = false
163 | super.setImageDrawable(drawable)
164 | savePreviousImageValues()
165 | fitImageToView()
166 | }
167 |
168 | override fun setImageURI(uri: Uri?) {
169 | imageRenderedAtLeastOnce = false
170 | super.setImageURI(uri)
171 | savePreviousImageValues()
172 | fitImageToView()
173 | }
174 |
175 | override fun setScaleType(type: ScaleType) {
176 | if (type == ScaleType.MATRIX) {
177 | super.setScaleType(ScaleType.MATRIX)
178 | } else {
179 | touchScaleType = type
180 | if (onDrawReady) {
181 | // If the image is already rendered, scaleType has been called programmatically
182 | // and the TouchImageView should be updated with the new scaleType.
183 | setZoom(this)
184 | }
185 | }
186 | }
187 |
188 | override fun getScaleType() = touchScaleType!!
189 |
190 | /**
191 | * Returns false if image is in initial, unzoomed state. False, otherwise.
192 | *
193 | * @return true if image is zoomed
194 | */
195 | val isZoomed: Boolean
196 | get() = currentZoom != 1f
197 |
198 | /**
199 | * Return a Rect representing the zoomed image.
200 | *
201 | * @return rect representing zoomed image
202 | */
203 | val zoomedRect: RectF
204 | get() {
205 | if (touchScaleType == ScaleType.FIT_XY) {
206 | throw UnsupportedOperationException("getZoomedRect() not supported with FIT_XY")
207 | }
208 | val topLeft = transformCoordTouchToBitmap(0f, 0f, true)
209 | val bottomRight = transformCoordTouchToBitmap(viewWidth.toFloat(), viewHeight.toFloat(), true)
210 | val w = getDrawableWidth(drawable).toFloat()
211 | val h = getDrawableHeight(drawable).toFloat()
212 | return RectF(topLeft.x / w, topLeft.y / h, bottomRight.x / w, bottomRight.y / h)
213 | }
214 |
215 | /**
216 | * Save the current matrix and view dimensions
217 | * in the prevMatrix and prevView variables.
218 | */
219 | fun savePreviousImageValues() {
220 | if (viewHeight != 0 && viewWidth != 0) {
221 | touchMatrix.getValues(floatMatrix)
222 | prevMatrix.setValues(floatMatrix)
223 | prevMatchViewHeight = matchViewHeight
224 | prevMatchViewWidth = matchViewWidth
225 | prevViewHeight = viewHeight
226 | prevViewWidth = viewWidth
227 | }
228 | }
229 |
230 | public override fun onSaveInstanceState(): Parcelable {
231 | touchMatrix.getValues(floatMatrix)
232 |
233 | return bundleOf(
234 | "parent" to super.onSaveInstanceState(),
235 | "orientation" to orientation,
236 | "saveScale" to currentZoom,
237 | "matchViewHeight" to matchViewHeight,
238 | "matchViewWidth" to matchViewWidth,
239 | "viewWidth" to viewWidth,
240 | "viewHeight" to viewHeight,
241 | "matrix" to floatMatrix,
242 | "imageRendered" to imageRenderedAtLeastOnce,
243 | "viewSizeChangeFixedPixel" to viewSizeChangeFixedPixel,
244 | "orientationChangeFixedPixel" to orientationChangeFixedPixel
245 | )
246 | }
247 |
248 | public override fun onRestoreInstanceState(state: Parcelable) {
249 | if (state is Bundle) {
250 | super.onRestoreInstanceState(BundleCompat.getParcelable(state, "parent", Parcelable::class.java))
251 | currentZoom = state.getFloat("saveScale")
252 | floatMatrix = state.getFloatArray("matrix")!!
253 | prevMatrix.setValues(floatMatrix)
254 | prevMatchViewHeight = state.getFloat("matchViewHeight")
255 | prevMatchViewWidth = state.getFloat("matchViewWidth")
256 | prevViewHeight = state.getInt("viewHeight")
257 | prevViewWidth = state.getInt("viewWidth")
258 | imageRenderedAtLeastOnce = state.getBoolean("imageRendered")
259 | viewSizeChangeFixedPixel = BundleCompat.getSerializable(state, "viewSizeChangeFixedPixel", FixedPixel::class.java)
260 | orientationChangeFixedPixel = BundleCompat.getSerializable(state, "orientationChangeFixedPixel", FixedPixel::class.java)
261 | val oldOrientation = state.getInt("orientation")
262 | if (orientation != oldOrientation) {
263 | orientationJustChanged = true
264 | }
265 | return
266 | }
267 | super.onRestoreInstanceState(state)
268 | }
269 |
270 | override fun onDraw(canvas: Canvas) {
271 | onDrawReady = true
272 | imageRenderedAtLeastOnce = true
273 | if (delayedZoomVariables != null) {
274 | setZoom(delayedZoomVariables!!.scale, delayedZoomVariables!!.focusX, delayedZoomVariables!!.focusY, delayedZoomVariables!!.scaleType)
275 | delayedZoomVariables = null
276 | }
277 | super.onDraw(canvas)
278 | }
279 |
280 | public override fun onConfigurationChanged(newConfig: Configuration) {
281 | super.onConfigurationChanged(newConfig)
282 | val newOrientation = resources.configuration.orientation
283 | if (newOrientation != orientation) {
284 | orientationJustChanged = true
285 | orientation = newOrientation
286 | }
287 | savePreviousImageValues()
288 | }
289 |
290 | /**
291 | * Set the max zoom multiplier to a constant. Default value: 3.
292 | * @return max zoom multiplier.
293 | */
294 | var maxZoom: Float
295 | get() = maxScale
296 | set(max) {
297 | maxScale = max
298 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale
299 | maxScaleIsSetByMultiplier = false
300 | }
301 |
302 | /**
303 | * Set the max zoom multiplier as a multiple of minZoom, whatever minZoom may change to. By
304 | * default, this is not done, and maxZoom has a fixed value of 3.
305 | *
306 | * @param max max zoom multiplier, as a multiple of minZoom
307 | */
308 | fun setMaxZoomRatio(max: Float) {
309 | maxScaleMultiplier = max
310 | maxScale = minScale * maxScaleMultiplier
311 | superMaxScale = SUPER_MAX_MULTIPLIER * maxScale
312 | maxScaleIsSetByMultiplier = true
313 | }
314 |
315 | /**
316 | * Set the min zoom multiplier. Default value: 1.
317 | * @return min zoom multiplier.
318 | */
319 | var minZoom: Float
320 | get() = minScale
321 | set(min) {
322 | userSpecifiedMinScale = min
323 | if (min == AUTOMATIC_MIN_ZOOM) {
324 | if (touchScaleType == ScaleType.CENTER || touchScaleType == ScaleType.CENTER_CROP) {
325 | val drawable = drawable
326 | val drawableWidth = getDrawableWidth(drawable)
327 | val drawableHeight = getDrawableHeight(drawable)
328 | if (drawable != null && drawableWidth > 0 && drawableHeight > 0) {
329 | val widthRatio = viewWidth.toFloat() / drawableWidth
330 | val heightRatio = viewHeight.toFloat() / drawableHeight
331 | minScale = if (touchScaleType == ScaleType.CENTER) {
332 | min(widthRatio, heightRatio)
333 | } else { // CENTER_CROP
334 | min(widthRatio, heightRatio) / max(widthRatio, heightRatio)
335 | }
336 | }
337 | } else {
338 | minScale = 1.0f
339 | }
340 | } else {
341 | minScale = userSpecifiedMinScale
342 | }
343 | if (maxScaleIsSetByMultiplier) {
344 | setMaxZoomRatio(maxScaleMultiplier)
345 | }
346 | superMinScale = SUPER_MIN_MULTIPLIER * minScale
347 | }
348 |
349 | // Reset zoom and translation to initial state.
350 | fun resetZoom() {
351 | currentZoom = 1f
352 | fitImageToView()
353 | }
354 |
355 | fun resetZoomAnimated() {
356 | setZoomAnimated(1f, 0.5f, 0.5f)
357 | }
358 |
359 | // Set zoom to the specified scale. Image will be centered by default.
360 | fun setZoom(scale: Float) {
361 | setZoom(scale, 0.5f, 0.5f)
362 | }
363 |
364 | /**
365 | * Set zoom to the specified scale. Image will be centered around the point
366 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
367 | * as a fraction from the left and top of the view. For example, the top left
368 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
369 | */
370 | fun setZoom(scale: Float, focusX: Float, focusY: Float) {
371 | setZoom(scale, focusX, focusY, touchScaleType)
372 | }
373 |
374 | /**
375 | * Set zoom to the specified scale. Image will be centered around the point
376 | * (focusX, focusY). These floats range from 0 to 1 and denote the focus point
377 | * as a fraction from the left and top of the view. For example, the top left
378 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
379 | */
380 | fun setZoom(scale: Float, focusX: Float, focusY: Float, scaleType: ScaleType?) {
381 |
382 | // setZoom can be called before the image is on the screen, but at this point,
383 | // image and view sizes have not yet been calculated in onMeasure. Thus, we should
384 | // delay calling setZoom until the view has been measured.
385 | if (!onDrawReady) {
386 | delayedZoomVariables = ZoomVariables(scale, focusX, focusY, scaleType)
387 | return
388 | }
389 | if (userSpecifiedMinScale == AUTOMATIC_MIN_ZOOM) {
390 | minZoom = AUTOMATIC_MIN_ZOOM
391 | if (currentZoom < minScale) {
392 | currentZoom = minScale
393 | }
394 | }
395 | if (scaleType != touchScaleType) {
396 | setScaleType(scaleType!!)
397 | }
398 | resetZoom()
399 | scaleImage(scale.toDouble(), viewWidth / 2.toFloat(), viewHeight / 2.toFloat(), isSuperZoomEnabled)
400 | touchMatrix.getValues(floatMatrix)
401 | floatMatrix[Matrix.MTRANS_X] = -(focusX * imageWidth - viewWidth * 0.5f)
402 | floatMatrix[Matrix.MTRANS_Y] = -(focusY * imageHeight - viewHeight * 0.5f)
403 | touchMatrix.setValues(floatMatrix)
404 | fixTrans()
405 | savePreviousImageValues()
406 | imageMatrix = touchMatrix
407 | }
408 |
409 | /**
410 | * Set zoom parameters equal to another TouchImageView. Including scale, position and ScaleType.
411 | */
412 | fun setZoom(imageSource: TouchImageView) {
413 | val center = imageSource.scrollPosition
414 | setZoom(imageSource.currentZoom, center.x, center.y, imageSource.scaleType)
415 | }
416 |
417 | /**
418 | * Return the point at the center of the zoomed image. The PointF coordinates range
419 | * in value between 0 and 1 and the focus point is denoted as a fraction from the left
420 | * and top of the view. For example, the top left corner of the image would be (0, 0).
421 | * And the bottom right corner would be (1, 1).
422 | *
423 | * @return PointF representing the scroll position of the zoomed image.
424 | */
425 | val scrollPosition: PointF
426 | get() {
427 | val drawable = drawable ?: return PointF(.5f, .5f)
428 | val drawableWidth = getDrawableWidth(drawable)
429 | val drawableHeight = getDrawableHeight(drawable)
430 | val point = transformCoordTouchToBitmap(viewWidth / 2.toFloat(), viewHeight / 2.toFloat(), true)
431 | point.x /= drawableWidth.toFloat()
432 | point.y /= drawableHeight.toFloat()
433 | return point
434 | }
435 |
436 | private fun orientationMismatch(drawable: Drawable?): Boolean {
437 | return viewWidth > viewHeight != drawable!!.intrinsicWidth > drawable.intrinsicHeight
438 | }
439 |
440 | private fun getDrawableWidth(drawable: Drawable?): Int {
441 | return if (orientationMismatch(drawable) && isRotateImageToFitScreen) {
442 | drawable!!.intrinsicHeight
443 | } else
444 | drawable!!.intrinsicWidth
445 | }
446 |
447 | private fun getDrawableHeight(drawable: Drawable?): Int {
448 | return if (orientationMismatch(drawable) && isRotateImageToFitScreen) {
449 | drawable!!.intrinsicWidth
450 | } else
451 | drawable!!.intrinsicHeight
452 | }
453 |
454 | /**
455 | * Set the focus point of the zoomed image. The focus points are denoted as a fraction from the
456 | * left and top of the view. The focus points can range in value between 0 and 1.
457 | */
458 | fun setScrollPosition(focusX: Float, focusY: Float) {
459 | setZoom(currentZoom, focusX, focusY)
460 | }
461 |
462 | /**
463 | * Performs boundary checking and fixes the image matrix if it
464 | * is out of bounds.
465 | */
466 | private fun fixTrans() {
467 | touchMatrix.getValues(floatMatrix)
468 | val transX = floatMatrix[Matrix.MTRANS_X]
469 | val transY = floatMatrix[Matrix.MTRANS_Y]
470 | var offset = 0f
471 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) {
472 | offset = imageWidth
473 | }
474 | val fixTransX = getFixTrans(transX, viewWidth.toFloat(), imageWidth, offset)
475 | val fixTransY = getFixTrans(transY, viewHeight.toFloat(), imageHeight, 0f)
476 | touchMatrix.postTranslate(fixTransX, fixTransY)
477 | }
478 |
479 | /**
480 | * When transitioning from zooming from focus to zoom from center (or vice versa)
481 | * the image can become unaligned within the view. This is apparent when zooming
482 | * quickly. When the content size is less than the view size, the content will often
483 | * be centered incorrectly within the view. fixScaleTrans first calls fixTrans() and
484 | * then makes sure the image is centered correctly within the view.
485 | */
486 | private fun fixScaleTrans() {
487 | fixTrans()
488 | touchMatrix.getValues(floatMatrix)
489 | if (imageWidth < viewWidth) {
490 | var xOffset = (viewWidth - imageWidth) / 2
491 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) {
492 | xOffset += imageWidth
493 | }
494 | floatMatrix[Matrix.MTRANS_X] = xOffset
495 | }
496 | if (imageHeight < viewHeight) {
497 | floatMatrix[Matrix.MTRANS_Y] = (viewHeight - imageHeight) / 2
498 | }
499 | touchMatrix.setValues(floatMatrix)
500 | }
501 |
502 | private fun getFixTrans(trans: Float, viewSize: Float, contentSize: Float, offset: Float): Float {
503 | val minTrans: Float
504 | val maxTrans: Float
505 | if (contentSize <= viewSize) {
506 | minTrans = offset
507 | maxTrans = offset + viewSize - contentSize
508 | } else {
509 | minTrans = offset + viewSize - contentSize
510 | maxTrans = offset
511 | }
512 | if (trans < minTrans) return -trans + minTrans
513 | return if (trans > maxTrans) -trans + maxTrans else 0f
514 | }
515 |
516 | private fun getFixDragTrans(delta: Float, viewSize: Float, contentSize: Float): Float {
517 | return if (contentSize <= viewSize) {
518 | 0f
519 | } else
520 | delta
521 | }
522 |
523 | private val imageWidth: Float
524 | get() = matchViewWidth * currentZoom
525 |
526 | private val imageHeight: Float
527 | get() = matchViewHeight * currentZoom
528 |
529 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
530 | val drawable = drawable
531 | if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) {
532 | setMeasuredDimension(0, 0)
533 | return
534 | }
535 | val drawableWidth = getDrawableWidth(drawable)
536 | val drawableHeight = getDrawableHeight(drawable)
537 | val widthSize = MeasureSpec.getSize(widthMeasureSpec)
538 | val widthMode = MeasureSpec.getMode(widthMeasureSpec)
539 | val heightSize = MeasureSpec.getSize(heightMeasureSpec)
540 | val heightMode = MeasureSpec.getMode(heightMeasureSpec)
541 | val totalViewWidth = setViewSize(widthMode, widthSize, drawableWidth)
542 | val totalViewHeight = setViewSize(heightMode, heightSize, drawableHeight)
543 | if (!orientationJustChanged) {
544 | savePreviousImageValues()
545 | }
546 |
547 | // Set view dimensions
548 | setMeasuredDimension(totalViewWidth, totalViewHeight)
549 | }
550 |
551 | override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
552 | super.onSizeChanged(w, h, oldw, oldh)
553 |
554 | // Fit content within view.
555 | //
556 | // onMeasure may be called multiple times for each layout change, including orientation
557 | // changes. For example, if the TouchImageView is inside a ConstraintLayout, onMeasure may
558 | // be called with:
559 | // widthMeasureSpec == "AT_MOST 2556" and then immediately with
560 | // widthMeasureSpec == "EXACTLY 1404", then back and forth multiple times in quick
561 | // succession, as the ConstraintLayout tries to solve its constraints.
562 | //
563 | // onSizeChanged is called once after the final onMeasure is called. So we make all changes
564 | // to class members, such as fitting the image into the new shape of the TouchImageView,
565 | // here, after the final size has been determined. This helps us avoid both
566 | // repeated computations, and making irreversible changes (e.g. making the View temporarily too
567 | // big or too small, thus making the current zoom fall outside of an automatically-changing
568 | // minZoom and maxZoom).
569 | viewWidth = w - paddingRight -paddingLeft
570 | viewHeight = h - paddingTop - paddingBottom
571 | fitImageToView()
572 | }
573 |
574 | /**
575 | * This function can be called:
576 | * 1. When the TouchImageView is first loaded (onMeasure).
577 | * 2. When a new image is loaded (setImageResource|Bitmap|Drawable|URI).
578 | * 3. On rotation (onSaveInstanceState, then onRestoreInstanceState, then onMeasure).
579 | * 4. When the view is resized (onMeasure).
580 | * 5. When the zoom is reset (resetZoom).
581 | *
582 | * In cases 2, 3 and 4, we try to maintain the zoom state and position as directed by
583 | * orientationChangeFixedPixel or viewSizeChangeFixedPixel (if there is an existing zoom state
584 | * and position, which there might not be in case 2).
585 | *
586 | *
587 | * If the normalizedScale is equal to 1, then the image is made to fit the View. Otherwise, we
588 | * maintain zoom level and attempt to roughly put the same part of the image in the View as was
589 | * there before, paying attention to orientationChangeFixedPixel or viewSizeChangeFixedPixel.
590 | */
591 | private fun fitImageToView() {
592 | val fixedPixel = if (orientationJustChanged) orientationChangeFixedPixel else viewSizeChangeFixedPixel
593 | orientationJustChanged = false
594 | val drawable = drawable
595 | if (drawable == null || drawable.intrinsicWidth == 0 || drawable.intrinsicHeight == 0) {
596 | return
597 | }
598 | @Suppress("SENSELESS_COMPARISON")
599 | if (touchMatrix == null || prevMatrix == null) {
600 | return
601 | }
602 | if (userSpecifiedMinScale == AUTOMATIC_MIN_ZOOM) {
603 | minZoom = AUTOMATIC_MIN_ZOOM
604 | if (currentZoom < minScale) {
605 | currentZoom = minScale
606 | }
607 | }
608 | val drawableWidth = getDrawableWidth(drawable)
609 | val drawableHeight = getDrawableHeight(drawable)
610 |
611 | // Scale image for view
612 | var scaleX = viewWidth.toFloat() / drawableWidth
613 | var scaleY = viewHeight.toFloat() / drawableHeight
614 | when (touchScaleType) {
615 | ScaleType.CENTER -> {
616 | scaleY = 1f
617 | scaleX = scaleY
618 | }
619 | ScaleType.CENTER_CROP -> {
620 | scaleY = max(scaleX, scaleY)
621 | scaleX = scaleY
622 | }
623 | ScaleType.CENTER_INSIDE -> {
624 | run {
625 | scaleY = min(1f, min(scaleX, scaleY))
626 | scaleX = scaleY
627 | }
628 | run {
629 | scaleY = min(scaleX, scaleY)
630 | scaleX = scaleY
631 | }
632 | }
633 | ScaleType.FIT_CENTER, ScaleType.FIT_START, ScaleType.FIT_END -> {
634 | scaleY = min(scaleX, scaleY)
635 | scaleX = scaleY
636 | }
637 | ScaleType.FIT_XY -> Unit
638 | else -> Unit
639 | }
640 |
641 | // Put the image's center in the right place.
642 | val redundantXSpace = viewWidth - scaleX * drawableWidth
643 | val redundantYSpace = viewHeight - scaleY * drawableHeight
644 | matchViewWidth = viewWidth - redundantXSpace
645 | matchViewHeight = viewHeight - redundantYSpace
646 | if (!isZoomed && !imageRenderedAtLeastOnce) {
647 |
648 | // Stretch and center image to fit view
649 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) {
650 | touchMatrix.setRotate(90f)
651 | touchMatrix.postTranslate(drawableWidth.toFloat(), 0f)
652 | touchMatrix.postScale(scaleX, scaleY)
653 | } else {
654 | touchMatrix.setScale(scaleX, scaleY)
655 | }
656 | when (touchScaleType) {
657 | ScaleType.FIT_START -> touchMatrix.postTranslate(0f, 0f)
658 | ScaleType.FIT_END -> touchMatrix.postTranslate(redundantXSpace, redundantYSpace)
659 | else -> touchMatrix.postTranslate(redundantXSpace / 2, redundantYSpace / 2)
660 | }
661 | currentZoom = 1f
662 | } else {
663 | // These values should never be 0 or we will set viewWidth and viewHeight
664 | // to NaN in newTranslationAfterChange. To avoid this, call savePreviousImageValues
665 | // to set them equal to the current values.
666 | if (prevMatchViewWidth == 0f || prevMatchViewHeight == 0f) {
667 | savePreviousImageValues()
668 | }
669 |
670 | // Use the previous matrix as our starting point for the new matrix.
671 | prevMatrix.getValues(floatMatrix)
672 |
673 | // Rescale Matrix if appropriate
674 | floatMatrix[Matrix.MSCALE_X] = matchViewWidth / drawableWidth * currentZoom
675 | floatMatrix[Matrix.MSCALE_Y] = matchViewHeight / drawableHeight * currentZoom
676 |
677 | // TransX and TransY from previous matrix
678 | val transX = floatMatrix[Matrix.MTRANS_X]
679 | val transY = floatMatrix[Matrix.MTRANS_Y]
680 |
681 | // X position
682 | val prevActualWidth = prevMatchViewWidth * currentZoom
683 | val actualWidth = imageWidth
684 | floatMatrix[Matrix.MTRANS_X] =
685 | newTranslationAfterChange(transX, prevActualWidth, actualWidth, prevViewWidth, viewWidth, drawableWidth, fixedPixel)
686 |
687 | // Y position
688 | val prevActualHeight = prevMatchViewHeight * currentZoom
689 | val actualHeight = imageHeight
690 | floatMatrix[Matrix.MTRANS_Y] =
691 | newTranslationAfterChange(transY, prevActualHeight, actualHeight, prevViewHeight, viewHeight, drawableHeight, fixedPixel)
692 |
693 | // Set the matrix to the adjusted scale and translation values.
694 | touchMatrix.setValues(floatMatrix)
695 | }
696 | fixTrans()
697 | imageMatrix = touchMatrix
698 | }
699 |
700 | // Set view dimensions based on layout params
701 | private fun setViewSize(mode: Int, size: Int, drawableWidth: Int): Int {
702 | return when (mode) {
703 | MeasureSpec.EXACTLY -> size
704 | MeasureSpec.AT_MOST -> min(drawableWidth, size)
705 | MeasureSpec.UNSPECIFIED -> drawableWidth
706 | else -> size
707 | }
708 | }
709 |
710 | /**
711 | * After any change described in the comments for fitImageToView, the matrix needs to be
712 | * translated. This function translates the image so that the fixed pixel in the image
713 | * stays in the same place in the View.
714 | *
715 | * @param trans the value of trans in that axis before the rotation
716 | * @param prevImageSize the width/height of the image before the rotation
717 | * @param imageSize width/height of the image after rotation
718 | * @param prevViewSize width/height of view before rotation
719 | * @param viewSize width/height of view after rotation
720 | * @param drawableSize width/height of drawable
721 | * @param sizeChangeFixedPixel how we should choose the fixed pixel
722 | */
723 | private fun newTranslationAfterChange(
724 | trans: Float,
725 | prevImageSize: Float,
726 | imageSize: Float,
727 | prevViewSize: Int,
728 | viewSize: Int,
729 | drawableSize: Int,
730 | sizeChangeFixedPixel: FixedPixel?
731 | ): Float {
732 | return when {
733 | imageSize < viewSize -> {
734 | // The width/height of image is less than the view's width/height. Center it.
735 | (viewSize - drawableSize * floatMatrix[Matrix.MSCALE_X]) * 0.5f
736 | }
737 | trans > 0 -> {
738 | // The image is larger than the view, but was not before the view changed. Center it.
739 | -((imageSize - viewSize) * 0.5f)
740 | }
741 | else -> {
742 | // Where is the pixel in the View that we are keeping stable, as a fraction of the width/height of the View?
743 | var fixedPixelPositionInView = 0.5f // CENTER
744 | if (sizeChangeFixedPixel == FixedPixel.BOTTOM_RIGHT) {
745 | fixedPixelPositionInView = 1.0f
746 | } else if (sizeChangeFixedPixel == FixedPixel.TOP_LEFT) {
747 | fixedPixelPositionInView = 0.0f
748 | }
749 | // Where is the pixel in the Image that we are keeping stable, as a fraction of the
750 | // width/height of the Image?
751 | val fixedPixelPositionInImage = (-trans + fixedPixelPositionInView * prevViewSize) / prevImageSize
752 |
753 | // Here's what the new translation should be so that, after whatever change triggered
754 | // this function to be called, the pixel at fixedPixelPositionInView of the View is
755 | // still the pixel at fixedPixelPositionInImage of the image.
756 | -(fixedPixelPositionInImage * imageSize - viewSize * fixedPixelPositionInView)
757 | }
758 | }
759 | }
760 |
761 | private fun setState(imageActionState: ImageActionState) {
762 | this.imageActionState = imageActionState
763 | }
764 |
765 | override fun canScrollHorizontally(direction: Int): Boolean {
766 | touchMatrix.getValues(floatMatrix)
767 | val x = floatMatrix[Matrix.MTRANS_X]
768 | return if (imageWidth < viewWidth) {
769 | false
770 | } else if (x >= -1 && direction < 0) {
771 | false
772 | } else abs(x) + viewWidth + 1 < imageWidth || direction <= 0
773 | }
774 |
775 | override fun canScrollVertically(direction: Int): Boolean {
776 | touchMatrix.getValues(floatMatrix)
777 | val y = floatMatrix[Matrix.MTRANS_Y]
778 | return if (imageHeight < viewHeight) {
779 | false
780 | } else if (y >= -1 && direction < 0) {
781 | false
782 | } else abs(y) + viewHeight + 1 < imageHeight || direction <= 0
783 | }
784 |
785 | /**
786 | * Gesture Listener detects a single click or long click and passes that on
787 | * to the view's listener.
788 | */
789 | private inner class GestureListener : SimpleOnGestureListener() {
790 | override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
791 | // Pass on to the OnDoubleTapListener if it is present, otherwise let the View handle the click.
792 | return doubleTapListener?.onSingleTapConfirmed(e) ?: performClick()
793 | }
794 |
795 | override fun onLongPress(e: MotionEvent) {
796 | performLongClick()
797 | }
798 |
799 | override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
800 | // If a previous fling is still active, it should be cancelled so that two flings
801 | // are not run simultaneously.
802 | fling?.cancelFling()
803 | fling = Fling(velocityX.toInt(), velocityY.toInt()).also { compatPostOnAnimation(it) }
804 | return super.onFling(e1, e2, velocityX, velocityY)
805 | }
806 |
807 | override fun onDoubleTap(e: MotionEvent): Boolean {
808 | var consumed = false
809 | if (isZoomEnabled) {
810 | doubleTapListener?.let {
811 | consumed = it.onDoubleTap(e)
812 | }
813 | if (imageActionState == ImageActionState.NONE) {
814 | val maxZoomScale = if (doubleTapScale == 0f) maxScale else doubleTapScale
815 | val targetZoom = if (currentZoom == minScale) maxZoomScale else minScale
816 | val doubleTap = DoubleTapZoom(targetZoom, e.x, e.y, false)
817 | compatPostOnAnimation(doubleTap)
818 | consumed = true
819 | }
820 | }
821 | return consumed
822 | }
823 |
824 | override fun onDoubleTapEvent(e: MotionEvent): Boolean {
825 | return doubleTapListener?.onDoubleTapEvent(e) ?: false
826 | }
827 | }
828 |
829 | /**
830 | * Responsible for all touch events. Handles the heavy lifting of drag and also sends
831 | * touch events to Scale Detector and Gesture Detector.
832 | */
833 | private inner class PrivateOnTouchListener : OnTouchListener {
834 |
835 | // Remember last point position for dragging
836 | private val last = PointF()
837 | override fun onTouch(v: View, event: MotionEvent): Boolean {
838 | if (drawable == null) {
839 | setState(ImageActionState.NONE)
840 | return false
841 | }
842 | if (isZoomEnabled) {
843 | scaleDetector.onTouchEvent(event)
844 | }
845 | gestureDetector.onTouchEvent(event)
846 | val curr = PointF(event.x, event.y)
847 | if (imageActionState == ImageActionState.NONE || imageActionState == ImageActionState.DRAG || imageActionState == ImageActionState.FLING) {
848 | when (event.action) {
849 | MotionEvent.ACTION_DOWN -> {
850 | last.set(curr)
851 | fling?.cancelFling()
852 | setState(ImageActionState.DRAG)
853 | }
854 | MotionEvent.ACTION_MOVE -> if (imageActionState == ImageActionState.DRAG) {
855 | val deltaX = curr.x - last.x
856 | val deltaY = curr.y - last.y
857 | val fixTransX = getFixDragTrans(deltaX, viewWidth.toFloat(), imageWidth)
858 | val fixTransY = getFixDragTrans(deltaY, viewHeight.toFloat(), imageHeight)
859 | touchMatrix.postTranslate(fixTransX, fixTransY)
860 | fixTrans()
861 | last[curr.x] = curr.y
862 | }
863 | MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> setState(ImageActionState.NONE)
864 | }
865 | }
866 |
867 | touchCoordinatesListener?.let {
868 | val bitmapPoint = transformCoordTouchToBitmap(event.x, event.y, true)
869 | it.onTouchCoordinate(v, event, bitmapPoint)
870 | }
871 |
872 | imageMatrix = touchMatrix
873 |
874 | // User-defined OnTouchListener
875 | userTouchListener?.onTouch(v, event)
876 |
877 | // OnTouchImageViewListener is set: TouchImageView dragged by user.
878 | touchImageViewListener?.onMove()
879 |
880 | // indicate event was handled
881 | return true
882 | }
883 | }
884 |
885 | /**
886 | * ScaleListener detects user two finger scaling and scales image.
887 | */
888 | private inner class ScaleListener : SimpleOnScaleGestureListener() {
889 | override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
890 | setState(ImageActionState.ZOOM)
891 | return true
892 | }
893 |
894 | override fun onScale(detector: ScaleGestureDetector): Boolean {
895 | scaleImage(detector.scaleFactor.toDouble(), detector.focusX, detector.focusY, isSuperZoomEnabled)
896 |
897 | // OnTouchImageViewListener is set: TouchImageView pinch zoomed by user.
898 | touchImageViewListener?.onMove()
899 | return true
900 | }
901 |
902 | override fun onScaleEnd(detector: ScaleGestureDetector) {
903 | super.onScaleEnd(detector)
904 | setState(ImageActionState.NONE)
905 | var animateToZoomBoundary = false
906 | var targetZoom: Float = currentZoom
907 | if (currentZoom > maxScale) {
908 | targetZoom = maxScale
909 | animateToZoomBoundary = true
910 | } else if (currentZoom < minScale) {
911 | targetZoom = minScale
912 | animateToZoomBoundary = true
913 | }
914 | if (animateToZoomBoundary) {
915 | val doubleTap = DoubleTapZoom(targetZoom, (viewWidth / 2).toFloat(), (viewHeight / 2).toFloat(), isSuperZoomEnabled)
916 | compatPostOnAnimation(doubleTap)
917 | }
918 | }
919 | }
920 |
921 | private fun scaleImage(deltaScale: Double, focusX: Float, focusY: Float, stretchImageToSuper: Boolean) {
922 | var deltaScaleLocal = deltaScale
923 | val lowerScale: Float
924 | val upperScale: Float
925 | if (stretchImageToSuper) {
926 | lowerScale = superMinScale
927 | upperScale = superMaxScale
928 | } else {
929 | lowerScale = minScale
930 | upperScale = maxScale
931 | }
932 | val origScale = currentZoom
933 | currentZoom *= deltaScaleLocal.toFloat()
934 | if (currentZoom > upperScale) {
935 | currentZoom = upperScale
936 | deltaScaleLocal = upperScale / origScale.toDouble()
937 | } else if (currentZoom < lowerScale) {
938 | currentZoom = lowerScale
939 | deltaScaleLocal = lowerScale / origScale.toDouble()
940 | }
941 | touchMatrix.postScale(deltaScaleLocal.toFloat(), deltaScaleLocal.toFloat(), focusX, focusY)
942 | fixScaleTrans()
943 | }
944 |
945 | /**
946 | * DoubleTapZoom calls a series of runnables which apply
947 | * an animated zoom in/out graphic to the image.
948 | */
949 | private inner class DoubleTapZoom(targetZoom: Float, focusX: Float, focusY: Float, stretchImageToSuper: Boolean) : Runnable {
950 | private val startTime: Long
951 | private val startZoom: Float
952 | private val targetZoom: Float
953 | private val bitmapX: Float
954 | private val bitmapY: Float
955 | private val stretchImageToSuper: Boolean
956 | private val interpolator = AccelerateDecelerateInterpolator()
957 | private val startTouch: PointF
958 | private val endTouch: PointF
959 | override fun run() {
960 | if (drawable == null) {
961 | setState(ImageActionState.NONE)
962 | return
963 | }
964 | val t = interpolate()
965 | val deltaScale = calculateDeltaScale(t)
966 | scaleImage(deltaScale, bitmapX, bitmapY, stretchImageToSuper)
967 | translateImageToCenterTouchPosition(t)
968 | fixScaleTrans()
969 | imageMatrix = touchMatrix
970 |
971 | // double tap runnable updates listener with every frame.
972 | touchImageViewListener?.onMove()
973 | if (t < 1f) {
974 | // We haven't finished zooming
975 | compatPostOnAnimation(this)
976 | } else {
977 | // Finished zooming
978 | setState(ImageActionState.NONE)
979 | }
980 | }
981 |
982 | /**
983 | * Interpolate between where the image should start and end in order to translate
984 | * the image so that the point that is touched is what ends up centered at the end
985 | * of the zoom.
986 | */
987 | private fun translateImageToCenterTouchPosition(t: Float) {
988 | val targetX = startTouch.x + t * (endTouch.x - startTouch.x)
989 | val targetY = startTouch.y + t * (endTouch.y - startTouch.y)
990 | val curr = transformCoordBitmapToTouch(bitmapX, bitmapY)
991 | touchMatrix.postTranslate(targetX - curr.x, targetY - curr.y)
992 | }
993 |
994 | /**
995 | * Use interpolator to get t
996 | */
997 | private fun interpolate(): Float {
998 | val currTime = System.currentTimeMillis()
999 | var elapsed = (currTime - startTime) / DEFAULT_ZOOM_TIME.toFloat()
1000 | elapsed = min(1f, elapsed)
1001 | return interpolator.getInterpolation(elapsed)
1002 | }
1003 |
1004 | /**
1005 | * Interpolate the current targeted zoom and get the delta
1006 | * from the current zoom.
1007 | */
1008 | private fun calculateDeltaScale(t: Float): Double {
1009 | val zoom = startZoom + t * (targetZoom - startZoom).toDouble()
1010 | return zoom / currentZoom
1011 | }
1012 |
1013 | init {
1014 | setState(ImageActionState.ANIMATE_ZOOM)
1015 | startTime = System.currentTimeMillis()
1016 | startZoom = currentZoom
1017 | this.targetZoom = targetZoom
1018 | this.stretchImageToSuper = stretchImageToSuper
1019 | val bitmapPoint = transformCoordTouchToBitmap(focusX, focusY, false)
1020 | bitmapX = bitmapPoint.x
1021 | bitmapY = bitmapPoint.y
1022 |
1023 | // Used for translating image during scaling
1024 | startTouch = transformCoordBitmapToTouch(bitmapX, bitmapY)
1025 | endTouch = PointF((viewWidth / 2).toFloat(), (viewHeight / 2).toFloat())
1026 | }
1027 | }
1028 |
1029 | /**
1030 | * This function will transform the coordinates in the touch event to the coordinate
1031 | * system of the drawable that the imageview contain
1032 | *
1033 | * @param x x-coordinate of touch event
1034 | * @param y y-coordinate of touch event
1035 | * @param clipToBitmap Touch event may occur within view, but outside image content. True, to clip return value
1036 | * to the bounds of the bitmap size.
1037 | * @return Coordinates of the point touched, in the coordinate system of the original drawable.
1038 | */
1039 | protected fun transformCoordTouchToBitmap(x: Float, y: Float, clipToBitmap: Boolean): PointF {
1040 | touchMatrix.getValues(floatMatrix)
1041 | val origW = drawable.intrinsicWidth.toFloat()
1042 | val origH = drawable.intrinsicHeight.toFloat()
1043 | val transX = floatMatrix[Matrix.MTRANS_X]
1044 | val transY = floatMatrix[Matrix.MTRANS_Y]
1045 | var finalX = (x - transX) * origW / imageWidth
1046 | var finalY = (y - transY) * origH / imageHeight
1047 | if (clipToBitmap) {
1048 | finalX = min(max(finalX, 0f), origW)
1049 | finalY = min(max(finalY, 0f), origH)
1050 | }
1051 | return PointF(finalX, finalY)
1052 | }
1053 |
1054 | /**
1055 | * Inverse of transformCoordTouchToBitmap. This function will transform the coordinates in the
1056 | * drawable's coordinate system to the view's coordinate system.
1057 | *
1058 | * @param bx x-coordinate in original bitmap coordinate system
1059 | * @param by y-coordinate in original bitmap coordinate system
1060 | * @return Coordinates of the point in the view's coordinate system.
1061 | */
1062 | protected fun transformCoordBitmapToTouch(bx: Float, by: Float): PointF {
1063 | touchMatrix.getValues(floatMatrix)
1064 | val origW = drawable.intrinsicWidth.toFloat()
1065 | val origH = drawable.intrinsicHeight.toFloat()
1066 | val px = bx / origW
1067 | val py = by / origH
1068 | val finalX = floatMatrix[Matrix.MTRANS_X] + imageWidth * px
1069 | val finalY = floatMatrix[Matrix.MTRANS_Y] + imageHeight * py
1070 | return PointF(finalX, finalY)
1071 | }
1072 |
1073 | /**
1074 | * Fling launches sequential runnables which apply
1075 | * the fling graphic to the image. The values for the translation
1076 | * are interpolated by the Scroller.
1077 | */
1078 | private inner class Fling(velocityX: Int, velocityY: Int) : Runnable {
1079 | var scroller: CompatScroller
1080 | var currX: Int
1081 | var currY: Int
1082 |
1083 | init {
1084 | setState(ImageActionState.FLING)
1085 | scroller = CompatScroller(context)
1086 | touchMatrix.getValues(floatMatrix)
1087 | var startX = floatMatrix[Matrix.MTRANS_X].toInt()
1088 | val startY = floatMatrix[Matrix.MTRANS_Y].toInt()
1089 | val minX: Int
1090 | val maxX: Int
1091 | val minY: Int
1092 | val maxY: Int
1093 | if (isRotateImageToFitScreen && orientationMismatch(drawable)) {
1094 | startX -= imageWidth.toInt()
1095 | }
1096 | if (imageWidth > viewWidth) {
1097 | minX = viewWidth - imageWidth.toInt()
1098 | maxX = 0
1099 | } else {
1100 | maxX = startX
1101 | minX = maxX
1102 | }
1103 | if (imageHeight > viewHeight) {
1104 | minY = viewHeight - imageHeight.toInt()
1105 | maxY = 0
1106 | } else {
1107 | maxY = startY
1108 | minY = maxY
1109 | }
1110 | scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)
1111 | currX = startX
1112 | currY = startY
1113 | }
1114 |
1115 | fun cancelFling() {
1116 | setState(ImageActionState.NONE)
1117 | scroller.forceFinished(true)
1118 | }
1119 |
1120 | override fun run() {
1121 |
1122 | // OnTouchImageViewListener is set: TouchImageView listener has been flung by user.
1123 | // Listener runnable updated with each frame of fling animation.
1124 | touchImageViewListener?.onMove()
1125 | if (scroller.isFinished) {
1126 | return
1127 | }
1128 | if (scroller.computeScrollOffset()) {
1129 | val newX = scroller.currX
1130 | val newY = scroller.currY
1131 | val transX = newX - currX
1132 | val transY = newY - currY
1133 | currX = newX
1134 | currY = newY
1135 | touchMatrix.postTranslate(transX.toFloat(), transY.toFloat())
1136 | fixTrans()
1137 | imageMatrix = touchMatrix
1138 | compatPostOnAnimation(this)
1139 | }
1140 | }
1141 |
1142 | }
1143 |
1144 | private inner class CompatScroller(context: Context?) {
1145 | var overScroller: OverScroller = OverScroller(context)
1146 | fun fling(startX: Int, startY: Int, velocityX: Int, velocityY: Int, minX: Int, maxX: Int, minY: Int, maxY: Int) {
1147 | overScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY)
1148 | }
1149 |
1150 | fun forceFinished(finished: Boolean) {
1151 | overScroller.forceFinished(finished)
1152 | }
1153 |
1154 | val isFinished: Boolean
1155 | get() = overScroller.isFinished
1156 |
1157 | fun computeScrollOffset(): Boolean {
1158 | overScroller.computeScrollOffset()
1159 | return overScroller.computeScrollOffset()
1160 | }
1161 |
1162 | val currX: Int
1163 | get() = overScroller.currX
1164 |
1165 | val currY: Int
1166 | get() = overScroller.currY
1167 |
1168 | }
1169 |
1170 | private fun compatPostOnAnimation(runnable: Runnable) {
1171 | postOnAnimation(runnable)
1172 | }
1173 |
1174 | /**
1175 | * Set zoom to the specified scale with a linearly interpolated animation. Image will be
1176 | * centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the
1177 | * focus point as a fraction from the left and top of the view. For example, the top left
1178 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
1179 | */
1180 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float) {
1181 | setZoomAnimated(scale, focusX, focusY, DEFAULT_ZOOM_TIME)
1182 | }
1183 |
1184 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, zoomTimeMs: Int) {
1185 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), zoomTimeMs)
1186 | compatPostOnAnimation(animation)
1187 | }
1188 |
1189 | /**
1190 | * Set zoom to the specified scale with a linearly interpolated animation. Image will be
1191 | * centered around the point (focusX, focusY). These floats range from 0 to 1 and denote the
1192 | * focus point as a fraction from the left and top of the view. For example, the top left
1193 | * corner of the image would be (0, 0). And the bottom right corner would be (1, 1).
1194 | *
1195 | * @param listener the listener, which will be notified, once the animation ended
1196 | */
1197 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, zoomTimeMs: Int, listener: OnZoomFinishedListener?) {
1198 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), zoomTimeMs)
1199 | animation.setListener(listener)
1200 | compatPostOnAnimation(animation)
1201 | }
1202 |
1203 | fun setZoomAnimated(scale: Float, focusX: Float, focusY: Float, listener: OnZoomFinishedListener?) {
1204 | val animation = AnimatedZoom(scale, PointF(focusX, focusY), DEFAULT_ZOOM_TIME)
1205 | animation.setListener(listener)
1206 | compatPostOnAnimation(animation)
1207 | }
1208 |
1209 | /**
1210 | * AnimatedZoom calls a series of runnables which apply
1211 | * an animated zoom to the specified target focus at the specified zoom level.
1212 | */
1213 | private inner class AnimatedZoom(targetZoom: Float, focus: PointF, zoomTimeMillis: Int) : Runnable {
1214 | private val zoomTimeMillis: Int
1215 | private val startTime: Long
1216 | private val startZoom: Float
1217 | private val targetZoom: Float
1218 | private val startFocus: PointF
1219 | private val targetFocus: PointF
1220 | private val interpolator = LinearInterpolator()
1221 | private var zoomFinishedListener: OnZoomFinishedListener? = null
1222 |
1223 | init {
1224 | setState(ImageActionState.ANIMATE_ZOOM)
1225 | startTime = System.currentTimeMillis()
1226 | startZoom = currentZoom
1227 | this.targetZoom = targetZoom
1228 | this.zoomTimeMillis = zoomTimeMillis
1229 |
1230 | // Used for translating image during zooming
1231 | startFocus = scrollPosition
1232 | targetFocus = focus
1233 | }
1234 |
1235 | override fun run() {
1236 | val t = interpolate()
1237 |
1238 | // Calculate the next focus and zoom based on the progress of the interpolation
1239 | val nextZoom = startZoom + (targetZoom - startZoom) * t
1240 | val nextX = startFocus.x + (targetFocus.x - startFocus.x) * t
1241 | val nextY = startFocus.y + (targetFocus.y - startFocus.y) * t
1242 | setZoom(nextZoom, nextX, nextY)
1243 | if (t < 1f) {
1244 | // We haven't finished zooming
1245 | compatPostOnAnimation(this)
1246 | } else {
1247 | // Finished zooming
1248 | setState(ImageActionState.NONE)
1249 | zoomFinishedListener?.onZoomFinished()
1250 | }
1251 | }
1252 |
1253 | /**
1254 | * Use interpolator to get t
1255 | *
1256 | * @return progress of the interpolation
1257 | */
1258 | private fun interpolate(): Float {
1259 | var elapsed = (System.currentTimeMillis() - startTime) / zoomTimeMillis.toFloat()
1260 | elapsed = min(1f, elapsed)
1261 | return interpolator.getInterpolation(elapsed)
1262 | }
1263 |
1264 | fun setListener(listener: OnZoomFinishedListener?) {
1265 | this.zoomFinishedListener = listener
1266 | }
1267 |
1268 | }
1269 |
1270 | companion object {
1271 | // SuperMin and SuperMax multipliers. Determine how much the image can be zoomed below or above the zoom boundaries,
1272 | // before animating back to the min/max zoom boundary.
1273 | private const val SUPER_MIN_MULTIPLIER = .75f
1274 | private const val SUPER_MAX_MULTIPLIER = 1.25f
1275 | private const val DEFAULT_ZOOM_TIME = 500
1276 |
1277 | // If setMinZoom(AUTOMATIC_MIN_ZOOM), then we'll set the min scale to include the whole image.
1278 | const val AUTOMATIC_MIN_ZOOM = -1.0f
1279 | }
1280 |
1281 | }
1282 |
--------------------------------------------------------------------------------
/touchview/src/main/java/com/ortiz/touchview/ZoomVariables.kt:
--------------------------------------------------------------------------------
1 | package com.ortiz.touchview
2 |
3 | import android.widget.ImageView
4 |
5 | internal data class ZoomVariables(var scale: Float, var focusX: Float, var focusY: Float, var scaleType: ImageView.ScaleType?)
--------------------------------------------------------------------------------
/touchview/src/main/res/values/attrs_touchimageview.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------