├── .arcconfig ├── .arclint ├── .editorconfig ├── .gitignore ├── .local.dependencies.template ├── .travis.yml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── checkstyle.xml ├── circle.yml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── install-local-dependency.sh ├── library ├── build.gradle ├── proguard-rules.pro └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── google │ │ └── android │ │ └── material │ │ └── motion │ │ └── gestures │ │ ├── DragGestureRecognizer.java │ │ ├── GestureRecognizer.java │ │ ├── RotateGestureRecognizer.java │ │ ├── ScaleGestureRecognizer.java │ │ ├── ValueVelocityTracker.java │ │ └── testing │ │ └── SimulatedGestureRecognizer.java │ └── test │ └── java │ └── com │ └── google │ └── android │ └── material │ └── motion │ └── gestures │ ├── DragGestureRecognizerTests.java │ ├── GestureRecognizerTests.java │ ├── RotateGestureRecognizerTests.java │ ├── ScaleGestureRecognizerTests.java │ ├── TrackingGestureStateChangeListener.java │ ├── ValueVelocityTrackerTests.java │ └── testing │ └── SimulatedGestureRecognizerTests.java ├── local-dependency-substitution.gradle ├── sample ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── google │ │ └── android │ │ └── material │ │ └── motion │ │ └── gestures │ │ └── sample │ │ ├── CheckerboardDrawable.java │ │ └── MainActivity.java │ └── res │ ├── layout │ └── main_activity.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── strings.xml │ └── styles.xml └── settings.gradle /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "load": [ 3 | "material-arc-tools/third_party/arc-hook-conphig", 4 | "material-arc-tools/third_party/arc-hook-github-issues", 5 | "material-arc-tools/third_party/arc-proselint" 6 | ], 7 | "arcanist_configuration": "HookConphig", 8 | "phabricator.uri": "http://codereview.cc/", 9 | "repository.callsign": "MDMGESTURESANDROID", 10 | "arc.land.onto.default": "develop", 11 | "arc.feature.start.default": "origin/develop" 12 | } 13 | -------------------------------------------------------------------------------- /.arclint: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "chmod": { 4 | "type": "chmod" 5 | }, 6 | "text": { 7 | "type": "text", 8 | "include": "(\\.(java|xml)$)", 9 | "exclude": [], 10 | "severity": { 11 | "3": "disabled", 12 | "5": "disabled" 13 | } 14 | }, 15 | "prose": { 16 | "type": "prose", 17 | "include": "(\\.(md)$)", 18 | "exclude": [ 19 | "(^CHANGELOG.md)" 20 | ], 21 | "severity": { 22 | "consistency.spacing": "disabled", 23 | "typography.symbols.curly_quotes": "disabled", 24 | "typography.symbols.ellipsis": "disabled", 25 | "leonard.exclamation.30ppm": "disabled", 26 | "misc.annotations": "warning" 27 | } 28 | }, 29 | "spelling": { 30 | "type": "spelling", 31 | "include": "(\\.(md)$)" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = true 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | 18 | # Gradle files 19 | .gradle/ 20 | build/ 21 | 22 | # Local configuration file (sdk path, etc) 23 | local.properties 24 | local.dependencies 25 | 26 | # Proguard folder generated by Eclipse 27 | proguard/ 28 | 29 | # Log Files 30 | *.log 31 | 32 | # Android Studio Navigation editor temp files 33 | .navigation/ 34 | 35 | # Android Studio captures folder 36 | captures/ 37 | 38 | # Intellij 39 | *.iml 40 | .idea/ 41 | 42 | # Keystore files 43 | *.jks 44 | -------------------------------------------------------------------------------- /.local.dependencies.template: -------------------------------------------------------------------------------- 1 | # List local dependencies here to be applied to all projects in this library. 2 | # 3 | # ██████╗ ██╗ ███████╗ █████╗ ███████╗███████╗ ██████╗ ███████╗ █████╗ ██████╗ 4 | # ██╔══██╗██║ ██╔════╝██╔══██╗██╔════╝██╔════╝ ██╔══██╗██╔════╝██╔══██╗██╔══██╗ 5 | # ██████╔╝██║ █████╗ ███████║███████╗█████╗ ██████╔╝█████╗ ███████║██║ ██║ 6 | # ██╔═══╝ ██║ ██╔══╝ ██╔══██║╚════██║██╔══╝ ██╔══██╗██╔══╝ ██╔══██║██║ ██║ 7 | # ██║ ███████╗███████╗██║ ██║███████║███████╗ ██║ ██║███████╗██║ ██║██████╔╝ 8 | # ╚═╝ ╚══════╝╚══════╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═════╝ 9 | # 10 | # Format: 11 | # 12 | # [group]:[name] 13 | # 14 | # Example: 15 | # 16 | # com.github.material-motion:gestures-android 17 | # 18 | # These are dependencies defined in your build.gradle for which you would like to reflect any local 19 | # changes. This is useful if you would like to develop multiple libraries in tandem. 20 | # 21 | # You must `Sync Project with Gradle Files` every time you add or remove a local dependency. 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: android 3 | jdk: oraclejdk8 4 | env: 5 | matrix: 6 | - ANDROID_TARGET=android-25 7 | 8 | android: 9 | components: 10 | - platform-tools 11 | - tools 12 | - android-25 13 | - build-tools-25.0.2 14 | 15 | licenses: 16 | - 'android-sdk-license-.+' 17 | 18 | before_install: 19 | - echo yes | android update sdk --filter extra-android-m2repository --no-ui --force > /dev/null 20 | 21 | script: 22 | - ./gradlew check 23 | 24 | after_success: 25 | - bash <(curl -s https://codecov.io/bash) 26 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of Gestures authors for copyright purposes. 2 | # 3 | # This does not necessarily list everyone who has contributed code, since in 4 | # some cases, their employer may be the copyright holder. To see the full list 5 | # of contributors, see the revision history with git log. 6 | 7 | Google Inc. 8 | and other contributors 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.0 2 | 3 | ## New features 4 | 5 | New gesture recognizers for drag, scale, and rotate. 6 | 7 | ## Source changes 8 | 9 | * [Move gesture recognizers out from direct-manipulation-android into its own repo.](https://github.com/material-motion/gestures-android/commit/25c61043d739ab1440cff59624bb137c9cbd6514) (Mark Wei) 10 | 11 | ## API changes 12 | 13 | Auto-generated by running: 14 | 15 | apidiff origin/stable release-candidate android library 16 | 17 | ## DragGestureRecognizer 18 | 19 | *new* class: `DragGestureRecognizer` 20 | 21 | *new* constructor: `DragGestureRecognizer()` 22 | 23 | *new* field: `int dragSlop` 24 | 25 | *new* method: `float getTranslationX()` 26 | 27 | *new* method: `float getTranslationY()` 28 | 29 | *new* method: `float getUntransformedCentroidX()` 30 | 31 | *new* method: `float getUntransformedCentroidY()` 32 | 33 | *new* method: `float getVelocityX()` 34 | 35 | *new* method: `float getVelocityY()` 36 | 37 | *new* method: `boolean onTouchEvent(MotionEvent)` 38 | 39 | *new* method: `void setElement(View)` 40 | 41 | 42 | ## GestureRecognizer 43 | 44 | *new* abstract class: `GestureRecognizer` 45 | 46 | *new* constructor: `GestureRecognizer()` 47 | 48 | *new* static final field: `int BEGAN` 49 | 50 | *new* static final field: `int CANCELLED` 51 | 52 | *new* static final field: `int CHANGED` 53 | 54 | *new* static final field: `int POSSIBLE` 55 | 56 | *new* static final field: `int RECOGNIZED` 57 | 58 | *new* method: `void addStateChangeListener(GestureStateChangeListener)` 59 | 60 | *new* final method: `float getCentroidX()` 61 | 62 | *new* final method: `float getCentroidY()` 63 | 64 | *new* method: `View getElement()` 65 | 66 | *new* method: `int getState()` 67 | 68 | *new* static method: `void getTransformationMatrix(View, Matrix, Matrix)` 69 | 70 | *new* abstract method: `float getUntransformedCentroidX()` 71 | 72 | *new* abstract method: `float getUntransformedCentroidY()` 73 | 74 | *new* abstract method: `boolean onTouchEvent(MotionEvent)` 75 | 76 | *new* method: `void removeStateChangeListener(GestureStateChangeListener)` 77 | 78 | *new* method: `void setElement(View)` 79 | 80 | 81 | ## GestureRecognizerState 82 | 83 | *new* annotation: `@GestureRecognizerState` 84 | 85 | 86 | ## GestureStateChangeListener 87 | 88 | *new* interface: `GestureStateChangeListener` 89 | 90 | *new* method: `void onStateChanged(GestureRecognizer)` 91 | 92 | 93 | ## Library 94 | 95 | *removed* class: `Library` 96 | 97 | *removed* constructor: `Library()` 98 | 99 | *removed* static final field: `String LIBRARY_NAME` 100 | 101 | 102 | ## RotateGestureRecognizer 103 | 104 | *new* class: `RotateGestureRecognizer` 105 | 106 | *new* constructor: `RotateGestureRecognizer()` 107 | 108 | *new* field: `float rotateSlop` 109 | 110 | *new* method: `float getRotation()` 111 | 112 | *new* method: `float getUntransformedCentroidX()` 113 | 114 | *new* method: `float getUntransformedCentroidY()` 115 | 116 | *new* method: `float getVelocity()` 117 | 118 | *new* method: `boolean onTouchEvent(MotionEvent)` 119 | 120 | *new* method: `void setElement(View)` 121 | 122 | 123 | ## ScaleGestureRecognizer 124 | 125 | *new* class: `ScaleGestureRecognizer` 126 | 127 | *new* constructor: `ScaleGestureRecognizer()` 128 | 129 | *new* field: `int scaleSlop` 130 | 131 | *new* method: `float getScale()` 132 | 133 | *new* method: `float getUntransformedCentroidX()` 134 | 135 | *new* method: `float getUntransformedCentroidY()` 136 | 137 | *new* method: `float getVelocity()` 138 | 139 | *new* method: `boolean onTouchEvent(MotionEvent)` 140 | 141 | *new* method: `void setElement(View)` 142 | 143 | 144 | ## AccumulationType 145 | 146 | *new* annotation: `@AccumulationType` 147 | 148 | 149 | ## SimulatedGestureRecognizer 150 | 151 | *new* class: `SimulatedGestureRecognizer` 152 | 153 | *new* constructor: `SimulatedGestureRecognizer(View)` 154 | 155 | *new* method: `float getUntransformedCentroidX()` 156 | 157 | *new* method: `float getUntransformedCentroidY()` 158 | 159 | *new* method: `boolean onTouchEvent(MotionEvent)` 160 | 161 | *new* method: `void setState(int)` 162 | 163 | 164 | 165 | ## Non-source changes 166 | 167 | * [Automatic changelog preparation for release.](https://github.com/material-motion/gestures-android/commit/d0a177c370d004378e2d1b417de3f16885a4f344) (Mark Wei) 168 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Want to contribute? Great! First, read this page (including the small print at 2 | the end). 3 | 4 | ### Before you contribute 5 | 6 | Before we can use your code, you must sign the 7 | [Google Individual Contributor License Agreement] 8 | (https://cla.developers.google.com/about/google-individual) 9 | (CLA), which you can do online. The CLA is necessary mainly because you own the 10 | copyright to your changes, even after your contribution becomes part of our 11 | codebase, so we need your permission to use and distribute your code. We also 12 | need to be sure of various other things—for instance that you'll tell us if you 13 | know that your code infringes on other people's patents. You don't have to sign 14 | the CLA until after you've submitted your code for review and a member has 15 | approved it, but you must do it before we can put your code into our codebase. 16 | Before you start working on a larger contribution, you should get in touch with 17 | us first through the issue tracker with your idea so that we can help out and 18 | possibly guide you. Coordinating up front makes it much easier to avoid 19 | frustration later on. 20 | 21 | ### Code reviews 22 | 23 | All submissions, including submissions by project members, require review. 24 | We use GitHub pull requests for this purpose. 25 | 26 | ### The small print 27 | 28 | Contributions made by corporations are covered by a different agreement than 29 | the one above, the 30 | [Software Grant and Corporate Contributor License Agreement] 31 | (https://cla.developers.google.com/about/google-corporate). 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gestures 2 | 3 | [![Build Status](https://travis-ci.org/material-motion/gestures-android.svg?branch=develop)](https://travis-ci.org/material-motion/gestures-android) 4 | [![codecov](https://codecov.io/gh/material-motion/gestures-android/branch/develop/graph/badge.svg)](https://codecov.io/gh/material-motion/gestures-android) 5 | [![Release](https://img.shields.io/github/release/material-motion/gestures-android.svg)](https://github.com/material-motion/gestures-android/releases/latest) 6 | [![Docs](https://img.shields.io/badge/jitpack-docs-green.svg)](https://jitpack.io/com/github/material-motion/gestures-android/stable-SNAPSHOT/javadoc/) 7 | 8 | The Gestures repo. 9 | 10 | Learn more about the APIs defined in the library by reading our 11 | [technical documentation](https://jitpack.io/com/github/material-motion/gestures-android/1.0.0/javadoc/) and our 12 | [Starmap](https://material-motion.github.io/material-motion/starmap/). 13 | 14 | ## Installation 15 | 16 | ### Installation with Jitpack 17 | 18 | Add the Jitpack repository to your project's `build.gradle`: 19 | 20 | ```gradle 21 | allprojects { 22 | repositories { 23 | maven { url "https://jitpack.io" } 24 | } 25 | } 26 | ``` 27 | 28 | Depend on the [latest version](https://github.com/material-motion/gestures-android/releases) of the library. 29 | Take care to occasionally [check for updates](https://github.com/ben-manes/gradle-versions-plugin). 30 | 31 | ```gradle 32 | dependencies { 33 | compile 'com.github.material-motion:gestures-android:1.0.0' 34 | } 35 | ``` 36 | 37 | Enable [Java 8 language features](https://developer.android.com/studio/preview/features/java8-support.html). 38 | 39 | ```gradle 40 | android { 41 | compileOptions { 42 | sourceCompatibility JavaVersion.VERSION_1_8 43 | targetCompatibility JavaVersion.VERSION_1_8 44 | } 45 | } 46 | ``` 47 | 48 | For more information regarding versioning, see: 49 | 50 | - [Material Motion Versioning Policies](https://material-motion.github.io/material-motion/team/essentials/core_team_contributors/release_process#versioning) 51 | 52 | ### Using the files from a folder local to the machine 53 | 54 | You can have a copy of this library with local changes and test it in tandem 55 | with its client project. To add a local dependency on this library, add this 56 | library's identifier to your project's `local.dependencies`: 57 | 58 | ``` 59 | com.github.material-motion:gestures-android 60 | ``` 61 | 62 | > Because `local.dependencies` is never to be checked into Version Control 63 | Systems, you must also ensure that any local dependencies are also defined in 64 | `build.gradle` as explained in the previous section. 65 | 66 | **Important** 67 | 68 | For each local dependency listed, you *must* run `gradle install` from its 69 | project root every time you make a change to it. That command will publish your 70 | latest changes to the local maven repository. If your local dependencies have 71 | local dependencies of their own, you must `gradle install` them as well. 72 | 73 | You must `gradle clean` your project every time you add or remove a local 74 | dependency. 75 | 76 | ### Usage 77 | 78 | How to use the library in your project. 79 | 80 | #### Editing the library in Android Studio 81 | 82 | Open Android Studio, 83 | choose `File > New > Import`, 84 | choose the root `build.gradle` file. 85 | 86 | ## Example apps/unit tests 87 | 88 | To build the sample application, run the following commands: 89 | 90 | git clone https://github.com/material-motion/gestures-android.git 91 | cd gestures-android 92 | gradle installDebug 93 | 94 | To run all unit tests, run the following commands: 95 | 96 | git clone https://github.com/material-motion/gestures-android.git 97 | cd gestures-android 98 | gradle test 99 | 100 | # Guides 101 | 102 | 1. [Architecture](#architecture) 103 | 1. [How to ...](#how-to-...) 104 | 105 | ## Architecture 106 | 107 | ## How to ... 108 | 109 | ## Contributing 110 | 111 | We welcome contributions! 112 | 113 | Check out our [upcoming milestones](https://github.com/material-motion/gestures-android/milestones). 114 | 115 | Learn more about [our team](https://material-motion.github.io/material-motion/team/), 116 | [our community](https://material-motion.github.io/material-motion/team/community/), and 117 | our [contributor essentials](https://material-motion.github.io/material-motion/team/essentials/). 118 | 119 | ## License 120 | 121 | Licensed under the Apache 2.0 license. See LICENSE for details. 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | apply from: 'local-dependency-substitution.gradle' 2 | apply plugin: "checkstyle" 3 | 4 | buildscript { 5 | repositories { 6 | jcenter() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:2.4.0-alpha6' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' 11 | classpath 'com.vanniktech:gradle-android-junit-jacoco-plugin:0.6.0' 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | maven { url "https://jitpack.io" } 19 | } 20 | } 21 | 22 | ext { 23 | compileSdkVersion = 25 24 | buildToolsVersion = '25.0.2' 25 | minSdkVersion = 15 26 | targetSdkVersion = compileSdkVersion 27 | 28 | supportLibVersion = '25.3.1' 29 | } 30 | 31 | subprojects { 32 | apply plugin: 'checkstyle' 33 | apply plugin: 'pmd' 34 | 35 | checkstyle { 36 | configFile = rootProject.file('checkstyle.xml') 37 | toolVersion = '7.1' 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 69 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 94 | 95 | 96 | 98 | 99 | 100 | 101 | 103 | 104 | 105 | 106 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 120 | 121 | 122 | 123 | 125 | 126 | 127 | 128 | 130 | 131 | 132 | 133 | 135 | 136 | 137 | 138 | 140 | 142 | 144 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | # From http://codereview.cc/harbormaster/step/edit/6/ 2 | dependencies: 3 | pre: 4 | # Android SDK Platform 25 5 | - if [ ! -d "/usr/local/android-sdk-linux/platforms/android-25" ]; then echo y | android update sdk --no-ui --all --filter "android-25"; fi 6 | # Android SDK Build-tools, revision 25.0.2 7 | - if [ ! -d "/usr/local/android-sdk-linux/build-tools/25.0.2" ]; then echo y | android update sdk --no-ui --all --filter "build-tools-25.0.2"; fi 8 | # Android Support Repository, revision 25.3.1 / Local Maven repository for Support Libraries 9 | - if [ ! -d "/usr/local/android-sdk-linux/extras/android/m2repository/com/android/support/design/25.3.1" ]; then echo y | android update sdk --no-ui --all --filter "extra-android-m2repository"; fi 10 | 11 | 12 | cache_directories: 13 | - /usr/local/android-sdk-linux/platforms/android-25 14 | - /usr/local/android-sdk-linux/build-tools/25.0.2 15 | - /usr/local/android-sdk-linux/extras/android/m2repository 16 | notify: 17 | webhooks: 18 | - url: http://codereview.cc/harbormaster/hook/circleci/ 19 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 06 21:26:45 EDT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /install-local-dependency.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # 3 | # Copyright 2016-present The Material Motion Authors. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # Usage: `install-local-dependency.sh ` 18 | # 19 | # Publishes the dependency : to the local maven repository 20 | # using `gradle install`. This builds the local changes in that project 21 | # and propagates them to dependent projects. 22 | # 23 | # Used by local-dependency-substitution.gradle 24 | 25 | group="$1" 26 | name="$2" 27 | 28 | dir="$(mdm dir $name)" || { 29 | cat << EOF 30 | Failed to get the local repo path for dependency $group:$name. 31 | Make sure you read through our Contributor essentials: https://material-motion.github.io/material-motion/team/essentials/ 32 | 33 | Especially make sure that: 34 | 35 | * You have installed our team's mdm tool https://material-motion.github.io/material-motion/team/essentials/frequent_contributors/tools 36 | \$(mdm dir) should output the correct directory 37 | * You have cloned the repo for $group:$name 38 | \$(mdm dir $name) should output the correct directory 39 | EOF 40 | exit 1 41 | } 42 | 43 | cd "$dir" 44 | ./gradlew install || { 45 | echo "Failed to publish dependency $group:$name to the local maven repository." 46 | exit 1 47 | } 48 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.vanniktech.android.junit.jacoco' 4 | 5 | group = 'com.github.material-motion' 6 | 7 | install { 8 | repositories.mavenInstaller { 9 | pom.version = 'local' 10 | pom.artifactId = 'gestures-android' 11 | } 12 | } 13 | 14 | android { 15 | compileSdkVersion rootProject.ext.compileSdkVersion 16 | buildToolsVersion rootProject.ext.buildToolsVersion 17 | 18 | defaultConfig { 19 | minSdkVersion rootProject.ext.minSdkVersion 20 | targetSdkVersion rootProject.ext.targetSdkVersion 21 | versionCode 1 22 | versionName "1.0" 23 | consumerProguardFiles 'proguard-rules.pro' 24 | } 25 | 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | 31 | lintOptions { 32 | abortOnError false 33 | } 34 | 35 | buildTypes { 36 | debug { 37 | testCoverageEnabled true 38 | } 39 | } 40 | } 41 | 42 | dependencies { 43 | // If you are developing any dependencies locally, also list them in local.dependencies. 44 | compile "com.android.support:support-compat:$supportLibVersion" 45 | 46 | testCompile 'com.google.truth:truth:0.28' 47 | testCompile 'junit:junit:4.12' 48 | testCompile 'org.mockito:mockito-core:1.10.19' 49 | testCompile 'org.robolectric:robolectric:3.1.2' 50 | } 51 | 52 | // build a jar with source files 53 | task sourcesJar(type: Jar) { 54 | from android.sourceSets.main.java.srcDirs 55 | classifier = 'sources' 56 | } 57 | 58 | task javadoc(type: Javadoc) { 59 | failOnError false 60 | source = android.sourceSets.main.java.sourceFiles 61 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 62 | classpath += configurations.compile 63 | } 64 | 65 | // build a jar with javadoc 66 | task javadocJar(type: Jar, dependsOn: javadoc) { 67 | classifier = 'javadoc' 68 | from javadoc.destinationDir 69 | } 70 | 71 | artifacts { 72 | archives sourcesJar 73 | archives javadocJar 74 | } 75 | -------------------------------------------------------------------------------- /library/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 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 | -keep @android.support.annotation.Keep class * 13 | 14 | -keepclassmembers class * { 15 | @android.support.annotation.Keep *; 16 | } 17 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/DragGestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.content.Context; 19 | import android.graphics.PointF; 20 | import android.support.annotation.Nullable; 21 | import android.support.v4.view.MotionEventCompat; 22 | import android.view.MotionEvent; 23 | import android.view.View; 24 | import android.view.ViewConfiguration; 25 | 26 | import static com.google.android.material.motion.gestures.ValueVelocityTracker.ADDITIVE; 27 | 28 | /** 29 | * A gesture recognizer that generates translation events. 30 | */ 31 | public class DragGestureRecognizer extends GestureRecognizer { 32 | 33 | /** 34 | * Touch slop for drag. Amount of pixels that the centroid needs to move in either axes. 35 | */ 36 | public int dragSlop = UNSET_SLOP; 37 | 38 | private float initialCentroidX; 39 | private float initialCentroidY; 40 | private float currentCentroidX; 41 | private float currentCentroidY; 42 | 43 | @Nullable 44 | private ValueVelocityTracker centroidXVelocityTracker; 45 | @Nullable 46 | private ValueVelocityTracker centroidYVelocityTracker; 47 | 48 | @Override 49 | public void setElement(@Nullable View element) { 50 | super.setElement(element); 51 | 52 | if (element == null) { 53 | return; 54 | } 55 | 56 | if (dragSlop == UNSET_SLOP) { 57 | Context context = element.getContext(); 58 | dragSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 59 | } 60 | if (centroidXVelocityTracker == null) { 61 | centroidXVelocityTracker = new ValueVelocityTracker(element.getContext(), ADDITIVE); 62 | centroidYVelocityTracker = new ValueVelocityTracker(element.getContext(), ADDITIVE); 63 | } 64 | } 65 | 66 | @Override 67 | protected boolean onTouch(MotionEvent event) { 68 | PointF centroid = calculateUntransformedCentroid(event); 69 | float centroidX = centroid.x; 70 | float centroidY = centroid.y; 71 | 72 | int action = MotionEventCompat.getActionMasked(event); 73 | if (action == MotionEvent.ACTION_DOWN) { 74 | initialCentroidX = centroidX; 75 | initialCentroidY = centroidY; 76 | currentCentroidX = centroidX; 77 | currentCentroidY = centroidY; 78 | 79 | centroidXVelocityTracker.onGestureStart(event, centroidX); 80 | centroidYVelocityTracker.onGestureStart(event, centroidY); 81 | 82 | if (dragSlop == 0) { 83 | setState(BEGAN); 84 | } 85 | } 86 | if (action == MotionEvent.ACTION_POINTER_DOWN 87 | || action == MotionEvent.ACTION_POINTER_UP) { 88 | float adjustX = centroidX - currentCentroidX; 89 | float adjustY = centroidY - currentCentroidY; 90 | 91 | initialCentroidX += adjustX; 92 | initialCentroidY += adjustY; 93 | currentCentroidX += adjustX; 94 | currentCentroidY += adjustY; 95 | 96 | centroidXVelocityTracker.onGestureAdjust(-adjustX); 97 | centroidYVelocityTracker.onGestureAdjust(-adjustY); 98 | } 99 | if (action == MotionEvent.ACTION_MOVE) { 100 | if (!isInProgress()) { 101 | float deltaX = centroidX - initialCentroidX; 102 | float deltaY = centroidY - initialCentroidY; 103 | if (Math.abs(deltaX) > dragSlop || Math.abs(deltaY) > dragSlop) { 104 | float adjustX = Math.signum(deltaX) * Math.min(Math.abs(deltaX), dragSlop); 105 | float adjustY = Math.signum(deltaY) * Math.min(Math.abs(deltaY), dragSlop); 106 | 107 | initialCentroidX += adjustX; 108 | initialCentroidY += adjustY; 109 | currentCentroidX += adjustX; 110 | currentCentroidY += adjustY; 111 | 112 | setState(BEGAN); 113 | } 114 | } 115 | 116 | if (isInProgress()) { 117 | currentCentroidX = centroidX; 118 | currentCentroidY = centroidY; 119 | 120 | setState(CHANGED); 121 | } 122 | 123 | centroidXVelocityTracker.onGestureMove(event, centroidX); 124 | centroidYVelocityTracker.onGestureMove(event, centroidY); 125 | } 126 | if (action == MotionEvent.ACTION_UP 127 | || action == MotionEvent.ACTION_CANCEL) { 128 | initialCentroidX = centroidX; 129 | initialCentroidY = centroidY; 130 | currentCentroidX = centroidX; 131 | currentCentroidY = centroidY; 132 | 133 | centroidXVelocityTracker.onGestureEnd(event, centroidX); 134 | centroidYVelocityTracker.onGestureEnd(event, centroidY); 135 | 136 | if (isInProgress()) { 137 | if (action == MotionEvent.ACTION_UP) { 138 | setState(RECOGNIZED); 139 | } else { 140 | setState(CANCELLED); 141 | } 142 | } 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Returns the translationX of the drag gesture. 150 | *

151 | * This reports the total translation over time since the {@link #BEGAN beginning} of the 152 | * gesture. This is not a delta value from the last {@link #CHANGED update}. 153 | */ 154 | public float getTranslationX() { 155 | return currentCentroidX - initialCentroidX; 156 | } 157 | 158 | /** 159 | * Returns the translationY of the drag gesture. 160 | *

161 | * This reports the total translation over time since the {@link #BEGAN beginning} of the 162 | * gesture. This is not a delta value from the last {@link #CHANGED update}. 163 | */ 164 | public float getTranslationY() { 165 | return currentCentroidY - initialCentroidY; 166 | } 167 | 168 | /** 169 | * Returns the positional velocityX of the drag gesture. 170 | *

171 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 172 | * 173 | * @return The velocity in pixels per second. 174 | */ 175 | public float getVelocityX() { 176 | return centroidXVelocityTracker != null ? centroidXVelocityTracker.getCurrentVelocity() : 0f; 177 | } 178 | 179 | /** 180 | * Returns the positional velocityY of the drag gesture. 181 | *

182 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 183 | * 184 | * @return The velocity in pixels per second. 185 | */ 186 | public float getVelocityY() { 187 | return centroidYVelocityTracker != null ? centroidYVelocityTracker.getCurrentVelocity() : 0f; 188 | } 189 | 190 | @Override 191 | public float getUntransformedCentroidX() { 192 | return currentCentroidX; 193 | } 194 | 195 | @Override 196 | public float getUntransformedCentroidY() { 197 | return currentCentroidY; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/GestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.graphics.Matrix; 19 | import android.graphics.PointF; 20 | import android.support.annotation.IntDef; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.view.MotionEventCompat; 23 | import android.view.MotionEvent; 24 | import android.view.View; 25 | import android.view.View.OnTouchListener; 26 | 27 | import java.lang.annotation.Retention; 28 | import java.lang.annotation.RetentionPolicy; 29 | import java.util.List; 30 | import java.util.concurrent.CopyOnWriteArrayList; 31 | 32 | /** 33 | * A gesture recognizer generates continuous or discrete events from a stream of device input 34 | * events. When attached to an element, any interactions with that element will be interpreted by 35 | * the gesture recognizer and turned into gesture events. The output is often a linear 36 | * transformation of translation, rotation, and/or scale. 37 | *

38 | * To use an instance of this class, forward all touch events from the element's parent to {@link 39 | * #onTouch(View, MotionEvent)}. 40 | */ 41 | public abstract class GestureRecognizer implements OnTouchListener { 42 | 43 | /** 44 | * A listener that receives {@link GestureRecognizer} events. 45 | */ 46 | public interface GestureStateChangeListener { 47 | 48 | /** 49 | * Notifies every time on {@link GestureRecognizerState state} change. 50 | *

51 | * Implementations should query the provided gesture recognizer for its current state and 52 | * properties. 53 | * 54 | * @param gestureRecognizer the gesture recognizer where the event originated from. 55 | */ 56 | void onStateChanged(GestureRecognizer gestureRecognizer); 57 | } 58 | 59 | /** 60 | * The gesture recognizer has not yet recognized its gesture, but may be evaluating touch 61 | * events. This is the default state. 62 | */ 63 | public static final int POSSIBLE = 0; 64 | /** 65 | * The gesture recognizer has received touch objects recognized as a continuous gesture. 66 | */ 67 | public static final int BEGAN = 1; 68 | /** 69 | * The gesture recognizer has received touches recognized as a change to a continuous gesture. 70 | */ 71 | public static final int CHANGED = 2; 72 | /** 73 | * The gesture recognizer has received touches recognized as the end of a continuous gesture. At 74 | * the next cycle of the run loop, the gesture recognizer resets its state to {@link 75 | * #POSSIBLE}. 76 | */ 77 | public static final int RECOGNIZED = 3; 78 | /** 79 | * The gesture recognizer has received touches resulting in the cancellation of a continuous 80 | * gesture. At the next cycle of the run loop, the gesture recognizer resets its state to {@link 81 | * #POSSIBLE}. 82 | */ 83 | public static final int CANCELLED = 4; 84 | 85 | /** 86 | * The state of the gesture recognizer. 87 | */ 88 | @IntDef({POSSIBLE, BEGAN, CHANGED, RECOGNIZED, CANCELLED}) 89 | @Retention(RetentionPolicy.SOURCE) 90 | public @interface GestureRecognizerState { 91 | 92 | } 93 | 94 | protected static final int UNSET_SLOP = -1; 95 | 96 | /* Temporary variables. */ 97 | private final Matrix matrix = new Matrix(); 98 | private final float[] array = new float[2]; 99 | private final PointF pointF = new PointF(); 100 | 101 | /** 102 | * Inverse transformation matrix that is updated on a untransformed point calculation. Use this 103 | * to convert untransformed points back to the element's local coordinate system. 104 | */ 105 | private final Matrix inverse = new Matrix(); 106 | 107 | private final List listeners = new CopyOnWriteArrayList<>(); 108 | @Nullable 109 | private View element; 110 | @GestureRecognizerState 111 | private int state = POSSIBLE; 112 | 113 | /** 114 | * Sets the view that this gesture recognizer is attached to. This must be called before this 115 | * gesture recognizer can start {@link #onTouchEvent(MotionEvent) accepting touch events}. 116 | * 117 | * @deprecated in #develop#. Will be made protected in a future release. No longer necessary for 118 | * clients to call this after {@link #onTouch(View, MotionEvent)}. 119 | */ 120 | @Deprecated 121 | public void setElement(@Nullable View element) { 122 | this.element = element; 123 | } 124 | 125 | /** 126 | * Returns the view associated with this gesture recognizer. 127 | */ 128 | public View getElement() { 129 | return element; 130 | } 131 | 132 | /** 133 | * Returns the current state of the gesture recognizer. 134 | */ 135 | @GestureRecognizerState 136 | public int getState() { 137 | return state; 138 | } 139 | 140 | /** 141 | * Forwards touch events from a {@link OnTouchListener} to this gesture recognizer. 142 | */ 143 | @Override 144 | public final boolean onTouch(View view, MotionEvent event) { 145 | if (view != element) { 146 | setElement(view); 147 | } 148 | return onTouchEvent(event); 149 | } 150 | 151 | /** 152 | * Gesture recognizers should implement this to handle touch events. 153 | */ 154 | protected boolean onTouch(MotionEvent event) { 155 | return false; 156 | } 157 | 158 | /** 159 | * Forwards touch events from a {@link OnTouchListener} to this gesture recognizer. 160 | * 161 | * @deprecated in #develop#. Call {@link #onTouch(View, MotionEvent)} to forward touch events 162 | * instead. GestureRecognizers should implement {@link #onTouch(MotionEvent)} instead. 163 | */ 164 | @Deprecated 165 | public boolean onTouchEvent(MotionEvent event) { 166 | return onTouch(event); 167 | } 168 | 169 | /** 170 | * Adds a listener to this gesture recognizer. 171 | */ 172 | public void addStateChangeListener(GestureStateChangeListener listener) { 173 | if (!listeners.contains(listener)) { 174 | listeners.add(listener); 175 | } 176 | } 177 | 178 | /** 179 | * Removes a listener from this gesture recognizer. 180 | */ 181 | public void removeStateChangeListener(GestureStateChangeListener listener) { 182 | listeners.remove(listener); 183 | } 184 | 185 | /** 186 | * Returns the centroidX position of the current gesture in the local coordinate space of the 187 | * {@link #element}. 188 | */ 189 | public final float getCentroidX() { 190 | array[0] = getUntransformedCentroidX(); 191 | array[1] = getUntransformedCentroidY(); 192 | 193 | inverse.mapPoints(array); 194 | 195 | return array[0]; 196 | } 197 | 198 | /** 199 | * Returns the centroidY position of the current gesture in the local coordinate space of the 200 | * {@link #element}. 201 | */ 202 | public final float getCentroidY() { 203 | array[0] = getUntransformedCentroidX(); 204 | array[1] = getUntransformedCentroidY(); 205 | 206 | inverse.mapPoints(array); 207 | 208 | return array[1]; 209 | } 210 | 211 | /** 212 | * Returns the untransformed centroidX position of the current gesture in the local coordinate 213 | * space of {@link #element}'s parent. 214 | */ 215 | public abstract float getUntransformedCentroidX(); 216 | 217 | /** 218 | * Returns the untransformed centroidY position of the current gesture in the local coordinate 219 | * space of {@link #element}'s parent. 220 | */ 221 | public abstract float getUntransformedCentroidY(); 222 | 223 | /** 224 | * Sets the state of the gesture recognizer and notifies all listeners. 225 | */ 226 | protected void setState(@GestureRecognizerState int state) { 227 | this.state = state; 228 | 229 | for (GestureStateChangeListener listener : listeners) { 230 | listener.onStateChanged(this); 231 | } 232 | 233 | element.removeCallbacks(setStateToPossible); 234 | if (state == RECOGNIZED || state == CANCELLED) { 235 | element.post(setStateToPossible); 236 | } 237 | } 238 | 239 | private final Runnable setStateToPossible = new Runnable() { 240 | @Override 241 | public void run() { 242 | setState(POSSIBLE); 243 | } 244 | }; 245 | 246 | protected boolean isInProgress() { 247 | return state == BEGAN || state == CHANGED; 248 | } 249 | 250 | /** 251 | * Calculates the untransformed centroid of all the active pointers in the given motion event. 252 | * 253 | * @return A point representing the centroid. The caller should read the values immediately as 254 | * the object may be reused in other calculations. 255 | */ 256 | protected PointF calculateUntransformedCentroid(MotionEvent event) { 257 | return calculateUntransformedCentroid(event, Integer.MAX_VALUE); 258 | } 259 | 260 | /** 261 | * Calculates the centroid of the first {@code n} active pointers in the given motion event. 262 | * 263 | * @return A point representing the centroid. The caller should read the values immediately as 264 | * the object may be reused in other calculations. 265 | */ 266 | protected PointF calculateUntransformedCentroid(MotionEvent event, int n) { 267 | int action = MotionEventCompat.getActionMasked(event); 268 | int index = MotionEventCompat.getActionIndex(event); 269 | 270 | float sumX = 0; 271 | float sumY = 0; 272 | int num = 0; 273 | for (int i = 0, count = event.getPointerCount(); i < count && i < n; i++) { 274 | if (action == MotionEvent.ACTION_POINTER_UP && index == i) { 275 | continue; 276 | } 277 | 278 | sumX += calculateUntransformedPoint(event, i).x; 279 | sumY += calculateUntransformedPoint(event, i).y; 280 | num++; 281 | } 282 | 283 | pointF.set(sumX / num, sumY / num); 284 | return pointF; 285 | } 286 | 287 | /** 288 | * Calculates the untransformed x and y of the pointer given by the pointer index in the given 289 | * motion event. 290 | *

291 | * An untransformed coordinate represents the location of a pointer that is not transformed by 292 | * the element's transformation matrix. {@code calculateUntransformedPoint(event, 0).x} is not 293 | * necessarily equal to {@code event.getRawX()}. 294 | * 295 | * @return A point representing the untransformed x and y. The caller should read the values 296 | * immediately as the object may be reused in other calculations. 297 | */ 298 | protected PointF calculateUntransformedPoint(MotionEvent event, int pointerIndex) { 299 | array[0] = event.getX(pointerIndex); 300 | array[1] = event.getY(pointerIndex); 301 | 302 | getTransformationMatrix(element, matrix, inverse); 303 | matrix.mapPoints(array); 304 | pointF.set(array[0], array[1]); 305 | 306 | return pointF; 307 | } 308 | 309 | /** 310 | * Calculates the transformation matrices that can convert from local to untransformed 311 | * coordinate spaces. 312 | * 313 | * @param matrix This output matrix can convert from local to untransformed coordinate space. 314 | * @param inverse This output matrix can convert from untransformed to local coordinate space. 315 | */ 316 | public static void getTransformationMatrix(View element, Matrix matrix, Matrix inverse) { 317 | matrix.reset(); 318 | matrix.postScale( 319 | element.getScaleX(), element.getScaleY(), element.getPivotX(), element.getPivotY()); 320 | matrix.postRotate(element.getRotation(), element.getPivotX(), element.getPivotY()); 321 | matrix.postTranslate(element.getTranslationX(), element.getTranslationY()); 322 | 323 | // Save the inverse matrix. 324 | matrix.invert(inverse); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/RotateGestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.graphics.PointF; 19 | import android.support.annotation.Nullable; 20 | import android.support.annotation.VisibleForTesting; 21 | import android.support.v4.view.MotionEventCompat; 22 | import android.view.MotionEvent; 23 | import android.view.View; 24 | 25 | import static com.google.android.material.motion.gestures.ValueVelocityTracker.ADDITIVE; 26 | 27 | /** 28 | * A gesture recognizer that generates scale events. 29 | */ 30 | public class RotateGestureRecognizer extends GestureRecognizer { 31 | 32 | /** 33 | * Touch slop for rotate. Amount of radians that the angle needs to change. 34 | */ 35 | public float rotateSlop = UNSET_SLOP; 36 | 37 | private float currentCentroidX; 38 | private float currentCentroidY; 39 | 40 | private float initialAngle; 41 | private float currentAngle; 42 | 43 | @Nullable 44 | private ValueVelocityTracker angleVelocityTracker; 45 | 46 | @Override 47 | public void setElement(@Nullable View element) { 48 | super.setElement(element); 49 | 50 | if (element == null) { 51 | return; 52 | } 53 | 54 | if (rotateSlop == UNSET_SLOP) { 55 | rotateSlop = (float) (Math.PI / 180); 56 | } 57 | if (angleVelocityTracker == null) { 58 | angleVelocityTracker = new ValueVelocityTracker(element.getContext(), ADDITIVE); 59 | } 60 | } 61 | 62 | @Override 63 | protected boolean onTouch(MotionEvent event) { 64 | PointF centroid = calculateUntransformedCentroid(event, 2); 65 | float centroidX = centroid.x; 66 | float centroidY = centroid.y; 67 | float angle = calculateAngle(event); 68 | 69 | int action = MotionEventCompat.getActionMasked(event); 70 | int pointerCount = event.getPointerCount(); 71 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2) { 72 | currentCentroidX = centroidX; 73 | currentCentroidY = centroidY; 74 | 75 | initialAngle = angle; 76 | currentAngle = angle; 77 | 78 | angleVelocityTracker.onGestureStart(event, angle); 79 | 80 | if (rotateSlop == 0) { 81 | setState(BEGAN); 82 | } 83 | } 84 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount > 2 85 | || action == MotionEvent.ACTION_POINTER_UP && pointerCount > 2) { 86 | float adjustX = centroidX - currentCentroidX; 87 | float adjustY = centroidY - currentCentroidY; 88 | 89 | currentCentroidX += adjustX; 90 | currentCentroidY += adjustY; 91 | 92 | float adjustAngle = angle - currentAngle; 93 | 94 | initialAngle += adjustAngle; 95 | currentAngle += adjustAngle; 96 | 97 | angleVelocityTracker.onGestureAdjust(-adjustAngle); 98 | } 99 | if (action == MotionEvent.ACTION_MOVE && pointerCount >= 2) { 100 | currentCentroidX = centroidX; 101 | currentCentroidY = centroidY; 102 | 103 | if (!isInProgress()) { 104 | float deltaAngle = angle - initialAngle; 105 | if (Math.abs(deltaAngle) > rotateSlop) { 106 | float adjustAngle = Math.signum(deltaAngle) * rotateSlop; 107 | 108 | initialAngle += adjustAngle; 109 | currentAngle += adjustAngle; 110 | 111 | setState(BEGAN); 112 | } 113 | } 114 | 115 | if (isInProgress()) { 116 | currentAngle = angle; 117 | 118 | setState(CHANGED); 119 | } 120 | 121 | angleVelocityTracker.onGestureMove(event, angle); 122 | } 123 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2 124 | || action == MotionEvent.ACTION_CANCEL && pointerCount >= 2) { 125 | currentCentroidX = centroidX; 126 | currentCentroidY = centroidY; 127 | 128 | initialAngle = 0; 129 | currentAngle = 0; 130 | 131 | angleVelocityTracker.onGestureEnd(event, angle); 132 | 133 | if (isInProgress()) { 134 | if (action == MotionEvent.ACTION_POINTER_UP) { 135 | setState(RECOGNIZED); 136 | } else { 137 | setState(CANCELLED); 138 | } 139 | } 140 | } 141 | 142 | return true; 143 | } 144 | 145 | /** 146 | * Returns the rotation of the rotate gesture in radians. 147 | *

148 | * This reports the total rotation over time since the {@link #BEGAN beginning} of the gesture. 149 | * This is not a delta value from the last {@link #CHANGED update}. 150 | */ 151 | public float getRotation() { 152 | return currentAngle - initialAngle; 153 | } 154 | 155 | /** 156 | * Returns the angular velocity of the angle gesture. 157 | *

158 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 159 | * 160 | * @return The velocity in radians per second. 161 | */ 162 | public float getVelocity() { 163 | return angleVelocityTracker != null ? angleVelocityTracker.getCurrentVelocity() : 0f; 164 | } 165 | 166 | @Override 167 | public float getUntransformedCentroidX() { 168 | return currentCentroidX; 169 | } 170 | 171 | @Override 172 | public float getUntransformedCentroidY() { 173 | return currentCentroidY; 174 | } 175 | 176 | /** 177 | * Calculates the angle between the first two pointers in the given motion event. 178 | *

179 | * Angle is calculated from finger 0 to finger 1. 180 | */ 181 | private float calculateAngle(MotionEvent event) { 182 | int action = MotionEventCompat.getActionMasked(event); 183 | int pointerIndex = MotionEventCompat.getActionIndex(event); 184 | int pointerCount = event.getPointerCount(); 185 | if (pointerCount < 2) { 186 | return 0; 187 | } 188 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2) { 189 | return 0; 190 | } 191 | 192 | int i0 = 0; 193 | int i1 = 1; 194 | if (action == MotionEvent.ACTION_POINTER_UP) { 195 | if (pointerIndex == 0) { 196 | i0++; 197 | i1++; 198 | } else if (pointerIndex == 1) { 199 | i1++; 200 | } 201 | } 202 | 203 | PointF point = calculateUntransformedPoint(event, i0); 204 | float x0 = point.x; 205 | float y0 = point.y; 206 | 207 | point = calculateUntransformedPoint(event, i1); 208 | float x1 = point.x; 209 | float y1 = point.y; 210 | 211 | return angle(x0, y0, x1, y1); 212 | } 213 | 214 | @VisibleForTesting 215 | static float angle(float x0, float y0, float x1, float y1) { 216 | return (float) Math.atan2(y1 - y0, x1 - x0); 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/ScaleGestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.content.Context; 19 | import android.graphics.PointF; 20 | import android.support.annotation.Nullable; 21 | import android.support.annotation.VisibleForTesting; 22 | import android.support.v4.view.MotionEventCompat; 23 | import android.view.MotionEvent; 24 | import android.view.View; 25 | import android.view.ViewConfiguration; 26 | 27 | import static com.google.android.material.motion.gestures.ValueVelocityTracker.MULTIPLICATIVE; 28 | 29 | /** 30 | * A gesture recognizer that generates scale events. 31 | */ 32 | public class ScaleGestureRecognizer extends GestureRecognizer { 33 | 34 | /** 35 | * Touch slop for scale. Amount of pixels that the span needs to change. 36 | */ 37 | public int scaleSlop = UNSET_SLOP; 38 | 39 | private float currentCentroidX; 40 | private float currentCentroidY; 41 | 42 | private float initialSpan; 43 | private float currentSpan; 44 | 45 | @Nullable 46 | private ValueVelocityTracker spanVelocityTracker; 47 | 48 | @Override 49 | public void setElement(@Nullable View element) { 50 | super.setElement(element); 51 | 52 | if (element == null) { 53 | return; 54 | } 55 | 56 | if (scaleSlop == UNSET_SLOP) { 57 | Context context = element.getContext(); 58 | scaleSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 59 | } 60 | if (spanVelocityTracker == null) { 61 | spanVelocityTracker = new ValueVelocityTracker(element.getContext(), MULTIPLICATIVE); 62 | } 63 | } 64 | 65 | @Override 66 | protected boolean onTouch(MotionEvent event) { 67 | PointF centroid = calculateUntransformedCentroid(event); 68 | float centroidX = centroid.x; 69 | float centroidY = centroid.y; 70 | float span = calculateAverageSpan(event, centroidX, centroidY); 71 | 72 | int action = MotionEventCompat.getActionMasked(event); 73 | int pointerCount = event.getPointerCount(); 74 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount == 2) { 75 | currentCentroidX = centroidX; 76 | currentCentroidY = centroidY; 77 | 78 | initialSpan = span; 79 | currentSpan = span; 80 | 81 | spanVelocityTracker.onGestureStart(event, span); 82 | 83 | if (scaleSlop == 0) { 84 | setState(BEGAN); 85 | } 86 | } 87 | if (action == MotionEvent.ACTION_POINTER_DOWN && pointerCount > 2 88 | || action == MotionEvent.ACTION_POINTER_UP && pointerCount > 2) { 89 | float adjustX = centroidX - currentCentroidX; 90 | float adjustY = centroidY - currentCentroidY; 91 | 92 | currentCentroidX += adjustX; 93 | currentCentroidY += adjustY; 94 | 95 | float adjustSpan = span / currentSpan; 96 | 97 | initialSpan *= adjustSpan; 98 | currentSpan *= adjustSpan; 99 | 100 | spanVelocityTracker.onGestureAdjust(1 / adjustSpan); 101 | } 102 | if (action == MotionEvent.ACTION_MOVE && pointerCount >= 2) { 103 | currentCentroidX = centroidX; 104 | currentCentroidY = centroidY; 105 | 106 | if (!isInProgress()) { 107 | float deltaSpan = span - initialSpan; 108 | if (Math.abs(deltaSpan) > scaleSlop) { 109 | float adjustSpan = 1 + Math.signum(deltaSpan) * (scaleSlop / initialSpan); 110 | 111 | initialSpan *= adjustSpan; 112 | currentSpan *= adjustSpan; 113 | 114 | setState(BEGAN); 115 | } 116 | } 117 | 118 | if (isInProgress()) { 119 | currentSpan = span; 120 | 121 | setState(CHANGED); 122 | } 123 | 124 | spanVelocityTracker.onGestureMove(event, span); 125 | } 126 | if (action == MotionEvent.ACTION_POINTER_UP && pointerCount == 2 127 | || action == MotionEvent.ACTION_CANCEL && pointerCount >= 2) { 128 | currentCentroidX = centroidX; 129 | currentCentroidY = centroidY; 130 | 131 | initialSpan = 0; 132 | currentSpan = 0; 133 | 134 | spanVelocityTracker.onGestureEnd(event, span); 135 | 136 | if (isInProgress()) { 137 | if (action == MotionEvent.ACTION_POINTER_UP) { 138 | setState(RECOGNIZED); 139 | } else { 140 | setState(CANCELLED); 141 | } 142 | } 143 | } 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Returns the scale of the pinch gesture. 150 | *

151 | * This reports the total scale over time since the {@link #BEGAN beginning} of the gesture. 152 | * This is not a delta value from the last {@link #CHANGED update}. 153 | */ 154 | public float getScale() { 155 | return initialSpan > 0 ? currentSpan / initialSpan : 1; 156 | } 157 | 158 | /** 159 | * Returns the scalar velocity of the scale gesture. 160 | *

161 | * Only read this when the state is {@link #RECOGNIZED} or {@link #CANCELLED}. 162 | * 163 | * @return The velocity in pixels per second. 164 | */ 165 | public float getVelocity() { 166 | return spanVelocityTracker != null ? spanVelocityTracker.getCurrentVelocity() : 0f; 167 | } 168 | 169 | @Override 170 | public float getUntransformedCentroidX() { 171 | return currentCentroidX; 172 | } 173 | 174 | @Override 175 | public float getUntransformedCentroidY() { 176 | return currentCentroidY; 177 | } 178 | 179 | /** 180 | * Calculates the average span of all the active pointers in the given motion event. 181 | *

182 | * The average span is twice the average distance of all pointers to the given centroid. 183 | */ 184 | private float calculateAverageSpan(MotionEvent event, float centroidX, float centroidY) { 185 | int action = MotionEventCompat.getActionMasked(event); 186 | int index = MotionEventCompat.getActionIndex(event); 187 | 188 | float sum = 0; 189 | int num = 0; 190 | for (int i = 0, count = event.getPointerCount(); i < count; i++) { 191 | if (action == MotionEvent.ACTION_POINTER_UP && index == i) { 192 | continue; 193 | } 194 | 195 | sum += calculateDistance(event, i, centroidX, centroidY); 196 | num++; 197 | } 198 | 199 | float averageDistance = sum / num; 200 | return averageDistance * 2; 201 | } 202 | 203 | /** 204 | * Calculates the distance between the pointer given by the pointer index and the given 205 | * centroid. 206 | */ 207 | private float calculateDistance( 208 | MotionEvent event, int pointerIndex, float centroidX, float centroidY) { 209 | PointF untransformedPoint = calculateUntransformedPoint(event, pointerIndex); 210 | 211 | return dist(centroidX, centroidY, untransformedPoint.x, untransformedPoint.y); 212 | } 213 | 214 | @VisibleForTesting 215 | static float dist(float x0, float y0, float x1, float y1) { 216 | float dx = x1 - x0; 217 | float dy = y1 - y0; 218 | return (float) Math.sqrt(dx * dx + dy * dy); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/ValueVelocityTracker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.content.Context; 19 | import android.support.annotation.IntDef; 20 | import android.support.annotation.Nullable; 21 | import android.support.v4.view.MotionEventCompat; 22 | import android.view.MotionEvent; 23 | import android.view.VelocityTracker; 24 | import android.view.ViewConfiguration; 25 | 26 | import java.lang.annotation.Retention; 27 | import java.lang.annotation.RetentionPolicy; 28 | 29 | /** 30 | * A velocity tracker for any arbitrary value. Uses a {@link VelocityTracker} under the hood which 31 | * is fed specially crafted {@link MotionEvent}s. 32 | */ 33 | class ValueVelocityTracker { 34 | 35 | /** 36 | * A type of value that is accumulated as a additive sum. 37 | */ 38 | public static final int ADDITIVE = 0; 39 | 40 | /** 41 | * A type of value that is accumulated as a multiplicative product. 42 | */ 43 | public static final int MULTIPLICATIVE = 1; 44 | 45 | /** 46 | * A type that describes how a value is accumulated. 47 | */ 48 | @IntDef({ADDITIVE, MULTIPLICATIVE}) 49 | @Retention(RetentionPolicy.SOURCE) 50 | public @interface AccumulationType { 51 | 52 | } 53 | 54 | private static final int PIXELS_PER_SECOND = 1000; 55 | private static final float DONT_CARE = 0f; 56 | 57 | private final float maximumFlingVelocity; 58 | @AccumulationType 59 | private final int type; 60 | 61 | @Nullable 62 | private VelocityTracker velocityTracker; 63 | private float adjust; 64 | private float currentVelocity; 65 | 66 | public ValueVelocityTracker(Context context, @AccumulationType int type) { 67 | this.maximumFlingVelocity = ViewConfiguration.get(context).getScaledMaximumFlingVelocity(); 68 | this.type = type; 69 | } 70 | 71 | /** 72 | * Returns the velocity calculated in the most recent {@link #onGestureEnd(MotionEvent, 73 | * float)}. 74 | */ 75 | public float getCurrentVelocity() { 76 | return currentVelocity; 77 | } 78 | 79 | /** 80 | * Processes the start of a gesture. 81 | *

82 | * Must be balanced with a call to {@link #onGestureEnd(MotionEvent, float)} to end the 83 | * gesture. 84 | */ 85 | public void onGestureStart(MotionEvent event, float value) { 86 | velocityTracker = VelocityTracker.obtain(); 87 | if (type == ADDITIVE) { 88 | adjust = 0f; 89 | } else { 90 | adjust = 1f; 91 | } 92 | currentVelocity = 0f; 93 | 94 | addValueMovement(event, value); 95 | } 96 | 97 | /** 98 | * Processes the adjustment of a gesture. Call this if you do not want the value to jump 99 | * discontinuously on additional fingers entering and exiting the gesture. 100 | *

101 | * May be called multiple times during a gesture. 102 | */ 103 | public void onGestureAdjust(float adjust) { 104 | this.adjust = adjust; 105 | } 106 | 107 | /** 108 | * Processes the movement of a gesture. 109 | *

110 | * May be called multiple times during a gesture. 111 | */ 112 | public void onGestureMove(MotionEvent event, float value) { 113 | addValueMovement(event, value); 114 | } 115 | 116 | /** 117 | * Processes the end of a gesture. 118 | *

119 | * Must be balanced with a previous call to {@link #onGestureStart(MotionEvent, float)}. 120 | */ 121 | public void onGestureEnd(MotionEvent event, float value) { 122 | if (velocityTracker == null) { 123 | return; 124 | } 125 | 126 | addValueMovement(event, value); 127 | 128 | velocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, maximumFlingVelocity); 129 | currentVelocity = velocityTracker.getXVelocity(); 130 | 131 | velocityTracker.recycle(); 132 | velocityTracker = null; 133 | } 134 | 135 | private void addValueMovement(MotionEvent event, float value) { 136 | if (velocityTracker == null) { 137 | return; 138 | } 139 | 140 | int valueMovementAction; 141 | 142 | int action = MotionEventCompat.getActionMasked(event); 143 | switch (action) { 144 | case MotionEvent.ACTION_DOWN: 145 | case MotionEvent.ACTION_MOVE: 146 | case MotionEvent.ACTION_UP: 147 | case MotionEvent.ACTION_CANCEL: 148 | valueMovementAction = action; 149 | break; 150 | case MotionEvent.ACTION_POINTER_DOWN: 151 | valueMovementAction = MotionEvent.ACTION_DOWN; 152 | break; 153 | case MotionEvent.ACTION_POINTER_UP: 154 | valueMovementAction = MotionEvent.ACTION_UP; 155 | break; 156 | default: 157 | throw new IllegalArgumentException("Unexpected action for event: " + event); 158 | } 159 | velocityTracker.addMovement( 160 | MotionEvent.obtain( 161 | event.getDownTime(), 162 | event.getEventTime(), 163 | valueMovementAction, 164 | apply(value, adjust), 165 | DONT_CARE, 166 | event.getMetaState())); 167 | } 168 | 169 | private float apply(float value, float adjust) { 170 | if (type == ADDITIVE) { 171 | return value + adjust; 172 | } else { 173 | return value * adjust; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /library/src/main/java/com/google/android/material/motion/gestures/testing/SimulatedGestureRecognizer.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures.testing; 17 | 18 | import android.graphics.PointF; 19 | import android.view.MotionEvent; 20 | import android.view.View; 21 | 22 | import com.google.android.material.motion.gestures.GestureRecognizer; 23 | 24 | /** 25 | * A no-op gesture recognizer for testing that exposes {@link #setState(int)}. 26 | */ 27 | public class SimulatedGestureRecognizer extends GestureRecognizer { 28 | 29 | private float untransformedCentroidX; 30 | private float untransformedCentroidY; 31 | 32 | public SimulatedGestureRecognizer(View element) { 33 | setElement(element); 34 | } 35 | 36 | @Override 37 | public void setState(@GestureRecognizerState int state) { 38 | super.setState(state); 39 | } 40 | 41 | public void setCentroid(float x, float y) { 42 | PointF centroid = 43 | calculateUntransformedCentroid(MotionEvent.obtain(0, 0, MotionEvent.ACTION_MOVE, x, y, 0)); 44 | untransformedCentroidX = centroid.x; 45 | untransformedCentroidY = centroid.y; 46 | 47 | setState(CHANGED); 48 | } 49 | 50 | @Override 51 | protected boolean onTouch(MotionEvent event) { 52 | return false; 53 | } 54 | 55 | @Override 56 | public float getUntransformedCentroidX() { 57 | return untransformedCentroidX; 58 | } 59 | 60 | @Override 61 | public float getUntransformedCentroidY() { 62 | return untransformedCentroidY; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/DragGestureRecognizerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.Robolectric; 27 | import org.robolectric.RobolectricTestRunner; 28 | import org.robolectric.annotation.Config; 29 | 30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN; 31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED; 32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED; 33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE; 34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED; 35 | import static com.google.common.truth.Truth.assertThat; 36 | import static org.mockito.Mockito.mock; 37 | import static org.mockito.Mockito.when; 38 | 39 | @RunWith(RobolectricTestRunner.class) 40 | @Config(constants = BuildConfig.class, sdk = 21) 41 | public class DragGestureRecognizerTests { 42 | 43 | private static final float E = 0.0001f; 44 | 45 | private View element; 46 | private DragGestureRecognizer dragGestureRecognizer; 47 | 48 | private long eventDownTime; 49 | private long eventTime; 50 | 51 | @Before 52 | public void setUp() { 53 | Context context = Robolectric.setupActivity(Activity.class); 54 | element = new View(context); 55 | dragGestureRecognizer = new DragGestureRecognizer(); 56 | dragGestureRecognizer.dragSlop = 0; 57 | 58 | eventDownTime = 0; 59 | eventTime = -16; 60 | } 61 | 62 | @Test 63 | public void defaultState() { 64 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 65 | assertThat(dragGestureRecognizer.getElement()).isEqualTo(null); 66 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f); 67 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f); 68 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(0).of(0f); 69 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(0).of(0f); 70 | assertThat(dragGestureRecognizer.getVelocityX()).isWithin(0).of(0f); 71 | assertThat(dragGestureRecognizer.getVelocityY()).isWithin(0).of(0f); 72 | } 73 | 74 | @Test 75 | public void smallMovementIsNotRecognized() { 76 | dragGestureRecognizer.dragSlop = 24; 77 | 78 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 79 | dragGestureRecognizer.addStateChangeListener(listener); 80 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 81 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 82 | 83 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 84 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 86 | 87 | // Move 1 pixel. Should not change the state. 88 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 1, 0)); 89 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 91 | } 92 | 93 | @Test 94 | public void largeHorizontalMovementIsRecognized() { 95 | dragGestureRecognizer.dragSlop = 24; 96 | 97 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 98 | dragGestureRecognizer.addStateChangeListener(listener); 99 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 100 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 101 | 102 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 103 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 104 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 105 | 106 | // Move 100 pixel right. Should change the state. 107 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0)); 108 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED); 109 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 110 | 111 | // Move 1 pixel. Should still change the state. 112 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 101, 0)); 113 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED); 114 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 115 | } 116 | 117 | @Test 118 | public void largeVerticalMovementIsRecognized() { 119 | dragGestureRecognizer.dragSlop = 24; 120 | 121 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 122 | dragGestureRecognizer.addStateChangeListener(listener); 123 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 124 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 125 | 126 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 127 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 128 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 129 | 130 | // Move 100 pixel right. Should change the state. 131 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 0, 100)); 132 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED); 133 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 134 | 135 | // Move 1 pixel. Should still change the state. 136 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 0, 101)); 137 | assertThat(dragGestureRecognizer.getState()).isEqualTo(CHANGED); 138 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 139 | } 140 | 141 | @Test 142 | public void completedGestureIsRecognized() { 143 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 144 | dragGestureRecognizer.addStateChangeListener(listener); 145 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 146 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0)); 147 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 100, 0)); 148 | 149 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 150 | assertThat(listener.states.toArray()) 151 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 152 | } 153 | 154 | @Test 155 | public void cancelledGestureIsNotRecognized() { 156 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 157 | dragGestureRecognizer.addStateChangeListener(listener); 158 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 159 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0)); 160 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0)); 161 | 162 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 163 | assertThat(listener.states.toArray()) 164 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE}); 165 | } 166 | 167 | @Test 168 | public void noMovementIsNotRecognized() { 169 | dragGestureRecognizer.dragSlop = 24; 170 | 171 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 172 | dragGestureRecognizer.addStateChangeListener(listener); 173 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 174 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 175 | 176 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 177 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 178 | } 179 | 180 | @Test 181 | public void irrelevantMotionIsIgnored() { 182 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 183 | dragGestureRecognizer.addStateChangeListener(listener); 184 | 185 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0)); 186 | 187 | assertThat(dragGestureRecognizer.getState()).isEqualTo(POSSIBLE); 188 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 189 | } 190 | 191 | @Test 192 | public void multitouchHasCorrectCentroidAndTranslation() { 193 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 194 | dragGestureRecognizer.addStateChangeListener(listener); 195 | 196 | // First finger down. Centroid is at finger location and translation is 0. 197 | dragGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 198 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 199 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 200 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0); 201 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0); 202 | 203 | // Second finger down. Centroid is in between fingers and translation is 0. 204 | dragGestureRecognizer.onTouch(element, 205 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 206 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50); 207 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50); 208 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0); 209 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0); 210 | 211 | // Second finger moves [dx, dy]. Centroid and translation moves [dx/2, dy/2]. 212 | float dx = 505; 213 | float dy = 507; 214 | dragGestureRecognizer.onTouch(element, 215 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy)); 216 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2); 217 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2); 218 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(dx / 2); 219 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(dy / 2); 220 | 221 | // Second finger up. Centroid is at first finger location and translation stays the same. 222 | dragGestureRecognizer.onTouch(element, 223 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy)); 224 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 225 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 226 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(dx / 2); 227 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(dy / 2); 228 | 229 | // Finger up. Centroid is at first finger location and translation is reset. 230 | dragGestureRecognizer.onTouch(element, 231 | createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 232 | assertThat(dragGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 233 | assertThat(dragGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 234 | assertThat(dragGestureRecognizer.getTranslationX()).isWithin(E).of(0); 235 | assertThat(dragGestureRecognizer.getTranslationY()).isWithin(E).of(0); 236 | 237 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 238 | } 239 | 240 | @Test(expected = NullPointerException.class) 241 | public void crashesForNullElement() { 242 | dragGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 243 | } 244 | 245 | @Test 246 | public void allowsSettingElementAgain() { 247 | dragGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 248 | dragGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 249 | } 250 | 251 | private MotionEvent createMotionEvent(int action, float x, float y) { 252 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0); 253 | } 254 | 255 | private MotionEvent createMultiTouchMotionEvent( 256 | int action, int index, float x0, float y0, float x1, float y1) { 257 | MotionEvent event = mock(MotionEvent.class); 258 | 259 | when(event.getDownTime()).thenReturn(eventDownTime); 260 | when(event.getEventTime()).thenReturn(eventTime += 16); 261 | 262 | when(event.getPointerCount()).thenReturn(2); 263 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 264 | when(event.getActionMasked()).thenReturn(action); 265 | when(event.getActionIndex()).thenReturn(index); 266 | 267 | when(event.getRawX()).thenReturn(x0); 268 | when(event.getRawY()).thenReturn(y0); 269 | 270 | when(event.getX(0)).thenReturn(x0); 271 | when(event.getY(0)).thenReturn(y0); 272 | 273 | when(event.getX(1)).thenReturn(x1); 274 | when(event.getY(1)).thenReturn(y1); 275 | 276 | return event; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/GestureRecognizerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.app.Activity; 19 | import android.view.View; 20 | 21 | import com.google.android.material.motion.gestures.testing.SimulatedGestureRecognizer; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.Robolectric; 27 | import org.robolectric.RobolectricTestRunner; 28 | import org.robolectric.annotation.Config; 29 | 30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN; 31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED; 32 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE; 33 | import static com.google.common.truth.Truth.assertThat; 34 | 35 | @RunWith(RobolectricTestRunner.class) 36 | @Config(constants = BuildConfig.class, sdk = 21) 37 | public class GestureRecognizerTests { 38 | 39 | private SimulatedGestureRecognizer gestureRecognizer; 40 | 41 | @Before 42 | public void setUp() { 43 | View element = new View(Robolectric.setupActivity(Activity.class)); 44 | gestureRecognizer = new SimulatedGestureRecognizer(element); 45 | } 46 | 47 | @Test 48 | public void removedListenerDoesNotGetEvents() { 49 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 50 | gestureRecognizer.addStateChangeListener(listener); 51 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 52 | 53 | gestureRecognizer.setState(BEGAN); 54 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN}); 55 | 56 | gestureRecognizer.removeStateChangeListener(listener); 57 | gestureRecognizer.setState(CHANGED); 58 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN}); 59 | 60 | } 61 | 62 | @Test 63 | public void addingSameListenerTwiceDoesNotSendTwoEvents() { 64 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 65 | 66 | gestureRecognizer.addStateChangeListener(listener); 67 | gestureRecognizer.addStateChangeListener(listener); 68 | 69 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 70 | 71 | gestureRecognizer.setState(BEGAN); 72 | 73 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN}); 74 | } 75 | 76 | @Test 77 | public void canSetNullElement() { 78 | gestureRecognizer.setElement(null); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/RotateGestureRecognizerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.Robolectric; 27 | import org.robolectric.RobolectricTestRunner; 28 | import org.robolectric.annotation.Config; 29 | 30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN; 31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED; 32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED; 33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE; 34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED; 35 | import static com.google.android.material.motion.gestures.RotateGestureRecognizer.angle; 36 | import static com.google.common.truth.Truth.assertThat; 37 | import static org.mockito.Mockito.mock; 38 | import static org.mockito.Mockito.when; 39 | 40 | @RunWith(RobolectricTestRunner.class) 41 | @Config(constants = BuildConfig.class, sdk = 21) 42 | public class RotateGestureRecognizerTests { 43 | 44 | private static final float E = 0.0001f; 45 | 46 | private View element; 47 | private RotateGestureRecognizer rotateGestureRecognizer; 48 | 49 | private long eventDownTime; 50 | private long eventTime; 51 | 52 | @Before 53 | public void setUp() { 54 | Context context = Robolectric.setupActivity(Activity.class); 55 | element = new View(context); 56 | rotateGestureRecognizer = new RotateGestureRecognizer(); 57 | rotateGestureRecognizer.rotateSlop = 0; 58 | 59 | eventDownTime = 0; 60 | eventTime = -16; 61 | } 62 | 63 | @Test 64 | public void defaultState() { 65 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 66 | assertThat(rotateGestureRecognizer.getElement()).isEqualTo(null); 67 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f); 68 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f); 69 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(0).of(0f); 70 | assertThat(rotateGestureRecognizer.getVelocity()).isWithin(0).of(0f); 71 | } 72 | 73 | @Test 74 | public void smallMovementIsNotRecognized() { 75 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees. 76 | 77 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 78 | rotateGestureRecognizer.addStateChangeListener(listener); 79 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 80 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 81 | 82 | // First finger down. 83 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 84 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 86 | 87 | // Second finger down. 88 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 89 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 91 | 92 | // Move second finger up less than 45 degrees. Should not change the state. 93 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 99)); 94 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 95 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 96 | } 97 | 98 | @Test 99 | public void largeCounterClockwiseMovementIsRecognized() { 100 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees. 101 | 102 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 103 | rotateGestureRecognizer.addStateChangeListener(listener); 104 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 105 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 106 | 107 | // First finger down. 108 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 109 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 110 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 111 | 112 | // Second finger down. 113 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 114 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 115 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 116 | 117 | // Move second finger up more than 45 degrees. Should change the state. 118 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 101)); 119 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED); 120 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 121 | 122 | // Move second finger 1 pixel. Should still change the state. 123 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 102)); 124 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED); 125 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 126 | } 127 | 128 | @Test 129 | public void largeClockwiseMovementIsRecognized() { 130 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees. 131 | 132 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 133 | rotateGestureRecognizer.addStateChangeListener(listener); 134 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 135 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 136 | 137 | // First finger down. 138 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 139 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 140 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 141 | 142 | // Second finger down. 143 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 144 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 145 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 146 | 147 | // Move second finger down more than 45 degrees. Should change the state. 148 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, -101)); 149 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED); 150 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 151 | 152 | // Move second finger 1 pixel. Should still change the state. 153 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, -102)); 154 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(CHANGED); 155 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 156 | } 157 | 158 | @Test 159 | public void completedGestureIsRecognized() { 160 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 161 | rotateGestureRecognizer.addStateChangeListener(listener); 162 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 163 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 164 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 100, 200, 200)); 165 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100, 200, 200)); 166 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 2, 0, 0, 200, 100, 200, 200)); 167 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 200, 100)); 168 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 169 | 170 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 171 | assertThat(listener.states.toArray()) 172 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 173 | } 174 | 175 | @Test 176 | public void cancelledOneFingerGestureIsNotRecognized() { 177 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 178 | rotateGestureRecognizer.addStateChangeListener(listener); 179 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 180 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0)); 181 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0)); 182 | 183 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 184 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 185 | } 186 | 187 | @Test 188 | public void cancelledTwoFingerGestureIsNotRecognized() { 189 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 190 | rotateGestureRecognizer.addStateChangeListener(listener); 191 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 192 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 193 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100)); 194 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_CANCEL, 0, 0, 0, 200, 100)); 195 | 196 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 197 | assertThat(listener.states.toArray()) 198 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE}); 199 | } 200 | 201 | @Test 202 | public void noMovementIsNotRecognized() { 203 | rotateGestureRecognizer.rotateSlop = (float) (Math.PI / 4); // 45 degrees. 204 | 205 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 206 | rotateGestureRecognizer.addStateChangeListener(listener); 207 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 208 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 209 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100)); 210 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 211 | 212 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 213 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 214 | } 215 | 216 | @Test 217 | public void irrelevantMotionIsIgnored() { 218 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 219 | rotateGestureRecognizer.addStateChangeListener(listener); 220 | 221 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0)); 222 | 223 | assertThat(rotateGestureRecognizer.getState()).isEqualTo(POSSIBLE); 224 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 225 | } 226 | 227 | @Test 228 | public void oneFingerDoesNotAffectRotate() { 229 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 230 | rotateGestureRecognizer.addStateChangeListener(listener); 231 | 232 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 233 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0f); 234 | 235 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 100)); 236 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0f); 237 | } 238 | 239 | @Test 240 | public void multitouchHasCorrectCentroidAndRotation() { 241 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 242 | rotateGestureRecognizer.addStateChangeListener(listener); 243 | 244 | // First finger down. Centroid is at finger location and rotation is 0. 245 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 246 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 247 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 248 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 249 | 250 | // Second finger down. Centroid is in between fingers and rotation is 1. 251 | rotateGestureRecognizer.onTouch(element, 252 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 253 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50); 254 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50); 255 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 256 | 257 | // Second finger moves [dx, dy]. Centroid moves [dx/2, dy/2], rotation is calculated correctly. 258 | float dx = 5; 259 | float dy = 507; 260 | rotateGestureRecognizer.onTouch(element, 261 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy)); 262 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2); 263 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2); 264 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of( 265 | angle(0, 0, 100 + dx, 100 + dy) - angle(0, 0, 100, 100)); 266 | 267 | // Second finger up. State is now reset. 268 | rotateGestureRecognizer.onTouch(element, 269 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy)); 270 | assertThat(rotateGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 271 | assertThat(rotateGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 272 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 273 | 274 | assertThat(listener.states.toArray()).isEqualTo( 275 | new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 276 | } 277 | 278 | @Test 279 | public void thirdFingerDoesNotAffectRotation() { 280 | // First finger. 281 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 282 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 283 | 284 | // Second finger on horizontal line. 285 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 286 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 287 | 288 | // Third finger also on horizontal line. 289 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0)); 290 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 291 | 292 | // Move third finger. 293 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 2, 0, 0, 100, 0, 200, 200)); 294 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 295 | } 296 | 297 | @Test 298 | public void rotationIsStableOnFirstFingerUp() { 299 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 300 | rotateGestureRecognizer.addStateChangeListener(listener); 301 | 302 | // First finger down. 303 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 304 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 305 | 306 | // Second finger down on horizontal line. 307 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 308 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 309 | 310 | // Third finger also down on horizontal line. 311 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0)); 312 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 313 | 314 | // Move second finger 45 degrees. 315 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 100, 200, 0)); 316 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4)); 317 | 318 | // First finger up. Rotation stays the same. 319 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 0, 0, 0, 100, 100, 200, 0)); 320 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4)); 321 | } 322 | 323 | @Test 324 | public void rotationIsStableOnSecondFingerUp() { 325 | // First finger down. 326 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 327 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 328 | 329 | // Second finger down on horizontal line. 330 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 0)); 331 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 332 | 333 | // Third finger also down on horizontal line. 334 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 0, 200, 0)); 335 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of(0); 336 | 337 | // Move second finger 45 degrees. 338 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 100, 200, 0)); 339 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4)); 340 | 341 | // Second finger up. Rotation stays the same. 342 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100, 200, 0)); 343 | assertThat(rotateGestureRecognizer.getRotation()).isWithin(E).of((float) (Math.PI / 4)); 344 | } 345 | 346 | @Test 347 | public void nonZeroVelocity() { 348 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 349 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 10, 0)); 350 | 351 | float move = 0; 352 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10, 0 + (move += 10))); 353 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10, 0 + (move += 10))); 354 | 355 | rotateGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 10 + move, 0)); 356 | rotateGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 357 | 358 | assertThat(rotateGestureRecognizer.getVelocity()).isGreaterThan(0f); 359 | } 360 | 361 | @Test(expected = NullPointerException.class) 362 | public void crashesForNullElement() { 363 | rotateGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 364 | } 365 | 366 | @Test 367 | public void allowsSettingElementAgain() { 368 | rotateGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 369 | rotateGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 370 | } 371 | 372 | private MotionEvent createMotionEvent(int action, float x, float y) { 373 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0); 374 | } 375 | 376 | private MotionEvent createMultiTouchMotionEvent( 377 | int action, int index, float x0, float y0, float x1, float y1) { 378 | MotionEvent event = mock(MotionEvent.class); 379 | 380 | when(event.getDownTime()).thenReturn(eventDownTime); 381 | when(event.getEventTime()).thenReturn(eventTime += 16); 382 | 383 | when(event.getPointerCount()).thenReturn(2); 384 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 385 | when(event.getActionMasked()).thenReturn(action); 386 | when(event.getActionIndex()).thenReturn(index); 387 | 388 | when(event.getRawX()).thenReturn(x0); 389 | when(event.getRawY()).thenReturn(y0); 390 | 391 | when(event.getX(0)).thenReturn(x0); 392 | when(event.getY(0)).thenReturn(y0); 393 | 394 | when(event.getX(1)).thenReturn(x1); 395 | when(event.getY(1)).thenReturn(y1); 396 | 397 | return event; 398 | } 399 | 400 | private MotionEvent createMultiTouchMotionEvent( 401 | int action, int index, float x0, float y0, float x1, float y1, float x2, float y2) { 402 | MotionEvent event = mock(MotionEvent.class); 403 | 404 | when(event.getDownTime()).thenReturn(eventDownTime); 405 | when(event.getEventTime()).thenReturn(eventTime += 16); 406 | 407 | when(event.getPointerCount()).thenReturn(3); 408 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 409 | when(event.getActionMasked()).thenReturn(action); 410 | when(event.getActionIndex()).thenReturn(index); 411 | 412 | when(event.getRawX()).thenReturn(x0); 413 | when(event.getRawY()).thenReturn(y0); 414 | 415 | when(event.getX(0)).thenReturn(x0); 416 | when(event.getY(0)).thenReturn(y0); 417 | 418 | when(event.getX(1)).thenReturn(x1); 419 | when(event.getY(1)).thenReturn(y1); 420 | 421 | when(event.getX(2)).thenReturn(x2); 422 | when(event.getY(2)).thenReturn(y2); 423 | 424 | return event; 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/ScaleGestureRecognizerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.robolectric.Robolectric; 27 | import org.robolectric.RobolectricTestRunner; 28 | import org.robolectric.annotation.Config; 29 | 30 | import static com.google.android.material.motion.gestures.GestureRecognizer.BEGAN; 31 | import static com.google.android.material.motion.gestures.GestureRecognizer.CANCELLED; 32 | import static com.google.android.material.motion.gestures.GestureRecognizer.CHANGED; 33 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE; 34 | import static com.google.android.material.motion.gestures.GestureRecognizer.RECOGNIZED; 35 | import static com.google.android.material.motion.gestures.ScaleGestureRecognizer.dist; 36 | import static com.google.common.truth.Truth.assertThat; 37 | import static org.mockito.Mockito.mock; 38 | import static org.mockito.Mockito.when; 39 | 40 | @RunWith(RobolectricTestRunner.class) 41 | @Config(constants = BuildConfig.class, sdk = 21) 42 | public class ScaleGestureRecognizerTests { 43 | 44 | private static final float E = 0.0001f; 45 | 46 | private View element; 47 | private ScaleGestureRecognizer scaleGestureRecognizer; 48 | 49 | private long eventDownTime; 50 | private long eventTime; 51 | 52 | @Before 53 | public void setUp() { 54 | Context context = Robolectric.setupActivity(Activity.class); 55 | element = new View(context); 56 | scaleGestureRecognizer = new ScaleGestureRecognizer(); 57 | scaleGestureRecognizer.scaleSlop = 0; 58 | 59 | eventDownTime = 0; 60 | eventTime = -16; 61 | } 62 | 63 | @Test 64 | public void defaultState() { 65 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 66 | assertThat(scaleGestureRecognizer.getElement()).isEqualTo(null); 67 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(0).of(0f); 68 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(0).of(0f); 69 | assertThat(scaleGestureRecognizer.getScale()).isWithin(0).of(1f); 70 | assertThat(scaleGestureRecognizer.getVelocity()).isWithin(0).of(0f); 71 | } 72 | 73 | @Test 74 | public void smallMovementIsNotRecognized() { 75 | scaleGestureRecognizer.scaleSlop = 24; 76 | 77 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 78 | scaleGestureRecognizer.addStateChangeListener(listener); 79 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 80 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 81 | 82 | // First finger down. 83 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 84 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 85 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 86 | 87 | // Second finger down. 88 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 89 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 90 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 91 | 92 | // Move second finger 1 pixel. Should not change the state. 93 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 101, 100)); 94 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 95 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 96 | } 97 | 98 | @Test 99 | public void largeHorizontalMovementIsRecognized() { 100 | scaleGestureRecognizer.scaleSlop = 24; 101 | 102 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 103 | scaleGestureRecognizer.addStateChangeListener(listener); 104 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 105 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 106 | 107 | // First finger down. 108 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 109 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 110 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 111 | 112 | // Second finger down. 113 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 114 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 115 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 116 | 117 | // Move second finger 100 pixel right. Should change the state. 118 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100)); 119 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED); 120 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 121 | 122 | // Move second finger 1 pixel. Should still change the state. 123 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 201, 100)); 124 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED); 125 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 126 | } 127 | 128 | @Test 129 | public void largeVerticalMovementIsRecognized() { 130 | scaleGestureRecognizer.scaleSlop = 24; 131 | 132 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 133 | scaleGestureRecognizer.addStateChangeListener(listener); 134 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 135 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 136 | 137 | // First finger down. 138 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 139 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 140 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 141 | 142 | // Second finger down. 143 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 144 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 145 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 146 | 147 | // Move second finger 100 pixel down. Should change the state. 148 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 200)); 149 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED); 150 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED}); 151 | 152 | // Move second finger 1 pixel. Should still change the state. 153 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100, 201)); 154 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(CHANGED); 155 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CHANGED}); 156 | } 157 | 158 | @Test 159 | public void completedGestureIsRecognized() { 160 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 161 | scaleGestureRecognizer.addStateChangeListener(listener); 162 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 163 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 164 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 2, 0, 0, 100, 100, 200, 200)); 165 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100, 200, 200)); 166 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 2, 0, 0, 200, 100, 200, 200)); 167 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 200, 100)); 168 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 169 | 170 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 171 | assertThat(listener.states.toArray()) 172 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 173 | } 174 | 175 | @Test 176 | public void cancelledOneFingerGestureIsNotRecognized() { 177 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 178 | scaleGestureRecognizer.addStateChangeListener(listener); 179 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 180 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 0)); 181 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_CANCEL, 100, 0)); 182 | 183 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 184 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 185 | } 186 | 187 | @Test 188 | public void cancelledTwoFingerGestureIsNotRecognized() { 189 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 190 | scaleGestureRecognizer.addStateChangeListener(listener); 191 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 192 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 193 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 200, 100)); 194 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_CANCEL, 0, 0, 0, 200, 100)); 195 | 196 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 197 | assertThat(listener.states.toArray()) 198 | .isEqualTo(new Integer[]{POSSIBLE, BEGAN, CHANGED, CANCELLED, POSSIBLE}); 199 | } 200 | 201 | @Test 202 | public void noMovementIsNotRecognized() { 203 | scaleGestureRecognizer.scaleSlop = 24; 204 | 205 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 206 | scaleGestureRecognizer.addStateChangeListener(listener); 207 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 208 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 209 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100, 100)); 210 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 211 | 212 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 213 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 214 | } 215 | 216 | @Test 217 | public void irrelevantMotionIsIgnored() { 218 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 219 | scaleGestureRecognizer.addStateChangeListener(listener); 220 | 221 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_HOVER_MOVE, 0, 0)); 222 | 223 | assertThat(scaleGestureRecognizer.getState()).isEqualTo(POSSIBLE); 224 | assertThat(listener.states.toArray()).isEqualTo(new Integer[]{POSSIBLE}); 225 | } 226 | 227 | @Test 228 | public void oneFingerDoesNotAffectScale() { 229 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 230 | scaleGestureRecognizer.addStateChangeListener(listener); 231 | 232 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 233 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1f); 234 | 235 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_MOVE, 100, 100)); 236 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1f); 237 | } 238 | 239 | @Test 240 | public void multitouchHasCorrectCentroidAndScale() { 241 | TrackingGestureStateChangeListener listener = new TrackingGestureStateChangeListener(); 242 | scaleGestureRecognizer.addStateChangeListener(listener); 243 | 244 | // First finger down. Centroid is at finger location and scale is 1. 245 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 246 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 247 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 248 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1); 249 | 250 | // Second finger down. Centroid is in between fingers and scale is 1. 251 | scaleGestureRecognizer.onTouch(element, 252 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 100, 100)); 253 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50); 254 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50); 255 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1); 256 | 257 | // Second finger moves [dx, dy]. Centroid moves [dx/2, dy/2], scale is calculated correctly. 258 | float dx = 505; 259 | float dy = 507; 260 | scaleGestureRecognizer.onTouch(element, 261 | createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 100 + dx, 100 + dy)); 262 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(50 + dx / 2); 263 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(50 + dy / 2); 264 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of( 265 | dist(0, 0, 100 + dx, 100 + dy) / dist(0, 0, 100, 100)); 266 | 267 | // Second finger up. State is now reset. 268 | scaleGestureRecognizer.onTouch(element, 269 | createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 100 + dx, 100 + dy)); 270 | assertThat(scaleGestureRecognizer.getUntransformedCentroidX()).isWithin(E).of(0); 271 | assertThat(scaleGestureRecognizer.getUntransformedCentroidY()).isWithin(E).of(0); 272 | assertThat(scaleGestureRecognizer.getScale()).isWithin(E).of(1); 273 | 274 | assertThat(listener.states.toArray()).isEqualTo( 275 | new Integer[]{POSSIBLE, BEGAN, CHANGED, RECOGNIZED, POSSIBLE}); 276 | } 277 | 278 | @Test 279 | public void nonZeroVelocity() { 280 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 281 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_DOWN, 1, 0, 0, 10, 0)); 282 | 283 | float move = 0; 284 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10 + (move += 10), 0)); 285 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_MOVE, 1, 0, 0, 10 + (move += 10), 0)); 286 | 287 | scaleGestureRecognizer.onTouch(element, createMultiTouchMotionEvent(MotionEvent.ACTION_POINTER_UP, 1, 0, 0, 10 + move, 0)); 288 | scaleGestureRecognizer.onTouch(element, createMotionEvent(MotionEvent.ACTION_UP, 0, 0)); 289 | 290 | assertThat(scaleGestureRecognizer.getVelocity()).isGreaterThan(0f); 291 | } 292 | 293 | @Test(expected = NullPointerException.class) 294 | public void crashesForNullElement() { 295 | scaleGestureRecognizer.onTouch(null, createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 296 | } 297 | 298 | @Test 299 | public void allowsSettingElementAgain() { 300 | scaleGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 301 | scaleGestureRecognizer.onTouch(new View(element.getContext()), createMotionEvent(MotionEvent.ACTION_DOWN, 0, 0)); 302 | } 303 | 304 | private MotionEvent createMotionEvent(int action, float x, float y) { 305 | return MotionEvent.obtain(eventDownTime, eventTime += 16, action, x, y, 0); 306 | } 307 | 308 | private MotionEvent createMultiTouchMotionEvent( 309 | int action, int index, float x0, float y0, float x1, float y1) { 310 | MotionEvent event = mock(MotionEvent.class); 311 | 312 | when(event.getDownTime()).thenReturn(eventDownTime); 313 | when(event.getEventTime()).thenReturn(eventTime += 16); 314 | 315 | when(event.getPointerCount()).thenReturn(2); 316 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 317 | when(event.getActionMasked()).thenReturn(action); 318 | when(event.getActionIndex()).thenReturn(index); 319 | 320 | when(event.getRawX()).thenReturn(x0); 321 | when(event.getRawY()).thenReturn(y0); 322 | 323 | when(event.getX(0)).thenReturn(x0); 324 | when(event.getY(0)).thenReturn(y0); 325 | 326 | when(event.getX(1)).thenReturn(x1); 327 | when(event.getY(1)).thenReturn(y1); 328 | 329 | return event; 330 | } 331 | 332 | private MotionEvent createMultiTouchMotionEvent( 333 | int action, int index, float x0, float y0, float x1, float y1, float x2, float y2) { 334 | MotionEvent event = mock(MotionEvent.class); 335 | 336 | when(event.getDownTime()).thenReturn(eventDownTime); 337 | when(event.getEventTime()).thenReturn(eventTime += 16); 338 | 339 | when(event.getPointerCount()).thenReturn(3); 340 | when(event.getAction()).thenReturn(action | (index << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); 341 | when(event.getActionMasked()).thenReturn(action); 342 | when(event.getActionIndex()).thenReturn(index); 343 | 344 | when(event.getRawX()).thenReturn(x0); 345 | when(event.getRawY()).thenReturn(y0); 346 | 347 | when(event.getX(0)).thenReturn(x0); 348 | when(event.getY(0)).thenReturn(y0); 349 | 350 | when(event.getX(1)).thenReturn(x1); 351 | when(event.getY(1)).thenReturn(y1); 352 | 353 | when(event.getX(2)).thenReturn(x2); 354 | when(event.getY(2)).thenReturn(y2); 355 | 356 | return event; 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/TrackingGestureStateChangeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import com.google.android.material.motion.gestures.GestureRecognizer.GestureStateChangeListener; 19 | import com.google.common.collect.Lists; 20 | 21 | import java.util.List; 22 | 23 | import static com.google.android.material.motion.gestures.GestureRecognizer.POSSIBLE; 24 | 25 | /** 26 | * A GestureStateChangeListener that tracks the state changes. Useful for tests. 27 | */ 28 | public class TrackingGestureStateChangeListener implements GestureStateChangeListener { 29 | List states = Lists.newArrayList(POSSIBLE); 30 | 31 | @Override 32 | public void onStateChanged(GestureRecognizer gestureRecognizer) { 33 | states.add(gestureRecognizer.getState()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/ValueVelocityTrackerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures; 17 | 18 | import android.app.Activity; 19 | import android.content.Context; 20 | import android.view.MotionEvent; 21 | 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import org.robolectric.Robolectric; 26 | import org.robolectric.RobolectricTestRunner; 27 | import org.robolectric.annotation.Config; 28 | 29 | @RunWith(RobolectricTestRunner.class) 30 | @Config(constants = BuildConfig.class, sdk = 21) 31 | public class ValueVelocityTrackerTests { 32 | 33 | private ValueVelocityTracker velocityTracker; 34 | 35 | @Before 36 | public void setUp() { 37 | Context context = Robolectric.setupActivity(Activity.class); 38 | velocityTracker = new ValueVelocityTracker(context, ValueVelocityTracker.ADDITIVE); 39 | } 40 | 41 | @Test(expected = IllegalArgumentException.class) 42 | public void unexpectedMotionActionCrashes() { 43 | velocityTracker.onGestureStart( 44 | MotionEvent.obtain(0, 0, MotionEvent.ACTION_BUTTON_PRESS, 0, 0, 0), 0); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /library/src/test/java/com/google/android/material/motion/gestures/testing/SimulatedGestureRecognizerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures.testing; 17 | 18 | import android.app.Activity; 19 | import android.view.View; 20 | 21 | import com.google.android.material.motion.gestures.BuildConfig; 22 | import com.google.android.material.motion.gestures.GestureRecognizer; 23 | 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | import org.junit.runner.RunWith; 27 | import org.robolectric.Robolectric; 28 | import org.robolectric.RobolectricTestRunner; 29 | import org.robolectric.annotation.Config; 30 | 31 | import static com.google.common.truth.Truth.assertThat; 32 | 33 | @RunWith(RobolectricTestRunner.class) 34 | @Config(constants = BuildConfig.class, sdk = 21) 35 | public class SimulatedGestureRecognizerTests { 36 | 37 | private SimulatedGestureRecognizer gestureRecognizer; 38 | private View element; 39 | 40 | @Before 41 | public void setUp() { 42 | element = new View(Robolectric.setupActivity(Activity.class)); 43 | gestureRecognizer = new SimulatedGestureRecognizer(element); 44 | } 45 | 46 | @Test 47 | public void defaultState() { 48 | assertThat(gestureRecognizer.onTouch(element, null)).isFalse(); 49 | assertThat(gestureRecognizer.getUntransformedCentroidX()).isWithin(0f).of(0f); 50 | assertThat(gestureRecognizer.getUntransformedCentroidY()).isWithin(0f).of(0f); 51 | } 52 | 53 | @Test 54 | public void settableState() { 55 | assertThat(gestureRecognizer.getState()).isEqualTo(GestureRecognizer.POSSIBLE); 56 | 57 | gestureRecognizer.setState(GestureRecognizer.CHANGED); 58 | assertThat(gestureRecognizer.getState()).isEqualTo(GestureRecognizer.CHANGED); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /local-dependency-substitution.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * This Gradle script does 4 things that enable local dependencies. 19 | * 20 | * 1. Ensure config file exists. 21 | * 2. Substitute each dependencies declared in the file for local versions. 22 | * 3. Publish each local dependencies on every build. 23 | * 4. Alert the user. 24 | */ 25 | 26 | def config = new File('local.dependencies') 27 | def substitutions = [] as Set 28 | 29 | /* 1. Ensure local.dependencies exists */ 30 | 31 | config.exists() || config << new File('.local.dependencies.template').text 32 | 33 | /* 2. Substitute each dependencies declared in local.dependencies with the ":local" version. */ 34 | 35 | subprojects { 36 | repositories { 37 | mavenLocal() 38 | } 39 | 40 | def trim = { line -> 41 | def comment = line.indexOf('#') 42 | if (comment != -1) { 43 | line = line.substring(0, comment) 44 | } 45 | line.trim() 46 | } 47 | 48 | configurations.all { 49 | List localDependencies = ( 50 | config as String[] 51 | ).collect { 52 | def line = trim it 53 | if (line) { 54 | this.project.dependencies.create("$line:local") 55 | } 56 | }.findAll() 57 | 58 | // resolutionStrategy.dependencySubstitution.all { DependencySubstitution dependency -> 59 | // if (dependency.requested instanceof ModuleComponentSelector) { 60 | // ModuleComponentSelector requested = dependency.requested; 61 | // 62 | // Dependency local = localDependencies.find { 63 | // it.group == requested.group && it.name == requested.module 64 | // } 65 | // 66 | // if (local) { 67 | // substitutions << local 68 | // dependency.useTarget([ 69 | // group : local.group, 70 | // name : local.name, 71 | // version: local.version, 72 | // ]) 73 | // logger.info("Substituded local dependency for: $requested.") 74 | // } 75 | // } 76 | // } 77 | } 78 | } 79 | 80 | /* 3. Publish each local dependencies's changes to the local maven repository on every build. */ 81 | 82 | subprojects { 83 | afterEvaluate { 84 | preBuild.dependsOn installLocalDependencies 85 | } 86 | 87 | task installLocalDependencies << { 88 | configurations 89 | .collectMany { it.allDependencies } 90 | .unique() 91 | .each { Dependency dependency -> 92 | if (substitutions.find { it.group == dependency.group && it.name == dependency.name }) { 93 | logger.lifecycle("Installing $dependency.group:$dependency.name") 94 | logger.info( 95 | "Executing \"./install-local-dependency.sh $dependency.group $dependency.name\"") 96 | 97 | def log = new File("${getTemporaryDir()}/${dependency.group}/${dependency.name}.log") 98 | log.parentFile.mkdirs() 99 | def out = new FileOutputStream(log) 100 | logger.info("Streaming output to $log") 101 | 102 | def result = exec { 103 | commandLine './install-local-dependency.sh' 104 | args dependency.group, dependency.name 105 | standardOutput out 106 | errorOutput out 107 | ignoreExitValue true 108 | } 109 | 110 | if (result.exitValue != 0) { 111 | throw new TaskExecutionException(it, 112 | new Exception("Command './install-local-dependency.sh' failed. See $log for output.")) 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | /* 4. Alert the user that local dependencies are being used. */ 120 | 121 | gradle.buildFinished { 122 | if (substitutions) { 123 | logger.lifecycle('Applied {} local dependency substitution(s).', substitutions.size()) 124 | logger.info(substitutions.inject('') { acc, val -> 125 | acc << "\n\t* $val.group:$val.name" 126 | }.substring(1)) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion rootProject.ext.compileSdkVersion 5 | buildToolsVersion rootProject.ext.buildToolsVersion 6 | 7 | defaultConfig { 8 | applicationId "com.google.android.material.motion.gestures.sample" 9 | minSdkVersion rootProject.ext.minSdkVersion 10 | targetSdkVersion rootProject.ext.targetSdkVersion 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | 15 | compileOptions { 16 | sourceCompatibility JavaVersion.VERSION_1_8 17 | targetCompatibility JavaVersion.VERSION_1_8 18 | } 19 | 20 | lintOptions { 21 | abortOnError false 22 | 23 | // Disable gradle dependency check for new versions. 24 | // Many of these have been chosen to work with Google Tools. 25 | disable 'GradleDependency' 26 | } 27 | 28 | buildTypes { 29 | release { 30 | minifyEnabled true 31 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 32 | } 33 | } 34 | } 35 | 36 | dependencies { 37 | // If you are developing any dependencies locally, also list them in local.dependencies. 38 | compile project(':library') 39 | compile "com.android.support:appcompat-v7:$supportLibVersion" 40 | } 41 | -------------------------------------------------------------------------------- /sample/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 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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/java/com/google/android/material/motion/gestures/sample/CheckerboardDrawable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures.sample; 17 | 18 | import android.graphics.Canvas; 19 | import android.graphics.Color; 20 | import android.graphics.ColorFilter; 21 | import android.graphics.Paint; 22 | import android.graphics.PixelFormat; 23 | import android.graphics.Rect; 24 | import android.graphics.drawable.Drawable; 25 | 26 | /** 27 | * Draws a checkerboard pattern. 28 | */ 29 | public class CheckerboardDrawable extends Drawable { 30 | public static final int COLS = 10; 31 | public static final int ROWS = 10; 32 | private final Paint gridPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 33 | private final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); 34 | 35 | public CheckerboardDrawable() { 36 | gridPaint.setColor(Color.BLACK); 37 | backgroundPaint.setColor(Color.RED); 38 | } 39 | 40 | @Override 41 | public void draw(Canvas canvas) { 42 | Rect bounds = getBounds(); 43 | 44 | canvas.drawRect(bounds, backgroundPaint); 45 | 46 | float cellWidth = (float) bounds.width() / COLS; 47 | float cellHeight = (float) bounds.height() / ROWS; 48 | 49 | for (int i = 0, x = bounds.left; i < COLS + 1; i++, x += cellWidth) { 50 | if (i == 0 || i == COLS) { 51 | gridPaint.setStrokeWidth(10); 52 | } else { 53 | gridPaint.setStrokeWidth(1); 54 | } 55 | canvas.drawLine(x, bounds.top, x, bounds.bottom, gridPaint); 56 | } 57 | for (int i = 0, y = bounds.top; i < ROWS + 1; i++, y += cellHeight) { 58 | if (i == 0 || i == COLS) { 59 | gridPaint.setStrokeWidth(10); 60 | } else { 61 | gridPaint.setStrokeWidth(1); 62 | } 63 | canvas.drawLine(bounds.left, y, bounds.right, y, gridPaint); 64 | } 65 | } 66 | 67 | @Override 68 | public void setAlpha(int alpha) { 69 | gridPaint.setAlpha(alpha); 70 | invalidateSelf(); 71 | } 72 | 73 | @Override 74 | public void setColorFilter(ColorFilter colorFilter) { 75 | gridPaint.setColorFilter(colorFilter); 76 | invalidateSelf(); 77 | } 78 | 79 | @Override 80 | public int getOpacity() { 81 | return PixelFormat.TRANSLUCENT; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sample/src/main/java/com/google/android/material/motion/gestures/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016-present The Material Motion Authors. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.google.android.material.motion.gestures.sample; 17 | 18 | import android.os.Bundle; 19 | import android.support.v7.app.AppCompatActivity; 20 | import android.view.MotionEvent; 21 | import android.view.View; 22 | import android.view.View.OnTouchListener; 23 | import android.widget.TextView; 24 | 25 | import com.google.android.material.motion.gestures.DragGestureRecognizer; 26 | import com.google.android.material.motion.gestures.GestureRecognizer; 27 | import com.google.android.material.motion.gestures.GestureRecognizer.GestureStateChangeListener; 28 | import com.google.android.material.motion.gestures.RotateGestureRecognizer; 29 | import com.google.android.material.motion.gestures.ScaleGestureRecognizer; 30 | 31 | import java.util.Locale; 32 | 33 | /** 34 | * Gestures sample Activity. 35 | */ 36 | public class MainActivity extends AppCompatActivity { 37 | 38 | private final DragGestureRecognizer dragGestureRecognizer = new DragGestureRecognizer(); 39 | private final ScaleGestureRecognizer scaleGestureRecognizer = new ScaleGestureRecognizer(); 40 | private final RotateGestureRecognizer rotateGestureRecognizer = new RotateGestureRecognizer(); 41 | 42 | private TextView dragText; 43 | private TextView scaleText; 44 | private TextView rotateText; 45 | 46 | @Override 47 | protected void onCreate(Bundle savedInstanceState) { 48 | super.onCreate(savedInstanceState); 49 | 50 | setContentView(R.layout.main_activity); 51 | 52 | View target = findViewById(R.id.target); 53 | dragText = (TextView) findViewById(R.id.drag_text); 54 | scaleText = (TextView) findViewById(R.id.scale_text); 55 | rotateText = (TextView) findViewById(R.id.rotate_text); 56 | 57 | dragGestureRecognizer.addStateChangeListener(stateChangeListener); 58 | scaleGestureRecognizer.addStateChangeListener(stateChangeListener); 59 | rotateGestureRecognizer.addStateChangeListener(stateChangeListener); 60 | 61 | target.setOnTouchListener(onTouchListener); 62 | } 63 | 64 | private final OnTouchListener onTouchListener = new OnTouchListener() { 65 | @Override 66 | public boolean onTouch(View v, MotionEvent event) { 67 | boolean handled = false; 68 | handled |= dragGestureRecognizer.onTouch(v, event); 69 | handled |= scaleGestureRecognizer.onTouch(v, event); 70 | handled |= rotateGestureRecognizer.onTouch(v, event); 71 | return handled; 72 | } 73 | }; 74 | 75 | private final GestureStateChangeListener stateChangeListener = new GestureStateChangeListener() { 76 | @Override 77 | public void onStateChanged(GestureRecognizer gestureRecognizer) { 78 | CharSequence string = createDebugString(gestureRecognizer); 79 | 80 | if (gestureRecognizer == dragGestureRecognizer) { 81 | dragText.setText(string); 82 | } else if (gestureRecognizer == scaleGestureRecognizer) { 83 | scaleText.setText(string); 84 | } else if (gestureRecognizer == rotateGestureRecognizer) { 85 | rotateText.setText(string); 86 | } 87 | } 88 | }; 89 | 90 | private CharSequence createDebugString(GestureRecognizer gestureRecognizer) { 91 | if (gestureRecognizer == dragGestureRecognizer) { 92 | return String.format(Locale.getDefault(), 93 | "Drag\nstate: %d, tx: %.3f, ty: %.3f, cx: %.3f, cy: %.3f, vx: %.3f, vy: %.3f", 94 | dragGestureRecognizer.getState(), 95 | dragGestureRecognizer.getTranslationX(), 96 | dragGestureRecognizer.getTranslationY(), 97 | dragGestureRecognizer.getCentroidX(), 98 | dragGestureRecognizer.getCentroidY(), 99 | dragGestureRecognizer.getVelocityX(), 100 | dragGestureRecognizer.getVelocityY()); 101 | } else if (gestureRecognizer == scaleGestureRecognizer) { 102 | return String.format(Locale.getDefault(), 103 | "Scale\nstate: %d, s: %.3f, cx: %.3f, cy: %.3f, v: %.3f", 104 | scaleGestureRecognizer.getState(), 105 | scaleGestureRecognizer.getScale(), 106 | scaleGestureRecognizer.getCentroidX(), 107 | scaleGestureRecognizer.getCentroidY(), 108 | scaleGestureRecognizer.getVelocity()); 109 | } else if (gestureRecognizer == rotateGestureRecognizer) { 110 | return String.format(Locale.getDefault(), 111 | "Rotate\nstate: %d, r: %.3f, cx: %.3f, cy: %.3f, v: %.3f", 112 | rotateGestureRecognizer.getState(), 113 | rotateGestureRecognizer.getRotation(), 114 | rotateGestureRecognizer.getCentroidX(), 115 | rotateGestureRecognizer.getCentroidY(), 116 | rotateGestureRecognizer.getVelocity()); 117 | } 118 | return null; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 21 | 22 | 28 | 29 | 34 | 35 | 40 | 41 | 46 | 47 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/material-motion/gestures-android/e0cbcce1049ef959de378dca90a9bab4176dd79b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | Gestures Sample 19 | 20 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':library', ':sample' 2 | --------------------------------------------------------------------------------