├── .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 |
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
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
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 |
--------------------------------------------------------------------------------