├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── screenshot1.png ├── screenshot2.png └── video.gif ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── getkeepsafe │ │ └── taptargetviewsample │ │ ├── MainActivity.java │ │ └── SampleApplication.java │ └── res │ ├── drawable │ ├── ic_android_black_24dp.xml │ ├── ic_arrow_back_white_24dp.xml │ ├── ic_directions_car_black_24dp.xml │ ├── ic_more_vert_black_24dp.xml │ └── ic_search_white_24dp.xml │ ├── layout │ └── activity_main.xml │ ├── menu │ └── menu_main.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-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── taptargetview ├── build.gradle └── src └── main ├── AndroidManifest.xml └── java └── com └── getkeepsafe └── taptargetview ├── FloatValueAnimatorBuilder.java ├── ReflectUtil.java ├── TapTarget.java ├── TapTargetSequence.java ├── TapTargetView.java ├── ToolbarTapTarget.java ├── UiUtil.java ├── ViewTapTarget.java └── ViewUtil.java /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Guidelines for contributing 2 | 3 | Thank you for your interest in contributing! We're always happy to get the community involved in our projects, however we have just a couple rules that we enforce for any pull requests: 4 | 5 | - You must follow the code style already in place 6 | - You must use meaningful commit messages 7 | 8 | Any pull request that does not meet the above criteria will not be merged. 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] I have verified the issue exists on the latest version 2 | - [ ] I am able to reproduce it 3 | 4 | **Version used:** 5 | 6 | **Stack trace:** 7 | 8 | **Android version:** 9 | 10 | -------------------------------------------------------------------------------- /.github/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/.github/screenshot1.png -------------------------------------------------------------------------------- /.github/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/.github/screenshot2.png -------------------------------------------------------------------------------- /.github/video.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/.github/video.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea/ 5 | .DS_Store 6 | /build 7 | /*/build/ 8 | /captures 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 11 | ## [Unreleased] 12 | 13 | ## [1.15.0] - Released October 8, 2024 14 | - Added attribute for force centering tap targets (#409) 15 | 16 | ## [1.14.0] - Released August 16, 2024 17 | - Modernize project build files (#407) 18 | - Enable edge to edge on sample app (#407) 19 | - Add setDrawBehindStatusBar and setDrawBehindNavigationBar (#407) 20 | 21 | ## [1.13.3] - Released July 9, 2021 22 | - Removed JCenter dependencies and updated other build dependencies (#388) 23 | 24 | ## [1.13.2] - Released March 10, 2021 25 | - Moved artifact publishing from JCenter to Maven Central (#385) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Keepsafe Software Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Video 1 3 | Screenshot 1 4 | Screenshot 2
5 | 6 | TapTargetView 7 |

8 | 9 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.getkeepsafe.taptargetview/taptargetview/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.getkeepsafe.taptargetview/taptargetview) 10 | [![Release](https://img.shields.io/github/tag/KeepSafe/TapTargetView.svg?label=jitpack)](https://jitpack.io/#KeepSafe/TapTargetView) 11 | 12 | 13 | An implementation of tap targets from [Google's Material Design guidelines on feature discovery](https://material.io/archive/guidelines/growth-communications/feature-discovery.html). 14 | 15 | **Min SDK:** 14 16 | 17 | [JavaDoc](https://javadoc.jitpack.io/com/github/KeepSafe/TapTargetView/latest/javadoc/) 18 | 19 | ## Installation 20 | 21 | TapTargetView is distributed using [MavenCentral](https://search.maven.org/artifact/com.getkeepsafe.taptargetview/taptargetview). 22 | 23 | ```groovy 24 | repositories { 25 | mavenCentral() 26 | } 27 | 28 | dependencies { 29 | implementation 'com.getkeepsafe.taptargetview:taptargetview:x.x.x' 30 | } 31 | ``` 32 | 33 | If you wish, you may also use TapTargetView with [jitpack](https://jitpack.io/#KeepSafe/TapTargetView). 34 | For snapshots, please follow the instructions [here](https://jitpack.io/#KeepSafe/TapTargetView/-SNAPSHOT). 35 | 36 | ## Usage 37 | 38 | ### Simple usage 39 | 40 | ```java 41 | TapTargetView.showFor(this, // `this` is an Activity 42 | TapTarget.forView(findViewById(R.id.target), "This is a target", "We have the best targets, believe me") 43 | // All options below are optional 44 | .outerCircleColor(R.color.red) // Specify a color for the outer circle 45 | .outerCircleAlpha(0.96f) // Specify the alpha amount for the outer circle 46 | .targetCircleColor(R.color.white) // Specify a color for the target circle 47 | .titleTextSize(20) // Specify the size (in sp) of the title text 48 | .titleTextColor(R.color.white) // Specify the color of the title text 49 | .descriptionTextSize(10) // Specify the size (in sp) of the description text 50 | .descriptionTextColor(R.color.red) // Specify the color of the description text 51 | .textColor(R.color.blue) // Specify a color for both the title and description text 52 | .textTypeface(Typeface.SANS_SERIF) // Specify a typeface for the text 53 | .dimColor(R.color.black) // If set, will dim behind the view with 30% opacity of the given color 54 | .drawShadow(true) // Whether to draw a drop shadow or not 55 | .cancelable(false) // Whether tapping outside the outer circle dismisses the view 56 | .tintTarget(true) // Whether to tint the target view's color 57 | .transparentTarget(false) // Specify whether the target is transparent (displays the content underneath) 58 | .icon(Drawable) // Specify a custom drawable to draw as the target 59 | .targetRadius(60), // Specify the target radius (in dp) 60 | new TapTargetView.Listener() { // The listener can listen for regular clicks, long clicks or cancels 61 | @Override 62 | public void onTargetClick(TapTargetView view) { 63 | super.onTargetClick(view); // This call is optional 64 | doSomething(); 65 | } 66 | }); 67 | ``` 68 | 69 | You may also choose to target your own custom `Rect` with `TapTarget.forBounds(Rect, ...)` 70 | 71 | Additionally, each color can be specified via a `@ColorRes` or a `@ColorInt`. Functions that have the suffix `Int` take a `@ColorInt`. 72 | 73 | *Tip: When targeting a Toolbar item, be careful with Proguard and ensure you're keeping certain fields. See [#180](https://github.com/KeepSafe/TapTargetView/issues/180)* 74 | 75 | ### Sequences 76 | 77 | You can easily create a sequence of tap targets with `TapTargetSequence`: 78 | 79 | ```java 80 | new TapTargetSequence(this) 81 | .targets( 82 | TapTarget.forView(findViewById(R.id.never), "Gonna"), 83 | TapTarget.forView(findViewById(R.id.give), "You", "Up") 84 | .dimColor(android.R.color.never) 85 | .outerCircleColor(R.color.gonna) 86 | .targetCircleColor(R.color.let) 87 | .textColor(android.R.color.you), 88 | TapTarget.forBounds(rickTarget, "Down", ":^)") 89 | .cancelable(false) 90 | .icon(rick)) 91 | .listener(new TapTargetSequence.Listener() { 92 | // This listener will tell us when interesting(tm) events happen in regards 93 | // to the sequence 94 | @Override 95 | public void onSequenceFinish() { 96 | // Yay 97 | } 98 | 99 | @Override 100 | public void onSequenceStep(TapTarget lastTarget, boolean targetClicked) { 101 | // Perform action for the current target 102 | } 103 | 104 | @Override 105 | public void onSequenceCanceled(TapTarget lastTarget) { 106 | // Boo 107 | } 108 | }); 109 | ``` 110 | 111 | A sequence is started via a call to `start()` on the `TapTargetSequence` instance 112 | 113 | For more examples of usage, please look at the included sample app. 114 | 115 | ### Tutorials 116 | - [raywenderlich.com](https://www.raywenderlich.com/5194-taptargetview-for-android-tutorial) 117 | 118 | ## Third Party Bindings 119 | 120 | ### React Native 121 | Thanks to @prscX, you may now use this library with [React Native](https://github.com/facebook/react-native) via the module [here](https://github.com/prscX/react-native-taptargetview) 122 | 123 | ### NativeScript 124 | Thanks to @hamdiwanis, you may now use this library with [NativeScript](https://nativescript.org) via the plugin [here](https://github.com/hamdiwanis/nativescript-app-tour) 125 | 126 | ### Xamarin 127 | Thanks to @btripp, you may now use this library via a Xamarin Binding located [here](https://www.nuget.org/packages/Xamarin.TapTargetView). 128 | 129 | ## License 130 | 131 | Copyright 2016 Keepsafe Software Inc. 132 | 133 | Licensed under the Apache License, Version 2.0 (the "License"); 134 | you may not use this file except in compliance with the License. 135 | You may obtain a copy of the License at 136 | 137 | http://www.apache.org/licenses/LICENSE-2.0 138 | 139 | Unless required by applicable law or agreed to in writing, software 140 | distributed under the License is distributed on an "AS IS" BASIS, 141 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 142 | See the License for the specific language governing permissions and 143 | limitations under the License. 144 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | How To Release 2 | ============== 3 | 4 | Due to Maven Central's very particular requirements, the release process is a bit 5 | elaborate and requires a good deal of local configuration. This guide should walk 6 | you through it. It won't do anyone outside of KeepSafe any good, but the workflow 7 | is representative of just about any project deploying via Sonatype. 8 | 9 | We currently deploy to Maven Central (via Sonatype's OSS Nexus instance). 10 | 11 | ### Prerequisites 12 | 13 | 1. A *published* GPG code-signing key 14 | 1. A Sonatype Nexus OSS account with permission to publish in com.getkeepsafe 15 | 1. Permission to push directly to https://github.com/KeepSafe/TapTargetView 16 | 17 | ### Setup 18 | 19 | 1. Add your GPG key to your github profile - this is required 20 | for github to know that your commits and tags are "verified". 21 | 1. Configure your code-signing key in ~/.gradle/gradle.properties: 22 | ```gradle 23 | signing.keyId= 24 | signing.password= 25 | signing.secretKeyRingFile=/path/to/your/secring.gpg 26 | ``` 27 | 1. Create a token for your Sonatype credentials. [Source](https://community.sonatype.com/t/401-content-access-is-protected-by-token-authentication-failure-while-performing-maven-release/12741/4) 28 | ``` 29 | 1. Go to https://oss.sonatype.org/ and login 30 | 2. Go to profile 31 | 3. Change the pulldown from “Summary” to “User Token” 32 | 4. Click on “Access User Token” 33 | ``` 34 | 1. Configure your Sonatype credentials in ~/.gradle/gradle.properties: 35 | ```gradle 36 | mavenCentralUsername= 37 | mavenCentralPassword= 38 | SONATYPE_STAGING_PROFILE=com.getkeepsafe 39 | ``` 40 | 1. Configure git with your codesigning key; make sure it's the same as the one 41 | you use to sign binaries (i.e. it's the same one you added to gradle.properties): 42 | ```bash 43 | # Do this for the TapTargetView repo only 44 | git config user.email "your@email.com" 45 | git config user.signingKey "your-key-id" 46 | ``` 47 | 1. Add your GPG key to `~/.gnupg` 48 | 49 | ### Pushing a build 50 | 51 | 1. Edit gradle.properties, update the VERSION property for the new version release 52 | 1. Edit changelog, add relevant changes, note the date and new version (follow the existing pattern) 53 | 1. Add new `## [Unreleased]` header for next release 54 | 1. Verify that the everything works: 55 | ```bash 56 | ./gradlew clean check 57 | ``` 58 | 1. Make a *signed* commit: 59 | ```bash 60 | git commit -S -m "Release version X.Y.Z" 61 | ``` 62 | 1. Make a *signed* tag: 63 | ```bash 64 | git tag -s -a X.Y.Z 65 | ``` 66 | 1. Publish to Release: 67 | ```bash 68 | ./gradlew :taptargetview:publishAndReleaseToMavenCentral --no-configuration-cache 69 | ``` 70 | 1. Wait until that's done. It takes a while to publish and be available in [MavenCentral](https://repo.maven.apache.org/maven2/com/getkeepsafe/). Monitor until the latest published version is visible. 71 | 1. Hooray, we're in Maven Central now! 72 | 1. Push all of our work to Github to make it official. Check previous [releases](https://github.com/KeepSafe/TapTargetView/releases) and edit tag release changes: 73 | ```bash 74 | git push --tags origin master 75 | ``` 76 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | } 4 | 5 | android { 6 | namespace 'com.getkeepsafe.taptargetviewsample' 7 | compileSdk libs.versions.compileSdk.get().toInteger() 8 | 9 | defaultConfig { 10 | applicationId 'com.getkeepsafe.taptargetviewsample' 11 | minSdkVersion libs.versions.minSdk.get().toInteger() 12 | targetSdkVersion libs.versions.compileSdk.get().toInteger() 13 | versionCode 1 14 | versionName '1.0' 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | } 24 | 25 | dependencies { 26 | implementation project(':taptargetview') 27 | implementation libs.androidx.appcompat 28 | implementation libs.material 29 | implementation libs.stetho 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/xiphirx/Documents/dev/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.getkeepsafe.taptargetviewsample; 2 | 3 | import android.content.DialogInterface; 4 | import android.graphics.Rect; 5 | import android.graphics.Typeface; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Bundle; 8 | 9 | import androidx.core.content.ContextCompat; 10 | import androidx.appcompat.app.AlertDialog; 11 | import androidx.appcompat.app.AppCompatActivity; 12 | import androidx.appcompat.widget.Toolbar; 13 | import androidx.core.view.WindowCompat; 14 | 15 | import android.text.SpannableString; 16 | import android.text.style.StyleSpan; 17 | import android.text.style.UnderlineSpan; 18 | import android.util.Log; 19 | import android.view.Display; 20 | import android.widget.TextView; 21 | import android.widget.Toast; 22 | 23 | import com.getkeepsafe.taptargetview.TapTarget; 24 | import com.getkeepsafe.taptargetview.TapTargetSequence; 25 | import com.getkeepsafe.taptargetview.TapTargetView; 26 | 27 | public class MainActivity extends AppCompatActivity { 28 | @Override 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | WindowCompat.setDecorFitsSystemWindows(getWindow(), false); 32 | setContentView(R.layout.activity_main); 33 | 34 | final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 35 | toolbar.inflateMenu(R.menu.menu_main); 36 | toolbar.setNavigationIcon(ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp)); 37 | 38 | // We load a drawable and create a location to show a tap target here 39 | // We need the display to get the width and height at this point in time 40 | final Display display = getWindowManager().getDefaultDisplay(); 41 | // Load our little droid guy 42 | final Drawable droid = ContextCompat.getDrawable(this, R.drawable.ic_android_black_24dp); 43 | // Tell our droid buddy where we want him to appear 44 | final Rect droidTarget = new Rect(0, 0, droid.getIntrinsicWidth() * 2, droid.getIntrinsicHeight() * 2); 45 | // Using deprecated methods makes you look way cool 46 | droidTarget.offset(display.getWidth() / 2, display.getHeight() / 2); 47 | 48 | final SpannableString sassyDesc = new SpannableString("It allows you to go back, sometimes"); 49 | sassyDesc.setSpan(new StyleSpan(Typeface.ITALIC), sassyDesc.length() - "sometimes".length(), sassyDesc.length(), 0); 50 | 51 | // We have a sequence of targets, so lets build it! 52 | final TapTargetSequence sequence = new TapTargetSequence(this) 53 | .targets( 54 | // This tap target will target the back button, we just need to pass its containing toolbar 55 | TapTarget.forToolbarNavigationIcon(toolbar, "This is the back button", sassyDesc).id(1), 56 | // Likewise, this tap target will target the search button 57 | TapTarget.forToolbarMenuItem(toolbar, R.id.search, "This is a search icon", "As you can see, it has gotten pretty dark around here...") 58 | .dimColor(android.R.color.black) 59 | .outerCircleColor(R.color.colorAccent) 60 | .targetCircleColor(android.R.color.black) 61 | .transparentTarget(true) 62 | .textColor(android.R.color.black) 63 | .id(2), 64 | // You can also target the overflow button in your toolbar 65 | TapTarget.forToolbarOverflow(toolbar, "This will show more options", "But they're not useful :(").id(3), 66 | // This tap target will target our droid buddy at the given target rect 67 | TapTarget.forBounds(droidTarget, "Oh look!", "You can point to any part of the screen. You also can't cancel this one!") 68 | .cancelable(false) 69 | .icon(droid) 70 | .id(4) 71 | ) 72 | .listener(new TapTargetSequence.Listener() { 73 | // This listener will tell us when interesting(tm) events happen in regards 74 | // to the sequence 75 | @Override 76 | public void onSequenceFinish() { 77 | ((TextView) findViewById(R.id.educated)).setText("Congratulations! You're educated now!"); 78 | } 79 | 80 | @Override 81 | public void onSequenceStep(TapTarget lastTarget, boolean targetClicked) { 82 | Log.d("TapTargetView", "Clicked on " + lastTarget.id()); 83 | } 84 | 85 | @Override 86 | public void onSequenceCanceled(TapTarget lastTarget) { 87 | final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this) 88 | .setTitle("Uh oh") 89 | .setMessage("You canceled the sequence") 90 | .setPositiveButton("Oops", null).show(); 91 | TapTargetView.showFor(dialog, 92 | TapTarget.forView(dialog.getButton(DialogInterface.BUTTON_POSITIVE), "Uh oh!", "You canceled the sequence at step " + lastTarget.id()) 93 | .cancelable(false) 94 | .tintTarget(false), new TapTargetView.Listener() { 95 | @Override 96 | public void onTargetClick(TapTargetView view) { 97 | super.onTargetClick(view); 98 | dialog.dismiss(); 99 | } 100 | }); 101 | } 102 | }); 103 | 104 | // You don't always need a sequence, and for that there's a single time tap target 105 | final SpannableString spannedDesc = new SpannableString("This is the sample app for TapTargetView"); 106 | spannedDesc.setSpan(new UnderlineSpan(), spannedDesc.length() - "TapTargetView".length(), spannedDesc.length(), 0); 107 | TapTargetView.showFor(this, TapTarget.forView(findViewById(R.id.fab), "Hello, world!", spannedDesc) 108 | .cancelable(false) 109 | .drawShadow(true) 110 | .titleTextDimen(R.dimen.title_text_size) 111 | .tintTarget(false), new TapTargetView.Listener() { 112 | @Override 113 | public void onTargetClick(TapTargetView view) { 114 | super.onTargetClick(view); 115 | // .. which evidently starts the sequence we defined earlier 116 | sequence.start(); 117 | } 118 | 119 | @Override 120 | public void onOuterCircleClick(TapTargetView view) { 121 | super.onOuterCircleClick(view); 122 | Toast.makeText(view.getContext(), "You clicked the outer circle!", Toast.LENGTH_SHORT).show(); 123 | } 124 | 125 | @Override 126 | public void onTargetDismissed(TapTargetView view, boolean userInitiated) { 127 | Log.d("TapTargetViewSample", "You dismissed me :("); 128 | } 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/com/getkeepsafe/taptargetviewsample/SampleApplication.java: -------------------------------------------------------------------------------- 1 | package com.getkeepsafe.taptargetviewsample; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.stetho.Stetho; 6 | 7 | public class SampleApplication extends Application { 8 | @Override 9 | public void onCreate() { 10 | super.onCreate(); 11 | Stetho.initializeWithDefaults(this); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_android_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_directions_car_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_vert_black_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 20 | 21 | 28 | 29 | 35 | 36 | 43 | 44 | 45 | 46 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 20dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TapTargetViewSample 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.mavenpublish) apply false 5 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | 4 | GROUP=com.getkeepsafe.taptargetview 5 | VERSION_NAME=1.15.0 6 | POM_ARTIFACT_ID=taptargetview 7 | 8 | POM_NAME=TapTargetView 9 | POM_PACKAGING=aar 10 | 11 | POM_DESCRIPTION=An implementation of tap targets from the Material Design guidelines for feature discovery 12 | POM_INCEPTION_YEAR=2016 13 | 14 | POM_URL=https://github.com/KeepSafe/TapTargetView 15 | POM_SCM_URL=https://github.com/KeepSafe/TapTargetView 16 | POM_SCM_CONNECTION=scm:git:git://github.com/KeepSafe/TapTargetView.git 17 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com:KeepSafe/TapTargetView.git 18 | 19 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 20 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 21 | POM_LICENCE_DIST=repo 22 | 23 | POM_DEVELOPER_ID=keepsafe 24 | POM_DEVELOPER_NAME=KeepSafe Software, Inc. 25 | POM_DEVELOPER_URL=https://github.com/KeepSafe/ -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | compileSdk = "30" 3 | minSdk = "14" 4 | 5 | androidGradlePlugin = "8.5.1" 6 | androidxAnnotation = "1.2.0" 7 | androidxAppcompat = "1.3.0" 8 | androidxCore = "1.6.0" 9 | material = "1.4.0" 10 | mavenpublish = "0.29.0" 11 | stetho = "1.5.0" 12 | 13 | [libraries] 14 | androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" } 15 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" } 16 | androidx-core = { group = "androidx.core", name = "core", version.ref = "androidxCore" } 17 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 18 | stetho = { group = "com.facebook.stetho", name = "stetho", version.ref = "stetho" } 19 | 20 | [plugins] 21 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } 22 | android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } 23 | mavenpublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenpublish" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KeepSafe/TapTargetView/101e89d23c4a56e7e4c11d8844dc7f7170ee2230/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Jul 15 20:56:48 EDT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | 17 | include ':app', ':taptargetview' 18 | -------------------------------------------------------------------------------- /taptargetview/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.library) 3 | alias(libs.plugins.mavenpublish) 4 | } 5 | 6 | android { 7 | namespace 'com.getkeepsafe.taptargetview' 8 | compileSdk libs.versions.compileSdk.get().toInteger() 9 | 10 | defaultConfig { 11 | minSdkVersion libs.versions.minSdk.get().toInteger() 12 | } 13 | } 14 | 15 | dependencies { 16 | api libs.androidx.annotation 17 | api libs.androidx.appcompat 18 | implementation libs.androidx.core 19 | } 20 | 21 | // build a jar with source files 22 | tasks.register('sourcesJar', Jar) { 23 | from android.sourceSets.main.java.srcDirs 24 | archiveClassifier = 'sources' 25 | } 26 | 27 | tasks.register('javadoc', Javadoc) { 28 | failOnError false 29 | source = android.sourceSets.main.java.sourceFiles 30 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 31 | } 32 | 33 | // build a jar with javadoc 34 | tasks.register('javadocJar', Jar) { 35 | dependsOn javadoc 36 | archiveClassifier = 'javadoc' 37 | from javadoc.destinationDir 38 | } 39 | 40 | artifacts { 41 | archives sourcesJar 42 | archives javadocJar 43 | } 44 | 45 | import com.vanniktech.maven.publish.AndroidSingleVariantLibrary 46 | import com.vanniktech.maven.publish.SonatypeHost 47 | 48 | mavenPublishing { 49 | configure(new AndroidSingleVariantLibrary("release", true, true)) 50 | publishToMavenCentral(SonatypeHost.DEFAULT, true) 51 | signAllPublications() 52 | } -------------------------------------------------------------------------------- /taptargetview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.animation.Animator; 19 | import android.animation.AnimatorListenerAdapter; 20 | import android.animation.TimeInterpolator; 21 | import android.animation.ValueAnimator; 22 | 23 | /** 24 | * A small wrapper around {@link ValueAnimator} to provide a builder-like interface 25 | */ 26 | class FloatValueAnimatorBuilder { 27 | final ValueAnimator animator; 28 | 29 | EndListener endListener; 30 | 31 | interface UpdateListener { 32 | void onUpdate(float lerpTime); 33 | } 34 | 35 | interface EndListener { 36 | void onEnd(); 37 | } 38 | 39 | protected FloatValueAnimatorBuilder() { 40 | this(false); 41 | } 42 | 43 | protected FloatValueAnimatorBuilder(boolean reverse) { 44 | if (reverse) { 45 | this.animator = ValueAnimator.ofFloat(1.0f, 0.0f); 46 | } else { 47 | this.animator = ValueAnimator.ofFloat(0.0f, 1.0f); 48 | } 49 | } 50 | 51 | public FloatValueAnimatorBuilder delayBy(long millis) { 52 | animator.setStartDelay(millis); 53 | return this; 54 | } 55 | 56 | public FloatValueAnimatorBuilder duration(long millis) { 57 | animator.setDuration(millis); 58 | return this; 59 | } 60 | 61 | public FloatValueAnimatorBuilder interpolator(TimeInterpolator lerper) { 62 | animator.setInterpolator(lerper); 63 | return this; 64 | } 65 | 66 | public FloatValueAnimatorBuilder repeat(int times) { 67 | animator.setRepeatCount(times); 68 | return this; 69 | } 70 | 71 | public FloatValueAnimatorBuilder onUpdate(final UpdateListener listener) { 72 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 73 | @Override 74 | public void onAnimationUpdate(ValueAnimator animation) { 75 | listener.onUpdate((float) animation.getAnimatedValue()); 76 | } 77 | }); 78 | return this; 79 | } 80 | 81 | public FloatValueAnimatorBuilder onEnd(final EndListener listener) { 82 | this.endListener = listener; 83 | return this; 84 | } 85 | 86 | public ValueAnimator build() { 87 | if (endListener != null) { 88 | animator.addListener(new AnimatorListenerAdapter() { 89 | @Override 90 | public void onAnimationEnd(Animator animation) { 91 | endListener.onEnd(); 92 | } 93 | }); 94 | } 95 | 96 | return animator; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/ReflectUtil.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import java.lang.reflect.Field; 19 | 20 | class ReflectUtil { 21 | ReflectUtil() { 22 | } 23 | 24 | /** Returns the value of the given private field from the source object **/ 25 | static Object getPrivateField(Object source, String fieldName) 26 | throws NoSuchFieldException, IllegalAccessException { 27 | final Field objectField = source.getClass().getDeclaredField(fieldName); 28 | objectField.setAccessible(true); 29 | return objectField.get(source); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTarget.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.content.Context; 19 | import android.graphics.Rect; 20 | import android.graphics.Typeface; 21 | import android.graphics.drawable.Drawable; 22 | import androidx.annotation.ColorInt; 23 | import androidx.annotation.ColorRes; 24 | import androidx.annotation.DimenRes; 25 | import androidx.annotation.IdRes; 26 | import androidx.annotation.Nullable; 27 | import androidx.core.content.ContextCompat; 28 | import androidx.appcompat.widget.Toolbar; 29 | import android.view.View; 30 | 31 | /** 32 | * Describes the properties and options for a {@link TapTargetView}. 33 | *

34 | * Each tap target describes a target via a pair of bounds and icon. The bounds dictate the 35 | * location and touch area of the target, where the icon is what will be drawn within the center of 36 | * the bounds. 37 | *

38 | * This class can be extended to support various target types. 39 | * 40 | * @see ViewTapTarget ViewTapTarget for targeting standard Android views 41 | */ 42 | public class TapTarget { 43 | final CharSequence title; 44 | @Nullable 45 | final CharSequence description; 46 | 47 | float outerCircleAlpha = 0.96f; 48 | int targetRadius = 44; 49 | 50 | Rect bounds; 51 | Drawable icon; 52 | Typeface titleTypeface; 53 | Typeface descriptionTypeface; 54 | 55 | @ColorRes 56 | private int outerCircleColorRes = -1; 57 | @ColorRes 58 | private int targetCircleColorRes = -1; 59 | @ColorRes 60 | private int dimColorRes = -1; 61 | @ColorRes 62 | private int titleTextColorRes = -1; 63 | @ColorRes 64 | private int descriptionTextColorRes = -1; 65 | 66 | private Integer outerCircleColor = null; 67 | private Integer targetCircleColor = null; 68 | private Integer dimColor = null; 69 | private Integer titleTextColor = null; 70 | private Integer descriptionTextColor = null; 71 | 72 | @DimenRes 73 | private int titleTextDimen = -1; 74 | @DimenRes 75 | private int descriptionTextDimen = -1; 76 | 77 | private int titleTextSize = 20; 78 | private int descriptionTextSize = 18; 79 | int id = -1; 80 | 81 | boolean drawShadow = false; 82 | boolean cancelable = true; 83 | boolean tintTarget = true; 84 | boolean transparentTarget = false; 85 | float descriptionTextAlpha = 0.54f; 86 | 87 | boolean drawBehindStatusBar = true; 88 | boolean drawBehindNavigationBar = true; 89 | 90 | boolean forceCenteredTarget = false; 91 | 92 | /** 93 | * Return a tap target for the overflow button from the given toolbar 94 | *

95 | * Note: This is currently experimental, use at your own risk 96 | */ 97 | public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title) { 98 | return forToolbarOverflow(toolbar, title, null); 99 | } 100 | 101 | /** Return a tap target for the overflow button from the given toolbar 102 | *

103 | * Note: This is currently experimental, use at your own risk 104 | */ 105 | public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title, 106 | @Nullable CharSequence description) { 107 | return new ToolbarTapTarget(toolbar, false, title, description); 108 | } 109 | 110 | /** Return a tap target for the overflow button from the given toolbar 111 | *

112 | * Note: This is currently experimental, use at your own risk 113 | */ 114 | public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title) { 115 | return forToolbarOverflow(toolbar, title, null); 116 | } 117 | 118 | /** Return a tap target for the overflow button from the given toolbar 119 | *

120 | * Note: This is currently experimental, use at your own risk 121 | */ 122 | public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title, 123 | @Nullable CharSequence description) { 124 | return new ToolbarTapTarget(toolbar, false, title, description); 125 | } 126 | 127 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ 128 | public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title) { 129 | return forToolbarNavigationIcon(toolbar, title, null); 130 | } 131 | 132 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ 133 | public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title, 134 | @Nullable CharSequence description) { 135 | return new ToolbarTapTarget(toolbar, true, title, description); 136 | } 137 | 138 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ 139 | public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title) { 140 | return forToolbarNavigationIcon(toolbar, title, null); 141 | } 142 | 143 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/ 144 | public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title, 145 | @Nullable CharSequence description) { 146 | return new ToolbarTapTarget(toolbar, true, title, description); 147 | } 148 | 149 | /** Return a tap target for the menu item from the given toolbar **/ 150 | public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId, 151 | CharSequence title) { 152 | return forToolbarMenuItem(toolbar, menuItemId, title, null); 153 | } 154 | 155 | /** Return a tap target for the menu item from the given toolbar **/ 156 | public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId, 157 | CharSequence title, @Nullable CharSequence description) { 158 | return new ToolbarTapTarget(toolbar, menuItemId, title, description); 159 | } 160 | 161 | /** Return a tap target for the menu item from the given toolbar **/ 162 | public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId, 163 | CharSequence title) { 164 | return forToolbarMenuItem(toolbar, menuItemId, title, null); 165 | } 166 | 167 | /** Return a tap target for the menu item from the given toolbar **/ 168 | public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId, 169 | CharSequence title, @Nullable CharSequence description) { 170 | return new ToolbarTapTarget(toolbar, menuItemId, title, description); 171 | } 172 | 173 | /** Return a tap target for the specified view **/ 174 | public static TapTarget forView(View view, CharSequence title) { 175 | return forView(view, title, null); 176 | } 177 | 178 | /** Return a tap target for the specified view **/ 179 | public static TapTarget forView(View view, CharSequence title, @Nullable CharSequence description) { 180 | return new ViewTapTarget(view, title, description); 181 | } 182 | 183 | /** Return a tap target for the specified bounds **/ 184 | public static TapTarget forBounds(Rect bounds, CharSequence title) { 185 | return forBounds(bounds, title, null); 186 | } 187 | 188 | /** Return a tap target for the specified bounds **/ 189 | public static TapTarget forBounds(Rect bounds, CharSequence title, @Nullable CharSequence description) { 190 | return new TapTarget(bounds, title, description); 191 | } 192 | 193 | protected TapTarget(Rect bounds, CharSequence title, @Nullable CharSequence description) { 194 | this(title, description); 195 | if (bounds == null) { 196 | throw new IllegalArgumentException("Cannot pass null bounds or title"); 197 | } 198 | 199 | this.bounds = bounds; 200 | } 201 | 202 | protected TapTarget(CharSequence title, @Nullable CharSequence description) { 203 | if (title == null) { 204 | throw new IllegalArgumentException("Cannot pass null title"); 205 | } 206 | 207 | this.title = title; 208 | this.description = description; 209 | } 210 | 211 | /** Specify whether the target should draw behind the status bar. */ 212 | public TapTarget setDrawBehindStatusBar(boolean drawBehindStatusBar) { 213 | this.drawBehindStatusBar = drawBehindStatusBar; 214 | return this; 215 | } 216 | 217 | /** Specify whether the target should draw behind the navigation bar. */ 218 | public TapTarget setDrawBehindNavigationBar(boolean drawBehindNavigationBar) { 219 | this.drawBehindNavigationBar = drawBehindNavigationBar; 220 | return this; 221 | } 222 | 223 | /** Specify whether the target should be forced to center on the target view. */ 224 | public TapTarget setForceCenteredTarget(boolean forceCenteredTarget) { 225 | this.forceCenteredTarget = forceCenteredTarget; 226 | return this; 227 | } 228 | 229 | /** Specify whether the target should be transparent **/ 230 | public TapTarget transparentTarget(boolean transparent) { 231 | this.transparentTarget = transparent; 232 | return this; 233 | } 234 | 235 | /** Specify the color resource for the outer circle **/ 236 | public TapTarget outerCircleColor(@ColorRes int color) { 237 | this.outerCircleColorRes = color; 238 | return this; 239 | } 240 | 241 | /** Specify the color value for the outer circle **/ 242 | // TODO(Hilal): In v2, this API should be cleaned up / torched 243 | public TapTarget outerCircleColorInt(@ColorInt int color) { 244 | this.outerCircleColor = color; 245 | return this; 246 | } 247 | 248 | /** Specify the alpha value [0.0, 1.0] of the outer circle **/ 249 | public TapTarget outerCircleAlpha(float alpha) { 250 | if (alpha < 0.0f || alpha > 1.0f) { 251 | throw new IllegalArgumentException("Given an invalid alpha value: " + alpha); 252 | } 253 | this.outerCircleAlpha = alpha; 254 | return this; 255 | } 256 | 257 | /** Specify the color resource for the target circle **/ 258 | public TapTarget targetCircleColor(@ColorRes int color) { 259 | this.targetCircleColorRes = color; 260 | return this; 261 | } 262 | 263 | /** Specify the color value for the target circle **/ 264 | // TODO(Hilal): In v2, this API should be cleaned up / torched 265 | public TapTarget targetCircleColorInt(@ColorInt int color) { 266 | this.targetCircleColor = color; 267 | return this; 268 | } 269 | 270 | /** Specify the color resource for all text **/ 271 | public TapTarget textColor(@ColorRes int color) { 272 | this.titleTextColorRes = color; 273 | this.descriptionTextColorRes = color; 274 | return this; 275 | } 276 | 277 | /** Specify the color value for all text **/ 278 | // TODO(Hilal): In v2, this API should be cleaned up / torched 279 | public TapTarget textColorInt(@ColorInt int color) { 280 | this.titleTextColor = color; 281 | this.descriptionTextColor = color; 282 | return this; 283 | } 284 | 285 | /** Specify the color resource for the title text **/ 286 | public TapTarget titleTextColor(@ColorRes int color) { 287 | this.titleTextColorRes = color; 288 | return this; 289 | } 290 | 291 | /** Specify the color value for the title text **/ 292 | // TODO(Hilal): In v2, this API should be cleaned up / torched 293 | public TapTarget titleTextColorInt(@ColorInt int color) { 294 | this.titleTextColor = color; 295 | return this; 296 | } 297 | 298 | /** Specify the color resource for the description text **/ 299 | public TapTarget descriptionTextColor(@ColorRes int color) { 300 | this.descriptionTextColorRes = color; 301 | return this; 302 | } 303 | 304 | /** Specify the color value for the description text **/ 305 | // TODO(Hilal): In v2, this API should be cleaned up / torched 306 | public TapTarget descriptionTextColorInt(@ColorInt int color) { 307 | this.descriptionTextColor = color; 308 | return this; 309 | } 310 | 311 | /** Specify the typeface for all text **/ 312 | public TapTarget textTypeface(Typeface typeface) { 313 | if (typeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); 314 | titleTypeface = typeface; 315 | descriptionTypeface = typeface; 316 | return this; 317 | } 318 | 319 | /** Specify the typeface for title text **/ 320 | public TapTarget titleTypeface(Typeface titleTypeface) { 321 | if (titleTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); 322 | this.titleTypeface = titleTypeface; 323 | return this; 324 | } 325 | 326 | /** Specify the typeface for description text **/ 327 | public TapTarget descriptionTypeface(Typeface descriptionTypeface) { 328 | if (descriptionTypeface == null) throw new IllegalArgumentException("Cannot use a null typeface"); 329 | this.descriptionTypeface = descriptionTypeface; 330 | return this; 331 | } 332 | 333 | /** Specify the text size for the title in SP **/ 334 | public TapTarget titleTextSize(int sp) { 335 | if (sp < 0) throw new IllegalArgumentException("Given negative text size"); 336 | this.titleTextSize = sp; 337 | return this; 338 | } 339 | 340 | /** Specify the text size for the description in SP **/ 341 | public TapTarget descriptionTextSize(int sp) { 342 | if (sp < 0) throw new IllegalArgumentException("Given negative text size"); 343 | this.descriptionTextSize = sp; 344 | return this; 345 | } 346 | 347 | /** 348 | * Specify the text size for the title via a dimen resource 349 | *

350 | * Note: If set, this value will take precedence over the specified sp size 351 | */ 352 | public TapTarget titleTextDimen(@DimenRes int dimen) { 353 | this.titleTextDimen = dimen; 354 | return this; 355 | } 356 | 357 | /** Specify the alpha value [0.0, 1.0] of the description text **/ 358 | public TapTarget descriptionTextAlpha(float descriptionTextAlpha) { 359 | if (descriptionTextAlpha < 0 || descriptionTextAlpha > 1f) { 360 | throw new IllegalArgumentException("Given an invalid alpha value: " + descriptionTextAlpha); 361 | } 362 | this.descriptionTextAlpha = descriptionTextAlpha; 363 | return this; 364 | } 365 | 366 | /** 367 | * Specify the text size for the description via a dimen resource 368 | *

369 | * Note: If set, this value will take precedence over the specified sp size 370 | */ 371 | public TapTarget descriptionTextDimen(@DimenRes int dimen) { 372 | this.descriptionTextDimen = dimen; 373 | return this; 374 | } 375 | 376 | /** 377 | * Specify the color resource to use as a dim effect 378 | *

379 | * Note: The given color will have its opacity modified to 30% automatically 380 | */ 381 | public TapTarget dimColor(@ColorRes int color) { 382 | this.dimColorRes = color; 383 | return this; 384 | } 385 | 386 | /** 387 | * Specify the color value to use as a dim effect 388 | *

389 | * Note: The given color will have its opacity modified to 30% automatically 390 | */ 391 | // TODO(Hilal): In v2, this API should be cleaned up / torched 392 | public TapTarget dimColorInt(@ColorInt int color) { 393 | this.dimColor = color; 394 | return this; 395 | } 396 | 397 | /** Specify whether or not to draw a drop shadow around the outer circle **/ 398 | public TapTarget drawShadow(boolean draw) { 399 | this.drawShadow = draw; 400 | return this; 401 | } 402 | 403 | /** Specify whether or not the target should be cancelable **/ 404 | public TapTarget cancelable(boolean status) { 405 | this.cancelable = status; 406 | return this; 407 | } 408 | 409 | /** Specify whether to tint the target's icon with the outer circle's color **/ 410 | public TapTarget tintTarget(boolean tint) { 411 | this.tintTarget = tint; 412 | return this; 413 | } 414 | 415 | /** Specify the icon that will be drawn in the center of the target bounds **/ 416 | public TapTarget icon(Drawable icon) { 417 | return icon(icon, false); 418 | } 419 | 420 | /** 421 | * Specify the icon that will be drawn in the center of the target bounds 422 | * @param hasSetBounds Whether the drawable already has its bounds correctly set. If the 423 | * drawable does not have its bounds set, then the following bounds will 424 | * be applied:
425 | * (0, 0, intrinsic-width, intrinsic-height) 426 | */ 427 | public TapTarget icon(Drawable icon, boolean hasSetBounds) { 428 | if (icon == null) throw new IllegalArgumentException("Cannot use null drawable"); 429 | this.icon = icon; 430 | 431 | if (!hasSetBounds) { 432 | this.icon.setBounds(new Rect(0, 0, this.icon.getIntrinsicWidth(), this.icon.getIntrinsicHeight())); 433 | } 434 | 435 | return this; 436 | } 437 | 438 | /** Specify a unique identifier for this target. **/ 439 | public TapTarget id(int id) { 440 | this.id = id; 441 | return this; 442 | } 443 | 444 | /** Specify the target radius in dp. **/ 445 | public TapTarget targetRadius(int targetRadius) { 446 | this.targetRadius = targetRadius; 447 | return this; 448 | } 449 | 450 | /** Return the id associated with this tap target **/ 451 | public int id() { 452 | return id; 453 | } 454 | 455 | /** 456 | * In case your target needs time to be ready (laid out in your view, not created, etc), the 457 | * runnable passed here will be invoked when the target is ready. 458 | */ 459 | public void onReady(Runnable runnable) { 460 | runnable.run(); 461 | } 462 | 463 | /** 464 | * Returns the target bounds. Throws an exception if they are not set 465 | * (target may not be ready) 466 | *

467 | * This will only be called internally when {@link #onReady(Runnable)} invokes its runnable 468 | */ 469 | public Rect bounds() { 470 | if (bounds == null) { 471 | throw new IllegalStateException("Requesting bounds that are not set! Make sure your target is ready"); 472 | } 473 | return bounds; 474 | } 475 | 476 | @Nullable 477 | Integer outerCircleColorInt(Context context) { 478 | return colorResOrInt(context, outerCircleColor, outerCircleColorRes); 479 | } 480 | 481 | @Nullable 482 | Integer targetCircleColorInt(Context context) { 483 | return colorResOrInt(context, targetCircleColor, targetCircleColorRes); 484 | } 485 | 486 | @Nullable 487 | Integer dimColorInt(Context context) { 488 | return colorResOrInt(context, dimColor, dimColorRes); 489 | } 490 | 491 | @Nullable 492 | Integer titleTextColorInt(Context context) { 493 | return colorResOrInt(context, titleTextColor, titleTextColorRes); 494 | } 495 | 496 | @Nullable 497 | Integer descriptionTextColorInt(Context context) { 498 | return colorResOrInt(context, descriptionTextColor, descriptionTextColorRes); 499 | } 500 | 501 | int titleTextSizePx(Context context) { 502 | return dimenOrSize(context, titleTextSize, titleTextDimen); 503 | } 504 | 505 | int descriptionTextSizePx(Context context) { 506 | return dimenOrSize(context, descriptionTextSize, descriptionTextDimen); 507 | } 508 | 509 | @Nullable 510 | private Integer colorResOrInt(Context context, @Nullable Integer value, @ColorRes int resource) { 511 | if (resource != -1) { 512 | return ContextCompat.getColor(context, resource); 513 | } 514 | 515 | return value; 516 | } 517 | 518 | private int dimenOrSize(Context context, int size, @DimenRes int dimen) { 519 | if (dimen != -1) { 520 | return context.getResources().getDimensionPixelSize(dimen); 521 | } 522 | 523 | return UiUtil.sp(context, size); 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.app.Activity; 19 | import android.app.Dialog; 20 | import androidx.annotation.Nullable; 21 | import androidx.annotation.UiThread; 22 | 23 | import java.util.Collections; 24 | import java.util.LinkedList; 25 | import java.util.List; 26 | import java.util.NoSuchElementException; 27 | import java.util.Queue; 28 | 29 | /** 30 | * Displays a sequence of {@link TapTargetView}s. 31 | *

32 | * Internally, a FIFO queue is held to dictate which {@link TapTarget} will be shown. 33 | */ 34 | public class TapTargetSequence { 35 | private final @Nullable Activity activity; 36 | private final @Nullable Dialog dialog; 37 | private final Queue targets; 38 | private boolean active; 39 | 40 | @Nullable 41 | private TapTargetView currentView; 42 | 43 | Listener listener; 44 | boolean considerOuterCircleCanceled; 45 | boolean continueOnCancel; 46 | 47 | public interface Listener { 48 | /** Called when there are no more tap targets to display */ 49 | void onSequenceFinish(); 50 | 51 | /** 52 | * Called when moving onto the next tap target. 53 | * @param lastTarget The last displayed target 54 | * @param targetClicked Whether the last displayed target was clicked (this will always be true 55 | * unless you have set {@link #continueOnCancel(boolean)} and the user 56 | * clicks outside of the target 57 | */ 58 | void onSequenceStep(TapTarget lastTarget, boolean targetClicked); 59 | 60 | /** 61 | * Called when the user taps outside of the current target, the target is cancelable, and 62 | * {@link #continueOnCancel(boolean)} is not set. 63 | * @param lastTarget The last displayed target 64 | */ 65 | void onSequenceCanceled(TapTarget lastTarget); 66 | } 67 | 68 | public TapTargetSequence(Activity activity) { 69 | if (activity == null) throw new IllegalArgumentException("Activity is null"); 70 | this.activity = activity; 71 | this.dialog = null; 72 | this.targets = new LinkedList<>(); 73 | } 74 | 75 | public TapTargetSequence(Dialog dialog) { 76 | if (dialog == null) throw new IllegalArgumentException("Given null Dialog"); 77 | this.dialog = dialog; 78 | this.activity = null; 79 | this.targets = new LinkedList<>(); 80 | } 81 | 82 | /** Adds the given targets, in order, to the pending queue of {@link TapTarget}s */ 83 | public TapTargetSequence targets(List targets) { 84 | this.targets.addAll(targets); 85 | return this; 86 | } 87 | 88 | /** Adds the given targets, in order, to the pending queue of {@link TapTarget}s */ 89 | public TapTargetSequence targets(TapTarget... targets) { 90 | Collections.addAll(this.targets, targets); 91 | return this; 92 | } 93 | 94 | /** Adds the given target to the pending queue of {@link TapTarget}s */ 95 | public TapTargetSequence target(TapTarget target) { 96 | this.targets.add(target); 97 | return this; 98 | } 99 | 100 | /** Whether or not to continue the sequence when a {@link TapTarget} is canceled **/ 101 | public TapTargetSequence continueOnCancel(boolean status) { 102 | this.continueOnCancel = status; 103 | return this; 104 | } 105 | 106 | /** Whether or not to consider taps on the outer circle as a cancellation **/ 107 | public TapTargetSequence considerOuterCircleCanceled(boolean status) { 108 | this.considerOuterCircleCanceled = status; 109 | return this; 110 | } 111 | 112 | /** Specify the listener for this sequence **/ 113 | public TapTargetSequence listener(Listener listener) { 114 | this.listener = listener; 115 | return this; 116 | } 117 | 118 | /** Immediately starts the sequence and displays the first target from the queue **/ 119 | @UiThread 120 | public void start() { 121 | if (targets.isEmpty() || active) { 122 | return; 123 | } 124 | 125 | active = true; 126 | showNext(); 127 | } 128 | 129 | /** Immediately starts the sequence from the given targetId's position in the queue */ 130 | public void startWith(int targetId) { 131 | if (active) { 132 | return; 133 | } 134 | 135 | while (targets.peek() != null && targets.peek().id() != targetId) { 136 | targets.poll(); 137 | } 138 | 139 | TapTarget peekedTarget = targets.peek(); 140 | if (peekedTarget == null || peekedTarget.id() != targetId) { 141 | throw new IllegalStateException("Given target " + targetId + " not in sequence"); 142 | } 143 | 144 | start(); 145 | } 146 | 147 | /** Immediately starts the sequence at the specified zero-based index in the queue */ 148 | public void startAt(int index) { 149 | if (active) { 150 | return; 151 | } 152 | 153 | if (index < 0 || index >= targets.size()) { 154 | throw new IllegalArgumentException("Given invalid index " + index); 155 | } 156 | 157 | final int expectedSize = targets.size() - index; 158 | while (targets.peek() != null && targets.size() != expectedSize) { 159 | targets.poll(); 160 | } 161 | 162 | if (targets.size() != expectedSize) { 163 | throw new IllegalStateException("Given index " + index + " not in sequence"); 164 | } 165 | 166 | start(); 167 | } 168 | 169 | /** 170 | * Cancels the sequence, if the current target is cancelable. 171 | * When the sequence is canceled, the current target is dismissed and the remaining targets are 172 | * removed from the sequence. 173 | * @return whether the sequence was canceled or not 174 | */ 175 | @UiThread 176 | public boolean cancel() { 177 | if (!active || currentView == null || !currentView.cancelable) { 178 | return false; 179 | } 180 | currentView.dismiss(false); 181 | active = false; 182 | targets.clear(); 183 | if (listener != null) { 184 | listener.onSequenceCanceled(currentView.target); 185 | } 186 | return true; 187 | } 188 | 189 | void showNext() { 190 | try { 191 | TapTarget tapTarget = targets.remove(); 192 | if (activity != null) { 193 | currentView = TapTargetView.showFor(activity, tapTarget, tapTargetListener); 194 | } else { 195 | currentView = TapTargetView.showFor(dialog, tapTarget, tapTargetListener); 196 | } 197 | } catch (NoSuchElementException e) { 198 | currentView = null; 199 | // No more targets 200 | if (listener != null) { 201 | listener.onSequenceFinish(); 202 | } 203 | } 204 | } 205 | 206 | private final TapTargetView.Listener tapTargetListener = new TapTargetView.Listener() { 207 | @Override 208 | public void onTargetClick(TapTargetView view) { 209 | super.onTargetClick(view); 210 | if (listener != null) { 211 | listener.onSequenceStep(view.target, true); 212 | } 213 | showNext(); 214 | } 215 | 216 | @Override 217 | public void onOuterCircleClick(TapTargetView view) { 218 | if (considerOuterCircleCanceled) { 219 | onTargetCancel(view); 220 | } 221 | } 222 | 223 | @Override 224 | public void onTargetCancel(TapTargetView view) { 225 | super.onTargetCancel(view); 226 | if (continueOnCancel) { 227 | if (listener != null) { 228 | listener.onSequenceStep(view.target, false); 229 | } 230 | showNext(); 231 | } else { 232 | if (listener != null) { 233 | listener.onSequenceCanceled(view.target); 234 | } 235 | } 236 | } 237 | }; 238 | } 239 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.animation.ValueAnimator; 19 | import android.annotation.SuppressLint; 20 | import android.annotation.TargetApi; 21 | import android.app.Activity; 22 | import android.app.Dialog; 23 | import android.content.Context; 24 | import android.content.res.Resources; 25 | import android.graphics.Bitmap; 26 | import android.graphics.Canvas; 27 | import android.graphics.Color; 28 | import android.graphics.Outline; 29 | import android.graphics.Paint; 30 | import android.graphics.Path; 31 | import android.graphics.PixelFormat; 32 | import android.graphics.PorterDuff; 33 | import android.graphics.PorterDuffColorFilter; 34 | import android.graphics.PorterDuffXfermode; 35 | import android.graphics.Rect; 36 | import android.graphics.Region; 37 | import android.graphics.Typeface; 38 | import android.graphics.drawable.Drawable; 39 | import android.os.Build; 40 | import androidx.annotation.Nullable; 41 | import android.text.DynamicLayout; 42 | import android.text.Layout; 43 | import android.text.SpannableStringBuilder; 44 | import android.text.StaticLayout; 45 | import android.text.TextPaint; 46 | import android.util.DisplayMetrics; 47 | import android.view.Gravity; 48 | import android.view.KeyEvent; 49 | import android.view.MotionEvent; 50 | import android.view.View; 51 | import android.view.ViewGroup; 52 | import android.view.ViewManager; 53 | import android.view.ViewOutlineProvider; 54 | import android.view.ViewTreeObserver; 55 | import android.view.WindowManager; 56 | import android.view.animation.AccelerateDecelerateInterpolator; 57 | 58 | /** 59 | * TapTargetView implements a feature discovery paradigm following Google's Material Design 60 | * guidelines. 61 | *

62 | * This class should not be instantiated directly. Instead, please use the 63 | * {@link #showFor(Activity, TapTarget, Listener)} static factory method instead. 64 | *

65 | * More information can be found here: 66 | * https://material.google.com/growth-communications/feature-discovery.html#feature-discovery-design 67 | */ 68 | @SuppressLint("ViewConstructor") 69 | public class TapTargetView extends View { 70 | private boolean isDismissed = false; 71 | private boolean isDismissing = false; 72 | private boolean isInteractable = true; 73 | 74 | final int TARGET_PADDING; 75 | final int TARGET_RADIUS; 76 | final int TARGET_PULSE_RADIUS; 77 | final int TEXT_PADDING; 78 | final int TEXT_SPACING; 79 | final int TEXT_MAX_WIDTH; 80 | final int TEXT_POSITIONING_BIAS; 81 | final int TEXT_SAFE_AREA_PADDING; 82 | final int CIRCLE_PADDING; 83 | final int GUTTER_DIM; 84 | final int SHADOW_DIM; 85 | final int SHADOW_JITTER_DIM; 86 | 87 | @Nullable 88 | final ViewGroup boundingParent; 89 | final ViewManager parent; 90 | final TapTarget target; 91 | final Rect targetBounds; 92 | 93 | final TextPaint titlePaint; 94 | final TextPaint descriptionPaint; 95 | final Paint outerCirclePaint; 96 | final Paint outerCircleShadowPaint; 97 | final Paint targetCirclePaint; 98 | final Paint targetCirclePulsePaint; 99 | 100 | CharSequence title; 101 | @Nullable 102 | StaticLayout titleLayout; 103 | @Nullable 104 | CharSequence description; 105 | @Nullable 106 | StaticLayout descriptionLayout; 107 | boolean isDark; 108 | boolean debug; 109 | boolean shouldTintTarget; 110 | boolean shouldDrawShadow; 111 | boolean cancelable; 112 | boolean visible; 113 | 114 | // Debug related variables 115 | @Nullable 116 | SpannableStringBuilder debugStringBuilder; 117 | @Nullable 118 | DynamicLayout debugLayout; 119 | @Nullable 120 | TextPaint debugTextPaint; 121 | @Nullable 122 | Paint debugPaint; 123 | 124 | // Drawing properties 125 | Rect drawingBounds; 126 | Rect textBounds; 127 | 128 | Path outerCirclePath; 129 | float outerCircleRadius; 130 | int calculatedOuterCircleRadius; 131 | int[] outerCircleCenter; 132 | int outerCircleAlpha; 133 | 134 | float targetCirclePulseRadius; 135 | int targetCirclePulseAlpha; 136 | 137 | float targetCircleRadius; 138 | int targetCircleAlpha; 139 | 140 | int textAlpha; 141 | int dimColor; 142 | 143 | float lastTouchX; 144 | float lastTouchY; 145 | 146 | int topBoundary; 147 | int bottomBoundary; 148 | 149 | Bitmap tintedTarget; 150 | 151 | Listener listener; 152 | 153 | @Nullable 154 | ViewOutlineProvider outlineProvider; 155 | 156 | public static TapTargetView showFor(Activity activity, TapTarget target) { 157 | return showFor(activity, target, null); 158 | } 159 | 160 | public static TapTargetView showFor(Activity activity, TapTarget target, Listener listener) { 161 | if (activity == null) throw new IllegalArgumentException("Activity is null"); 162 | 163 | final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView(); 164 | final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams( 165 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 166 | final ViewGroup content = (ViewGroup) decor.findViewById(android.R.id.content); 167 | final TapTargetView tapTargetView = new TapTargetView(activity, decor, content, target, listener); 168 | decor.addView(tapTargetView, layoutParams); 169 | 170 | return tapTargetView; 171 | } 172 | 173 | public static TapTargetView showFor(Dialog dialog, TapTarget target) { 174 | return showFor(dialog, target, null); 175 | } 176 | 177 | public static TapTargetView showFor(Dialog dialog, TapTarget target, Listener listener) { 178 | if (dialog == null) throw new IllegalArgumentException("Dialog is null"); 179 | 180 | final Context context = dialog.getContext(); 181 | final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 182 | final WindowManager.LayoutParams params = new WindowManager.LayoutParams(); 183 | params.type = WindowManager.LayoutParams.TYPE_APPLICATION; 184 | params.format = PixelFormat.RGBA_8888; 185 | params.flags = 0; 186 | params.gravity = Gravity.START | Gravity.TOP; 187 | params.x = 0; 188 | params.y = 0; 189 | params.width = WindowManager.LayoutParams.MATCH_PARENT; 190 | params.height = WindowManager.LayoutParams.MATCH_PARENT; 191 | 192 | final TapTargetView tapTargetView = new TapTargetView(context, windowManager, null, target, listener); 193 | windowManager.addView(tapTargetView, params); 194 | 195 | return tapTargetView; 196 | } 197 | 198 | public static class Listener { 199 | /** Signals that the user has clicked inside of the target **/ 200 | public void onTargetClick(TapTargetView view) { 201 | view.dismiss(true); 202 | } 203 | 204 | /** Signals that the user has long clicked inside of the target **/ 205 | public void onTargetLongClick(TapTargetView view) { 206 | onTargetClick(view); 207 | } 208 | 209 | /** If cancelable, signals that the user has clicked outside of the outer circle **/ 210 | public void onTargetCancel(TapTargetView view) { 211 | view.dismiss(false); 212 | } 213 | 214 | /** Signals that the user clicked on the outer circle portion of the tap target **/ 215 | public void onOuterCircleClick(TapTargetView view) { 216 | // no-op as default 217 | } 218 | 219 | /** 220 | * Signals that the tap target has been dismissed 221 | * @param userInitiated Whether the user caused this action 222 | */ 223 | public void onTargetDismissed(TapTargetView view, boolean userInitiated) { 224 | } 225 | } 226 | 227 | final FloatValueAnimatorBuilder.UpdateListener expandContractUpdateListener = new FloatValueAnimatorBuilder.UpdateListener() { 228 | @Override 229 | public void onUpdate(float lerpTime) { 230 | final float newOuterCircleRadius = calculatedOuterCircleRadius * lerpTime; 231 | final boolean expanding = newOuterCircleRadius > outerCircleRadius; 232 | if (!expanding) { 233 | // When contracting we need to invalidate the old drawing bounds. Otherwise 234 | // you will see artifacts as the circle gets smaller 235 | calculateDrawingBounds(); 236 | } 237 | 238 | final float targetAlpha = target.outerCircleAlpha * 255; 239 | outerCircleRadius = newOuterCircleRadius; 240 | outerCircleAlpha = (int) Math.min(targetAlpha, (lerpTime * 1.5f * targetAlpha)); 241 | outerCirclePath.reset(); 242 | outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW); 243 | 244 | targetCircleAlpha = (int) Math.min(255.0f, (lerpTime * 1.5f * 255.0f)); 245 | 246 | if (expanding) { 247 | targetCircleRadius = TARGET_RADIUS * Math.min(1.0f, lerpTime * 1.5f); 248 | } else { 249 | targetCircleRadius = TARGET_RADIUS * lerpTime; 250 | targetCirclePulseRadius *= lerpTime; 251 | } 252 | 253 | textAlpha = (int) (delayedLerp(lerpTime, 0.7f) * 255); 254 | 255 | if (expanding) { 256 | calculateDrawingBounds(); 257 | } 258 | 259 | invalidateViewAndOutline(drawingBounds); 260 | } 261 | }; 262 | 263 | final ValueAnimator expandAnimation = new FloatValueAnimatorBuilder() 264 | .duration(250) 265 | .delayBy(250) 266 | .interpolator(new AccelerateDecelerateInterpolator()) 267 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() { 268 | @Override 269 | public void onUpdate(float lerpTime) { 270 | expandContractUpdateListener.onUpdate(lerpTime); 271 | } 272 | }) 273 | .onEnd(new FloatValueAnimatorBuilder.EndListener() { 274 | @Override 275 | public void onEnd() { 276 | pulseAnimation.start(); 277 | isInteractable = true; 278 | } 279 | }) 280 | .build(); 281 | 282 | final ValueAnimator pulseAnimation = new FloatValueAnimatorBuilder() 283 | .duration(1000) 284 | .repeat(ValueAnimator.INFINITE) 285 | .interpolator(new AccelerateDecelerateInterpolator()) 286 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() { 287 | @Override 288 | public void onUpdate(float lerpTime) { 289 | final float pulseLerp = delayedLerp(lerpTime, 0.5f); 290 | targetCirclePulseRadius = (1.0f + pulseLerp) * TARGET_RADIUS; 291 | targetCirclePulseAlpha = (int) ((1.0f - pulseLerp) * 255); 292 | targetCircleRadius = TARGET_RADIUS + halfwayLerp(lerpTime) * TARGET_PULSE_RADIUS; 293 | 294 | if (outerCircleRadius != calculatedOuterCircleRadius) { 295 | outerCircleRadius = calculatedOuterCircleRadius; 296 | } 297 | 298 | calculateDrawingBounds(); 299 | invalidateViewAndOutline(drawingBounds); 300 | } 301 | }) 302 | .build(); 303 | 304 | final ValueAnimator dismissAnimation = new FloatValueAnimatorBuilder(true) 305 | .duration(250) 306 | .interpolator(new AccelerateDecelerateInterpolator()) 307 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() { 308 | @Override 309 | public void onUpdate(float lerpTime) { 310 | expandContractUpdateListener.onUpdate(lerpTime); 311 | } 312 | }) 313 | .onEnd(new FloatValueAnimatorBuilder.EndListener() { 314 | @Override 315 | public void onEnd() { 316 | finishDismiss(true); 317 | } 318 | }) 319 | .build(); 320 | 321 | private final ValueAnimator dismissConfirmAnimation = new FloatValueAnimatorBuilder() 322 | .duration(250) 323 | .interpolator(new AccelerateDecelerateInterpolator()) 324 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() { 325 | @Override 326 | public void onUpdate(float lerpTime) { 327 | final float spedUpLerp = Math.min(1.0f, lerpTime * 2.0f); 328 | outerCircleRadius = calculatedOuterCircleRadius * (1.0f + (spedUpLerp * 0.2f)); 329 | outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.outerCircleAlpha * 255.0f); 330 | outerCirclePath.reset(); 331 | outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW); 332 | targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS; 333 | targetCircleAlpha = (int) ((1.0f - lerpTime) * 255.0f); 334 | targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS; 335 | targetCirclePulseAlpha = (int) ((1.0f - lerpTime) * targetCirclePulseAlpha); 336 | textAlpha = (int) ((1.0f - spedUpLerp) * 255.0f); 337 | calculateDrawingBounds(); 338 | invalidateViewAndOutline(drawingBounds); 339 | } 340 | }) 341 | .onEnd(new FloatValueAnimatorBuilder.EndListener() { 342 | @Override 343 | public void onEnd() { 344 | finishDismiss(true); 345 | } 346 | }) 347 | .build(); 348 | 349 | private ValueAnimator[] animators = new ValueAnimator[] 350 | {expandAnimation, pulseAnimation, dismissConfirmAnimation, dismissAnimation}; 351 | 352 | private final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener; 353 | 354 | /** 355 | * This constructor should only be used directly for very specific use cases not covered by 356 | * the static factory methods. 357 | * 358 | * @param context The host context 359 | * @param parent The parent that this TapTargetView will become a child of. This parent should 360 | * allow the largest possible area for this view to utilize 361 | * @param boundingParent Optional. Will be used to calculate boundaries if needed. For example, 362 | * if your view is added to the decor view of your Window, then you want 363 | * to adjust for system ui like the navigation bar or status bar, and so 364 | * you would pass in the content view (which doesn't include system ui) 365 | * here. 366 | * @param target The {@link TapTarget} to target 367 | * @param userListener Optional. The {@link Listener} instance for this view 368 | */ 369 | public TapTargetView(final Context context, 370 | final ViewManager parent, 371 | @Nullable final ViewGroup boundingParent, 372 | final TapTarget target, 373 | @Nullable final Listener userListener) { 374 | super(context); 375 | if (target == null) throw new IllegalArgumentException("Target cannot be null"); 376 | 377 | this.target = target; 378 | this.parent = parent; 379 | this.boundingParent = boundingParent; 380 | this.listener = userListener != null ? userListener : new Listener(); 381 | this.title = target.title; 382 | this.description = target.description; 383 | 384 | TARGET_PADDING = UiUtil.dp(context, 20); 385 | CIRCLE_PADDING = UiUtil.dp(context, 40); 386 | TARGET_RADIUS = UiUtil.dp(context, target.targetRadius); 387 | TEXT_PADDING = UiUtil.dp(context, 40); 388 | TEXT_SPACING = UiUtil.dp(context, 8); 389 | TEXT_MAX_WIDTH = UiUtil.dp(context, 360); 390 | TEXT_POSITIONING_BIAS = UiUtil.dp(context, 20); 391 | TEXT_SAFE_AREA_PADDING = UiUtil.dp(getContext(), 10); 392 | GUTTER_DIM = UiUtil.dp(context, 88); 393 | SHADOW_DIM = UiUtil.dp(context, 8); 394 | SHADOW_JITTER_DIM = UiUtil.dp(context, 1); 395 | TARGET_PULSE_RADIUS = (int) (0.1f * TARGET_RADIUS); 396 | 397 | outerCirclePath = new Path(); 398 | targetBounds = new Rect(); 399 | drawingBounds = new Rect(); 400 | 401 | titlePaint = new TextPaint(); 402 | titlePaint.setTextSize(target.titleTextSizePx(context)); 403 | titlePaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL)); 404 | titlePaint.setAntiAlias(true); 405 | 406 | descriptionPaint = new TextPaint(); 407 | descriptionPaint.setTextSize(target.descriptionTextSizePx(context)); 408 | descriptionPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)); 409 | descriptionPaint.setAntiAlias(true); 410 | descriptionPaint.setAlpha((int) (0.54f * 255.0f)); 411 | 412 | outerCirclePaint = new Paint(); 413 | outerCirclePaint.setAntiAlias(true); 414 | outerCirclePaint.setAlpha((int) (target.outerCircleAlpha * 255.0f)); 415 | 416 | outerCircleShadowPaint = new Paint(); 417 | outerCircleShadowPaint.setAntiAlias(true); 418 | outerCircleShadowPaint.setAlpha(50); 419 | outerCircleShadowPaint.setStyle(Paint.Style.STROKE); 420 | outerCircleShadowPaint.setStrokeWidth(SHADOW_JITTER_DIM); 421 | outerCircleShadowPaint.setColor(Color.BLACK); 422 | 423 | targetCirclePaint = new Paint(); 424 | targetCirclePaint.setAntiAlias(true); 425 | 426 | targetCirclePulsePaint = new Paint(); 427 | targetCirclePulsePaint.setAntiAlias(true); 428 | 429 | applyTargetOptions(context); 430 | 431 | final boolean layoutNoLimits; 432 | if (context instanceof Activity) { 433 | Activity activity = (Activity) context; 434 | final int flags = activity.getWindow().getAttributes().flags; 435 | layoutNoLimits = (flags & WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) != 0; 436 | } else { 437 | layoutNoLimits = false; 438 | } 439 | 440 | globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() { 441 | @Override 442 | public void onGlobalLayout() { 443 | if (isDismissing) { 444 | return; 445 | } 446 | updateTextLayouts(); 447 | target.onReady(new Runnable() { 448 | @Override 449 | public void run() { 450 | final int[] offset = new int[2]; 451 | 452 | targetBounds.set(target.bounds()); 453 | 454 | getLocationOnScreen(offset); 455 | targetBounds.offset(-offset[0], -offset[1]); 456 | 457 | if (boundingParent != null) { 458 | final WindowManager windowManager 459 | = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 460 | final DisplayMetrics displayMetrics = new DisplayMetrics(); 461 | windowManager.getDefaultDisplay().getMetrics(displayMetrics); 462 | 463 | final Rect rect = new Rect(); 464 | boundingParent.getWindowVisibleDisplayFrame(rect); 465 | int[] parentLocation = new int[2]; 466 | boundingParent.getLocationInWindow(parentLocation); 467 | 468 | final boolean canDrawBehindSystemBars = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 469 | if (target.drawBehindStatusBar && canDrawBehindSystemBars) { 470 | rect.top = parentLocation[1]; 471 | } 472 | if (target.drawBehindNavigationBar && canDrawBehindSystemBars) { 473 | rect.bottom = parentLocation[1] + boundingParent.getHeight(); 474 | } 475 | 476 | // We bound the boundaries to be within the screen's coordinates to 477 | // handle the case where the flag FLAG_LAYOUT_NO_LIMITS is set 478 | if (layoutNoLimits) { 479 | topBoundary = Math.max(0, rect.top); 480 | bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels); 481 | } else { 482 | topBoundary = rect.top; 483 | bottomBoundary = rect.bottom; 484 | } 485 | } 486 | 487 | drawTintedTarget(); 488 | requestFocus(); 489 | calculateDimensions(); 490 | 491 | startExpandAnimation(); 492 | } 493 | }); 494 | } 495 | }; 496 | 497 | getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener); 498 | 499 | setFocusableInTouchMode(true); 500 | setClickable(true); 501 | setOnClickListener(new OnClickListener() { 502 | @Override 503 | public void onClick(View v) { 504 | if (listener == null || outerCircleCenter == null || !isInteractable) return; 505 | 506 | final boolean clickedInTarget = 507 | distance(targetBounds.centerX(), targetBounds.centerY(), (int) lastTouchX, (int) lastTouchY) <= targetCircleRadius; 508 | final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1], 509 | (int) lastTouchX, (int) lastTouchY); 510 | final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius; 511 | 512 | if (clickedInTarget) { 513 | isInteractable = false; 514 | listener.onTargetClick(TapTargetView.this); 515 | } else if (clickedInsideOfOuterCircle) { 516 | listener.onOuterCircleClick(TapTargetView.this); 517 | } else if (cancelable) { 518 | isInteractable = false; 519 | listener.onTargetCancel(TapTargetView.this); 520 | } 521 | } 522 | }); 523 | 524 | setOnLongClickListener(new OnLongClickListener() { 525 | @Override 526 | public boolean onLongClick(View v) { 527 | if (listener == null) return false; 528 | 529 | if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) { 530 | listener.onTargetLongClick(TapTargetView.this); 531 | return true; 532 | } 533 | 534 | return false; 535 | } 536 | }); 537 | } 538 | 539 | private void startExpandAnimation() { 540 | if (!visible) { 541 | isInteractable = false; 542 | expandAnimation.start(); 543 | visible = true; 544 | } 545 | } 546 | 547 | protected void applyTargetOptions(Context context) { 548 | shouldTintTarget = !target.transparentTarget && target.tintTarget; 549 | shouldDrawShadow = target.drawShadow; 550 | cancelable = target.cancelable; 551 | 552 | // We can't clip out portions of a view outline, so if the user specified a transparent 553 | // target, we need to fallback to drawing a jittered shadow approximation 554 | if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.transparentTarget) { 555 | outlineProvider = new ViewOutlineProvider() { 556 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 557 | @Override 558 | public void getOutline(View view, Outline outline) { 559 | if (outerCircleCenter == null) return; 560 | outline.setOval( 561 | (int) (outerCircleCenter[0] - outerCircleRadius), (int) (outerCircleCenter[1] - outerCircleRadius), 562 | (int) (outerCircleCenter[0] + outerCircleRadius), (int) (outerCircleCenter[1] + outerCircleRadius)); 563 | outline.setAlpha(outerCircleAlpha / 255.0f); 564 | if (Build.VERSION.SDK_INT >= 22) { 565 | outline.offset(0, SHADOW_DIM); 566 | } 567 | } 568 | }; 569 | 570 | setOutlineProvider(outlineProvider); 571 | setElevation(SHADOW_DIM); 572 | } 573 | 574 | if (shouldDrawShadow && outlineProvider == null && Build.VERSION.SDK_INT < 18) { 575 | setLayerType(LAYER_TYPE_SOFTWARE, null); 576 | } else { 577 | setLayerType(LAYER_TYPE_HARDWARE, null); 578 | } 579 | 580 | final Resources.Theme theme = context.getTheme(); 581 | isDark = UiUtil.themeIntAttr(context, "isLightTheme") == 0; 582 | 583 | final Integer outerCircleColor = target.outerCircleColorInt(context); 584 | if (outerCircleColor != null) { 585 | outerCirclePaint.setColor(outerCircleColor); 586 | } else if (theme != null) { 587 | outerCirclePaint.setColor(UiUtil.themeIntAttr(context, "colorPrimary")); 588 | } else { 589 | outerCirclePaint.setColor(Color.WHITE); 590 | } 591 | 592 | final Integer targetCircleColor = target.targetCircleColorInt(context); 593 | if (targetCircleColor != null) { 594 | targetCirclePaint.setColor(targetCircleColor); 595 | } else { 596 | targetCirclePaint.setColor(isDark ? Color.BLACK : Color.WHITE); 597 | } 598 | 599 | if (target.transparentTarget) { 600 | targetCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 601 | } 602 | 603 | targetCirclePulsePaint.setColor(targetCirclePaint.getColor()); 604 | 605 | final Integer targetDimColor = target.dimColorInt(context); 606 | if (targetDimColor != null) { 607 | dimColor = UiUtil.setAlpha(targetDimColor, 0.3f); 608 | } else { 609 | dimColor = -1; 610 | } 611 | 612 | final Integer titleTextColor = target.titleTextColorInt(context); 613 | if (titleTextColor != null) { 614 | titlePaint.setColor(titleTextColor); 615 | } else { 616 | titlePaint.setColor(isDark ? Color.BLACK : Color.WHITE); 617 | } 618 | 619 | final Integer descriptionTextColor = target.descriptionTextColorInt(context); 620 | if (descriptionTextColor != null) { 621 | descriptionPaint.setColor(descriptionTextColor); 622 | } else { 623 | descriptionPaint.setColor(titlePaint.getColor()); 624 | } 625 | 626 | if (target.titleTypeface != null) { 627 | titlePaint.setTypeface(target.titleTypeface); 628 | } 629 | 630 | if (target.descriptionTypeface != null) { 631 | descriptionPaint.setTypeface(target.descriptionTypeface); 632 | } 633 | } 634 | 635 | @Override 636 | protected void onDetachedFromWindow() { 637 | super.onDetachedFromWindow(); 638 | onDismiss(false); 639 | } 640 | 641 | void onDismiss(boolean userInitiated) { 642 | if (isDismissed) return; 643 | 644 | isDismissing = false; 645 | isDismissed = true; 646 | 647 | for (final ValueAnimator animator : animators) { 648 | animator.cancel(); 649 | animator.removeAllUpdateListeners(); 650 | } 651 | 652 | ViewUtil.removeOnGlobalLayoutListener(getViewTreeObserver(), globalLayoutListener); 653 | visible = false; 654 | 655 | if (listener != null) { 656 | listener.onTargetDismissed(this, userInitiated); 657 | } 658 | } 659 | 660 | @Override 661 | protected void onDraw(Canvas c) { 662 | if (isDismissed || outerCircleCenter == null) return; 663 | 664 | if (topBoundary > 0 && bottomBoundary > 0) { 665 | c.clipRect(0, topBoundary, getWidth(), bottomBoundary); 666 | } 667 | 668 | if (dimColor != -1) { 669 | c.drawColor(dimColor); 670 | } 671 | 672 | int saveCount; 673 | outerCirclePaint.setAlpha(outerCircleAlpha); 674 | if (shouldDrawShadow && outlineProvider == null) { 675 | saveCount = c.save(); 676 | { 677 | c.clipPath(outerCirclePath, Region.Op.DIFFERENCE); 678 | drawJitteredShadow(c); 679 | } 680 | c.restoreToCount(saveCount); 681 | } 682 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, outerCirclePaint); 683 | 684 | targetCirclePaint.setAlpha(targetCircleAlpha); 685 | if (targetCirclePulseAlpha > 0) { 686 | targetCirclePulsePaint.setAlpha(targetCirclePulseAlpha); 687 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), 688 | targetCirclePulseRadius, targetCirclePulsePaint); 689 | } 690 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), 691 | targetCircleRadius, targetCirclePaint); 692 | 693 | saveCount = c.save(); 694 | { 695 | c.translate(textBounds.left, textBounds.top); 696 | titlePaint.setAlpha(textAlpha); 697 | if (titleLayout != null) { 698 | titleLayout.draw(c); 699 | } 700 | 701 | if (descriptionLayout != null && titleLayout != null) { 702 | c.translate(0, titleLayout.getHeight() + TEXT_SPACING); 703 | descriptionPaint.setAlpha((int) (target.descriptionTextAlpha * textAlpha)); 704 | descriptionLayout.draw(c); 705 | } 706 | } 707 | c.restoreToCount(saveCount); 708 | 709 | saveCount = c.save(); 710 | { 711 | if (tintedTarget != null) { 712 | c.translate(targetBounds.centerX() - tintedTarget.getWidth() / 2, 713 | targetBounds.centerY() - tintedTarget.getHeight() / 2); 714 | c.drawBitmap(tintedTarget, 0, 0, targetCirclePaint); 715 | } else if (target.icon != null) { 716 | c.translate(targetBounds.centerX() - target.icon.getBounds().width() / 2, 717 | targetBounds.centerY() - target.icon.getBounds().height() / 2); 718 | target.icon.setAlpha(targetCirclePaint.getAlpha()); 719 | target.icon.draw(c); 720 | } 721 | } 722 | c.restoreToCount(saveCount); 723 | 724 | if (debug) { 725 | drawDebugInformation(c); 726 | } 727 | } 728 | 729 | @Override 730 | public boolean onTouchEvent(MotionEvent e) { 731 | lastTouchX = e.getX(); 732 | lastTouchY = e.getY(); 733 | return super.onTouchEvent(e); 734 | } 735 | 736 | @Override 737 | public boolean onKeyDown(int keyCode, KeyEvent event) { 738 | if (isVisible() && cancelable && keyCode == KeyEvent.KEYCODE_BACK) { 739 | event.startTracking(); 740 | return true; 741 | } 742 | 743 | return false; 744 | } 745 | 746 | @Override 747 | public boolean onKeyUp(int keyCode, KeyEvent event) { 748 | if (isVisible() && isInteractable && cancelable 749 | && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) { 750 | isInteractable = false; 751 | 752 | if (listener != null) { 753 | listener.onTargetCancel(this); 754 | } else { 755 | new Listener().onTargetCancel(this); 756 | } 757 | 758 | return true; 759 | } 760 | 761 | return false; 762 | } 763 | 764 | /** 765 | * Dismiss this view 766 | * @param tappedTarget If the user tapped the target or not 767 | * (results in different dismiss animations) 768 | */ 769 | public void dismiss(boolean tappedTarget) { 770 | isDismissing = true; 771 | pulseAnimation.cancel(); 772 | expandAnimation.cancel(); 773 | if (!visible || outerCircleCenter == null) { 774 | finishDismiss(tappedTarget); 775 | return; 776 | } 777 | if (tappedTarget) { 778 | dismissConfirmAnimation.start(); 779 | } else { 780 | dismissAnimation.start(); 781 | } 782 | } 783 | 784 | private void finishDismiss(boolean userInitiated) { 785 | onDismiss(userInitiated); 786 | ViewUtil.removeView(parent, TapTargetView.this); 787 | } 788 | 789 | /** Specify whether to draw a wireframe around the view, useful for debugging **/ 790 | public void setDrawDebug(boolean status) { 791 | if (debug != status) { 792 | debug = status; 793 | postInvalidate(); 794 | } 795 | } 796 | 797 | /** Returns whether this view is visible or not **/ 798 | public boolean isVisible() { 799 | return !isDismissed && visible; 800 | } 801 | 802 | void drawJitteredShadow(Canvas c) { 803 | final float baseAlpha = 0.20f * outerCircleAlpha; 804 | outerCircleShadowPaint.setStyle(Paint.Style.FILL_AND_STROKE); 805 | outerCircleShadowPaint.setAlpha((int) baseAlpha); 806 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM, outerCircleRadius, outerCircleShadowPaint); 807 | outerCircleShadowPaint.setStyle(Paint.Style.STROKE); 808 | final int numJitters = 7; 809 | for (int i = numJitters - 1; i > 0; --i) { 810 | outerCircleShadowPaint.setAlpha((int) ((i / (float) numJitters) * baseAlpha)); 811 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM , 812 | outerCircleRadius + (numJitters - i) * SHADOW_JITTER_DIM , outerCircleShadowPaint); 813 | } 814 | } 815 | 816 | void drawDebugInformation(Canvas c) { 817 | if (debugPaint == null) { 818 | debugPaint = new Paint(); 819 | debugPaint.setARGB(255, 255, 0, 0); 820 | debugPaint.setStyle(Paint.Style.STROKE); 821 | debugPaint.setStrokeWidth(UiUtil.dp(getContext(), 1)); 822 | } 823 | 824 | if (debugTextPaint == null) { 825 | debugTextPaint = new TextPaint(); 826 | debugTextPaint.setColor(0xFFFF0000); 827 | debugTextPaint.setTextSize(UiUtil.sp(getContext(), 16)); 828 | } 829 | 830 | // Draw wireframe 831 | debugPaint.setStyle(Paint.Style.STROKE); 832 | c.drawRect(textBounds, debugPaint); 833 | c.drawRect(targetBounds, debugPaint); 834 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], 10, debugPaint); 835 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], calculatedOuterCircleRadius - CIRCLE_PADDING, debugPaint); 836 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), TARGET_RADIUS + TARGET_PADDING, debugPaint); 837 | 838 | // Draw positions and dimensions 839 | debugPaint.setStyle(Paint.Style.FILL); 840 | final String debugText = 841 | "Text bounds: " + textBounds.toShortString() + "\n" + 842 | "Target bounds: " + targetBounds.toShortString() + "\n" + 843 | "Center: " + outerCircleCenter[0] + " " + outerCircleCenter[1] + "\n" + 844 | "View size: " + getWidth() + " " + getHeight() + "\n" + 845 | "Target bounds: " + targetBounds.toShortString(); 846 | 847 | if (debugStringBuilder == null) { 848 | debugStringBuilder = new SpannableStringBuilder(debugText); 849 | } else { 850 | debugStringBuilder.clear(); 851 | debugStringBuilder.append(debugText); 852 | } 853 | 854 | if (debugLayout == null) { 855 | debugLayout = new DynamicLayout(debugText, debugTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); 856 | } 857 | 858 | final int saveCount = c.save(); 859 | { 860 | debugPaint.setARGB(220, 0, 0, 0); 861 | c.translate(0.0f, topBoundary); 862 | c.drawRect(0.0f, 0.0f, debugLayout.getWidth(), debugLayout.getHeight(), debugPaint); 863 | debugPaint.setARGB(255, 255, 0, 0); 864 | debugLayout.draw(c); 865 | } 866 | c.restoreToCount(saveCount); 867 | } 868 | 869 | void drawTintedTarget() { 870 | final Drawable icon = target.icon; 871 | if (!shouldTintTarget || icon == null) { 872 | tintedTarget = null; 873 | return; 874 | } 875 | 876 | if (tintedTarget != null) return; 877 | 878 | tintedTarget = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), 879 | Bitmap.Config.ARGB_8888); 880 | final Canvas canvas = new Canvas(tintedTarget); 881 | icon.setColorFilter(new PorterDuffColorFilter( 882 | outerCirclePaint.getColor(), PorterDuff.Mode.SRC_ATOP)); 883 | icon.draw(canvas); 884 | icon.setColorFilter(null); 885 | } 886 | 887 | void updateTextLayouts() { 888 | final int textWidth = Math.min(getWidth(), TEXT_MAX_WIDTH) - TEXT_PADDING * 2; 889 | if (textWidth <= 0) { 890 | return; 891 | } 892 | 893 | titleLayout = new StaticLayout(title, titlePaint, textWidth, 894 | Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); 895 | 896 | if (description != null) { 897 | descriptionLayout = new StaticLayout(description, descriptionPaint, textWidth, 898 | Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false); 899 | } else { 900 | descriptionLayout = null; 901 | } 902 | } 903 | 904 | float halfwayLerp(float lerp) { 905 | if (lerp < 0.5f) { 906 | return lerp / 0.5f; 907 | } 908 | 909 | return (1.0f - lerp) / 0.5f; 910 | } 911 | 912 | float delayedLerp(float lerp, float threshold) { 913 | if (lerp < threshold) { 914 | return 0.0f; 915 | } 916 | 917 | return (lerp - threshold) / (1.0f - threshold); 918 | } 919 | 920 | void calculateDimensions() { 921 | textBounds = getTextBounds(); 922 | outerCircleCenter = getOuterCircleCenterPoint(); 923 | calculatedOuterCircleRadius = getOuterCircleRadius(outerCircleCenter[0], outerCircleCenter[1], textBounds, targetBounds); 924 | } 925 | 926 | void calculateDrawingBounds() { 927 | if (outerCircleCenter == null) { 928 | // Called dismiss before we got a chance to display the tap target 929 | // So we have no center -> cant determine the drawing bounds 930 | return; 931 | } 932 | drawingBounds.left = (int) Math.max(0, outerCircleCenter[0] - outerCircleRadius); 933 | drawingBounds.top = (int) Math.min(0, outerCircleCenter[1] - outerCircleRadius); 934 | drawingBounds.right = (int) Math.min(getWidth(), 935 | outerCircleCenter[0] + outerCircleRadius + CIRCLE_PADDING); 936 | drawingBounds.bottom = (int) Math.min(getHeight(), 937 | outerCircleCenter[1] + outerCircleRadius + CIRCLE_PADDING); 938 | } 939 | 940 | int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetBounds) { 941 | final int targetCenterX = targetBounds.centerX(); 942 | final int targetCenterY = targetBounds.centerY(); 943 | final int expandedRadius = (int) (1.1f * TARGET_RADIUS); 944 | final Rect expandedBounds = new Rect(targetCenterX, targetCenterY, targetCenterX, targetCenterY); 945 | expandedBounds.inset(-expandedRadius, -expandedRadius); 946 | 947 | final int textRadius = maxDistanceToPoints(centerX, centerY, textBounds); 948 | final int targetRadius = maxDistanceToPoints(centerX, centerY, expandedBounds); 949 | return Math.max(textRadius, targetRadius) + CIRCLE_PADDING; 950 | } 951 | 952 | Rect getTextBounds() { 953 | final int totalTextHeight = getTotalTextHeight(); 954 | final int totalTextWidth = getTotalTextWidth(); 955 | 956 | final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight; 957 | final int top; 958 | if (possibleTop > topBoundary) { 959 | Rect textSafeArea = new Rect(); 960 | getWindowVisibleDisplayFrame(textSafeArea); 961 | textSafeArea.inset(0, TEXT_SAFE_AREA_PADDING); 962 | top = Math.max(possibleTop, textSafeArea.top); 963 | } else { 964 | top = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING; 965 | } 966 | 967 | final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX(); 968 | final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS; 969 | final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth); 970 | final int right = Math.min(getWidth() - TEXT_PADDING, left + totalTextWidth); 971 | return new Rect(left, top, right, top + totalTextHeight); 972 | } 973 | 974 | int[] getOuterCircleCenterPoint() { 975 | if (inGutter(targetBounds.centerY()) || target.forceCenteredTarget) { 976 | return new int[]{targetBounds.centerX(), targetBounds.centerY()}; 977 | } 978 | 979 | final int targetRadius = Math.max(targetBounds.width(), targetBounds.height()) / 2 + TARGET_PADDING; 980 | final int totalTextHeight = getTotalTextHeight(); 981 | 982 | final boolean onTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight > 0; 983 | 984 | final int left = Math.min(textBounds.left, targetBounds.left - targetRadius); 985 | final int right = Math.max(textBounds.right, targetBounds.right + targetRadius); 986 | final int titleHeight = titleLayout == null ? 0 : titleLayout.getHeight(); 987 | final int centerY = onTop ? 988 | targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight + titleHeight 989 | : 990 | targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + titleHeight; 991 | 992 | return new int[] { (left + right) / 2, centerY }; 993 | } 994 | 995 | int getTotalTextHeight() { 996 | if (titleLayout == null) { 997 | return 0; 998 | } 999 | 1000 | if (descriptionLayout == null) { 1001 | return titleLayout.getHeight() + TEXT_SPACING; 1002 | } 1003 | 1004 | return titleLayout.getHeight() + descriptionLayout.getHeight() + TEXT_SPACING; 1005 | } 1006 | 1007 | int getTotalTextWidth() { 1008 | if (titleLayout == null) { 1009 | return 0; 1010 | } 1011 | 1012 | if (descriptionLayout == null) { 1013 | return titleLayout.getWidth(); 1014 | } 1015 | 1016 | return Math.max(titleLayout.getWidth(), descriptionLayout.getWidth()); 1017 | } 1018 | 1019 | boolean inGutter(int y) { 1020 | if (bottomBoundary > 0) { 1021 | return y < GUTTER_DIM || y > bottomBoundary - GUTTER_DIM; 1022 | } else { 1023 | return y < GUTTER_DIM || y > getHeight() - GUTTER_DIM; 1024 | } 1025 | } 1026 | 1027 | int maxDistanceToPoints(int x1, int y1, Rect bounds) { 1028 | final double tl = distance(x1, y1, bounds.left, bounds.top); 1029 | final double tr = distance(x1, y1, bounds.right, bounds.top); 1030 | final double bl = distance(x1, y1, bounds.left, bounds.bottom); 1031 | final double br = distance(x1, y1, bounds.right, bounds.bottom); 1032 | return (int) Math.max(tl, Math.max(tr, Math.max(bl, br))); 1033 | } 1034 | 1035 | double distance(int x1, int y1, int x2, int y2) { 1036 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)); 1037 | } 1038 | 1039 | void invalidateViewAndOutline(Rect bounds) { 1040 | invalidate(bounds); 1041 | if (outlineProvider != null && Build.VERSION.SDK_INT >= 21) { 1042 | invalidateOutline(); 1043 | } 1044 | } 1045 | } 1046 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.annotation.TargetApi; 19 | import android.graphics.drawable.Drawable; 20 | import android.os.Build; 21 | import android.text.TextUtils; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.ImageButton; 25 | import android.widget.ImageView; 26 | 27 | import java.util.ArrayList; 28 | import java.util.Stack; 29 | 30 | import androidx.annotation.IdRes; 31 | import androidx.annotation.Nullable; 32 | import androidx.appcompat.widget.Toolbar; 33 | 34 | class ToolbarTapTarget extends ViewTapTarget { 35 | ToolbarTapTarget(Toolbar toolbar, @IdRes int menuItemId, 36 | CharSequence title, @Nullable CharSequence description) { 37 | super(toolbar.findViewById(menuItemId), title, description); 38 | } 39 | 40 | ToolbarTapTarget(android.widget.Toolbar toolbar, @IdRes int menuItemId, 41 | CharSequence title, @Nullable CharSequence description) { 42 | super(toolbar.findViewById(menuItemId), title, description); 43 | } 44 | 45 | ToolbarTapTarget(Toolbar toolbar, boolean findNavView, 46 | CharSequence title, @Nullable CharSequence description) { 47 | super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description); 48 | } 49 | 50 | ToolbarTapTarget(android.widget.Toolbar toolbar, boolean findNavView, 51 | CharSequence title, @Nullable CharSequence description) { 52 | super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description); 53 | } 54 | 55 | private static ToolbarProxy proxyOf(Object instance) { 56 | if (instance == null) { 57 | throw new IllegalArgumentException("Given null instance"); 58 | } 59 | 60 | if (instance instanceof Toolbar) { 61 | return new SupportToolbarProxy((Toolbar) instance); 62 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP 63 | && instance instanceof android.widget.Toolbar) { 64 | return new StandardToolbarProxy((android.widget.Toolbar) instance); 65 | } 66 | 67 | throw new IllegalStateException("Couldn't provide proper toolbar proxy instance"); 68 | } 69 | 70 | private static View findNavView(Object instance) { 71 | final ToolbarProxy toolbar = proxyOf(instance); 72 | 73 | // First we try to find the view via its content description 74 | final CharSequence currentDescription = toolbar.getNavigationContentDescription(); 75 | final boolean hadContentDescription = !TextUtils.isEmpty(currentDescription); 76 | final CharSequence sentinel = hadContentDescription ? currentDescription : "taptarget-findme"; 77 | toolbar.setNavigationContentDescription(sentinel); 78 | 79 | final ArrayList possibleViews = new ArrayList<>(1); 80 | toolbar.findViewsWithText(possibleViews, sentinel, View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION); 81 | 82 | if (!hadContentDescription) { 83 | toolbar.setNavigationContentDescription(null); 84 | } 85 | 86 | if (possibleViews.size() > 0) { 87 | return possibleViews.get(0); 88 | } 89 | 90 | // If that doesn't work, we try to grab it via matching its drawable 91 | final Drawable navigationIcon = toolbar.getNavigationIcon(); 92 | if (navigationIcon == null) { 93 | throw new IllegalStateException("Toolbar does not have a navigation view set!"); 94 | } 95 | 96 | final int size = toolbar.getChildCount(); 97 | for (int i = 0; i < size; ++i) { 98 | final View child = toolbar.getChildAt(i); 99 | if (child instanceof ImageButton) { 100 | final Drawable childDrawable = ((ImageButton) child).getDrawable(); 101 | if (childDrawable == navigationIcon) { 102 | return child; 103 | } 104 | } 105 | } 106 | 107 | throw new IllegalStateException("Could not find navigation view for Toolbar!"); 108 | } 109 | 110 | private static View findOverflowView(Object instance) { 111 | final ToolbarProxy toolbar = proxyOf(instance); 112 | 113 | // First we try to find the overflow menu view via drawable matching 114 | final Drawable overflowDrawable = toolbar.getOverflowIcon(); 115 | if (overflowDrawable != null) { 116 | final Stack parents = new Stack<>(); 117 | parents.push((ViewGroup) toolbar.internalToolbar()); 118 | while (!parents.empty()) { 119 | ViewGroup parent = parents.pop(); 120 | final int size = parent.getChildCount(); 121 | for (int i = 0; i < size; ++i) { 122 | final View child = parent.getChildAt(i); 123 | if (child instanceof ViewGroup) { 124 | parents.push((ViewGroup) child); 125 | continue; 126 | } 127 | if (child instanceof ImageView) { 128 | final Drawable childDrawable = ((ImageView) child).getDrawable(); 129 | if (childDrawable == overflowDrawable) { 130 | return child; 131 | } 132 | } 133 | } 134 | } 135 | } 136 | 137 | // If that doesn't work, we fall-back to our last resort solution: Reflection 138 | // Toolbars contain an "ActionMenuView" which in turn contains an "ActionMenuPresenter". 139 | // The "ActionMenuPresenter" then holds a reference to an "OverflowMenuButton" which is the 140 | // desired target 141 | try { 142 | final Object actionMenuView = ReflectUtil.getPrivateField(toolbar.internalToolbar(), "mMenuView"); 143 | final Object actionMenuPresenter = ReflectUtil.getPrivateField(actionMenuView, "mPresenter"); 144 | return (View) ReflectUtil.getPrivateField(actionMenuPresenter, "mOverflowButton"); 145 | } catch (NoSuchFieldException e) { 146 | throw new IllegalStateException("Could not find overflow view for Toolbar!", e); 147 | } catch (IllegalAccessException e) { 148 | throw new IllegalStateException("Unable to access overflow view for Toolbar!", e); 149 | } 150 | } 151 | 152 | private interface ToolbarProxy { 153 | CharSequence getNavigationContentDescription(); 154 | 155 | void setNavigationContentDescription(CharSequence description); 156 | 157 | void findViewsWithText(ArrayList out, CharSequence toFind, int flags); 158 | 159 | Drawable getNavigationIcon(); 160 | 161 | @Nullable 162 | Drawable getOverflowIcon(); 163 | 164 | int getChildCount(); 165 | 166 | View getChildAt(int position); 167 | 168 | Object internalToolbar(); 169 | } 170 | 171 | private static class SupportToolbarProxy implements ToolbarProxy { 172 | private final Toolbar toolbar; 173 | 174 | SupportToolbarProxy(Toolbar toolbar) { 175 | this.toolbar = toolbar; 176 | } 177 | 178 | @Override 179 | public CharSequence getNavigationContentDescription() { 180 | return toolbar.getNavigationContentDescription(); 181 | } 182 | 183 | @Override 184 | public void setNavigationContentDescription(CharSequence description) { 185 | toolbar.setNavigationContentDescription(description); 186 | } 187 | 188 | @Override 189 | public void findViewsWithText(ArrayList out, CharSequence toFind, int flags) { 190 | toolbar.findViewsWithText(out, toFind, flags); 191 | } 192 | 193 | @Override 194 | public Drawable getNavigationIcon() { 195 | return toolbar.getNavigationIcon(); 196 | } 197 | 198 | @Override 199 | public Drawable getOverflowIcon() { 200 | return toolbar.getOverflowIcon(); 201 | } 202 | 203 | @Override 204 | public int getChildCount() { 205 | return toolbar.getChildCount(); 206 | } 207 | 208 | @Override 209 | public View getChildAt(int position) { 210 | return toolbar.getChildAt(position); 211 | } 212 | 213 | @Override 214 | public Object internalToolbar() { 215 | return toolbar; 216 | } 217 | } 218 | 219 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 220 | private static class StandardToolbarProxy implements ToolbarProxy { 221 | private final android.widget.Toolbar toolbar; 222 | 223 | StandardToolbarProxy(android.widget.Toolbar toolbar) { 224 | this.toolbar = toolbar; 225 | } 226 | 227 | @Override 228 | public CharSequence getNavigationContentDescription() { 229 | return toolbar.getNavigationContentDescription(); 230 | } 231 | 232 | @Override 233 | public void setNavigationContentDescription(CharSequence description) { 234 | toolbar.setNavigationContentDescription(description); 235 | } 236 | 237 | @Override 238 | public void findViewsWithText(ArrayList out, CharSequence toFind, int flags) { 239 | toolbar.findViewsWithText(out, toFind, flags); 240 | } 241 | 242 | @Override 243 | public Drawable getNavigationIcon() { 244 | return toolbar.getNavigationIcon(); 245 | } 246 | 247 | @Nullable 248 | @Override 249 | public Drawable getOverflowIcon() { 250 | if (Build.VERSION.SDK_INT >= 23) { 251 | return toolbar.getOverflowIcon(); 252 | } 253 | 254 | return null; 255 | } 256 | 257 | @Override 258 | public int getChildCount() { 259 | return toolbar.getChildCount(); 260 | } 261 | 262 | @Override 263 | public View getChildAt(int position) { 264 | return toolbar.getChildAt(position); 265 | } 266 | 267 | @Override 268 | public Object internalToolbar() { 269 | return toolbar; 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtil.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.content.Context; 19 | import android.content.res.Resources; 20 | 21 | import androidx.annotation.ColorRes; 22 | import androidx.annotation.DimenRes; 23 | import android.util.TypedValue; 24 | 25 | class UiUtil { 26 | UiUtil() { 27 | } 28 | 29 | /** Returns the given pixel value in dp **/ 30 | static int dp(Context context, int val) { 31 | return (int) TypedValue.applyDimension( 32 | TypedValue.COMPLEX_UNIT_DIP, val, context.getResources().getDisplayMetrics()); 33 | } 34 | 35 | /** Returns the given pixel value in sp **/ 36 | static int sp(Context context, int val) { 37 | return (int) TypedValue.applyDimension( 38 | TypedValue.COMPLEX_UNIT_SP, val, context.getResources().getDisplayMetrics()); 39 | } 40 | 41 | /** Returns the value of the desired theme integer attribute, or -1 if not found **/ 42 | static int themeIntAttr(Context context, String attr) { 43 | final Resources.Theme theme = context.getTheme(); 44 | if (theme == null) { 45 | return -1; 46 | } 47 | 48 | final TypedValue value = new TypedValue(); 49 | final int id = context.getResources().getIdentifier(attr, "attr", context.getPackageName()); 50 | 51 | if (id == 0) { 52 | // Not found 53 | return -1; 54 | } 55 | 56 | theme.resolveAttribute(id, value, true); 57 | return value.data; 58 | } 59 | 60 | /** Modifies the alpha value of the given ARGB color **/ 61 | static int setAlpha(int argb, float alpha) { 62 | if (alpha > 1.0f) { 63 | alpha = 1.0f; 64 | } else if (alpha <= 0.0f) { 65 | alpha = 0.0f; 66 | } 67 | 68 | return ((int) ((argb >>> 24) * alpha) << 24) | (argb & 0x00FFFFFF); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewTapTarget.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.graphics.Bitmap; 19 | import android.graphics.Canvas; 20 | import android.graphics.Rect; 21 | import android.graphics.drawable.BitmapDrawable; 22 | import androidx.annotation.Nullable; 23 | import android.view.View; 24 | 25 | class ViewTapTarget extends TapTarget { 26 | final View view; 27 | 28 | ViewTapTarget(View view, CharSequence title, @Nullable CharSequence description) { 29 | super(title, description); 30 | if (view == null) { 31 | throw new IllegalArgumentException("Given null view to target"); 32 | } 33 | this.view = view; 34 | } 35 | 36 | @Override 37 | public void onReady(final Runnable runnable) { 38 | ViewUtil.onLaidOut(view, new Runnable() { 39 | @Override 40 | public void run() { 41 | // Cache bounds 42 | final int[] location = new int[2]; 43 | view.getLocationOnScreen(location); 44 | bounds = new Rect(location[0], location[1], 45 | location[0] + view.getWidth(), location[1] + view.getHeight()); 46 | 47 | if (icon == null && view.getWidth() > 0 && view.getHeight() > 0) { 48 | final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 49 | final Canvas canvas = new Canvas(viewBitmap); 50 | view.draw(canvas); 51 | icon = new BitmapDrawable(view.getContext().getResources(), viewBitmap); 52 | icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 53 | } 54 | 55 | runnable.run(); 56 | } 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewUtil.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 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.getkeepsafe.taptargetview; 17 | 18 | import android.os.Build; 19 | import androidx.core.view.ViewCompat; 20 | import android.view.View; 21 | import android.view.ViewManager; 22 | import android.view.ViewTreeObserver; 23 | 24 | class ViewUtil { 25 | ViewUtil() { 26 | } 27 | 28 | /** Returns whether or not the view has been laid out **/ 29 | private static boolean isLaidOut(View view) { 30 | return ViewCompat.isLaidOut(view) && view.getWidth() > 0 && view.getHeight() > 0; 31 | } 32 | 33 | /** Executes the given {@link java.lang.Runnable} when the view is laid out **/ 34 | static void onLaidOut(final View view, final Runnable runnable) { 35 | if (isLaidOut(view)) { 36 | runnable.run(); 37 | return; 38 | } 39 | 40 | final ViewTreeObserver observer = view.getViewTreeObserver(); 41 | observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 42 | @Override 43 | public void onGlobalLayout() { 44 | final ViewTreeObserver trueObserver; 45 | 46 | if (observer.isAlive()) { 47 | trueObserver = observer; 48 | } else { 49 | trueObserver = view.getViewTreeObserver(); 50 | } 51 | 52 | removeOnGlobalLayoutListener(trueObserver, this); 53 | 54 | runnable.run(); 55 | } 56 | }); 57 | } 58 | 59 | @SuppressWarnings("deprecation") 60 | static void removeOnGlobalLayoutListener(ViewTreeObserver observer, 61 | ViewTreeObserver.OnGlobalLayoutListener listener) { 62 | if (Build.VERSION.SDK_INT >= 16) { 63 | observer.removeOnGlobalLayoutListener(listener); 64 | } else { 65 | observer.removeGlobalOnLayoutListener(listener); 66 | } 67 | } 68 | 69 | static void removeView(ViewManager parent, View child) { 70 | if (parent == null || child == null) { 71 | return; 72 | } 73 | 74 | try { 75 | parent.removeView(child); 76 | } catch (Exception ignored) { 77 | // This catch exists for modified versions of Android that have a buggy ViewGroup 78 | // implementation. See b.android.com/77639, #121 and #49 79 | } 80 | } 81 | } 82 | --------------------------------------------------------------------------------