├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── uk │ │ └── co │ │ └── deanwild │ │ └── materialshowcaseview │ │ ├── CircularRevealAnimationFactory.java │ │ ├── FadeAnimationFactory.java │ │ ├── IAnimationFactory.java │ │ ├── IDetachedListener.java │ │ ├── IShowcaseListener.java │ │ ├── MaterialShowcaseSequence.java │ │ ├── MaterialShowcaseView.java │ │ ├── PrefsManager.java │ │ ├── ShowcaseConfig.java │ │ ├── ShowcaseTooltip.java │ │ ├── shape │ │ ├── CircleShape.java │ │ ├── NoShape.java │ │ ├── OvalShape.java │ │ ├── RectangleShape.java │ │ └── Shape.java │ │ └── target │ │ ├── Target.java │ │ └── ViewTarget.java │ └── res │ └── layout │ └── showcase_content.xml ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── uk │ │ └── co │ │ └── deanwild │ │ └── materialshowcaseviewsample │ │ ├── CustomExample.java │ │ ├── MainActivity.java │ │ ├── SequenceExample.java │ │ ├── SimpleSingleExample.java │ │ └── TooltipExample.java │ └── res │ ├── drawable-xxhdpi │ ├── ic_android_white_24dp.png │ └── ic_edit.png │ ├── layout │ ├── activity_custom_example.xml │ ├── activity_main.xml │ ├── activity_sequence_example.xml │ ├── activity_simple_single_example.xml │ └── activity_tooltip_example.xml │ ├── menu │ └── activity_custom_example.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | /*/build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | # Proguard folder generated by Eclipse 24 | proguard/ 25 | 26 | # Log Files 27 | *.log 28 | 29 | .idea 30 | #.idea 31 | /.idea 32 | 33 | #imls 34 | *.iml 35 | -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Looking for collaborators to help maintain this library, drop me a line at me@deanwild.co.uk if you want to help.* 2 | 3 | # MaterialShowcaseView 4 | A Material Design themed ShowcaseView for Android 5 | 6 | 7 | This library is heavily inspired by the original [ShowcaseView library][1]. 8 | 9 | Since Google introduced the Material design philosophy I have seen quite a few apps with a nice clean, flat showcase view (the Youtube app is a good example). The only library out there however is the [original one][1]. This was a great library for a long time but the theming is now looking a bit dated. 10 | 11 | ![Logo](http://i.imgur.com/QIMYRJh.png) 12 | 13 | 14 | ![Animation][2] 15 | 16 | # Gradle 17 | -------- 18 | 19 | [![jitpack][4]][5] 20 | 21 | Add the jitpack repo to your your project's build.gradle at the end of repositories [Why?](#why-jitpack) 22 | 23 | /build.gradle 24 | ```groovy 25 | allprojects { 26 | repositories { 27 | jcenter() 28 | maven { url "https://jitpack.io" } 29 | } 30 | } 31 | ``` 32 | 33 | Then add the dependency to your module's build.gradle: 34 | 35 | /app/build.gradle 36 | ```groovy 37 | compile 'com.github.deano2390:MaterialShowcaseView:1.3.7' 38 | ``` 39 | 40 | NOTE: Some people have mentioned that they needed to add the @aar suffix to get it to resolve from JitPack: 41 | ```groovy 42 | compile 'com.github.deano2390:MaterialShowcaseView:1.3.7@aar' 43 | ``` 44 | 45 | # How to use 46 | -------- 47 | This is the basic usage of a single showcase view, you should check out the sample app for more advanced usage. 48 | 49 | ```java 50 | 51 | // single example 52 | new MaterialShowcaseView.Builder(this) 53 | .setTarget(mButtonShow) 54 | .setDismissText("GOT IT") 55 | .setContentText("This is some amazing feature you should know about") 56 | .setDelay(withDelay) // optional but starting animations immediately in onCreate can make them choppy 57 | .singleUse(SHOWCASE_ID) // provide a unique ID used to ensure it is only shown once 58 | .show(); 59 | 60 | 61 | 62 | 63 | // sequence example 64 | ShowcaseConfig config = new ShowcaseConfig(); 65 | config.setDelay(500); // half second between each showcase view 66 | 67 | MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(this, SHOWCASE_ID); 68 | 69 | sequence.setConfig(config); 70 | 71 | sequence.addSequenceItem(mButtonOne, 72 | "This is button one", "GOT IT"); 73 | 74 | sequence.addSequenceItem(mButtonTwo, 75 | "This is button two", "GOT IT"); 76 | 77 | sequence.addSequenceItem(mButtonThree, 78 | "This is button three", "GOT IT"); 79 | 80 | sequence.start(); 81 | 82 | ``` 83 | 84 | # Why Jitpack 85 | ------------ 86 | Publishing libraries to Maven is a chore that takes time and effort. Jitpack.io allows me to release without ever leaving GitHub so I can release easily and more often. 87 | 88 | # Apps using MaterialShowcaseView 89 | --------------------------------- 90 | 91 | * [Say It! - English Learning](https://play.google.com/store/apps/details?id=com.cesarsk.say_it) : An Android App aimed to improve your English Pronunciation. 92 | * [Github Page](https://github.com/cesarsk/say_it) 93 | 94 | * [Queskr](https://play.google.com/store/apps/details?id=com.queskr.www.queskrandroidapp) : Social Q&A at your fingertips 95 | 96 | # Learning Resources 97 | [https://medium.com/@yashgirdhar/android-material-showcase-view-part-1-22abd5c65b85][6] 98 | 99 | [https://1bucketlist.blogspot.com/2017/03/android-material-showcase-view-1.html][7] 100 | 101 | [https://blog.fossasia.org/tag/material-showcase-view/][8] 102 | 103 | 104 | 105 | # License 106 | ------- 107 | 108 | Copyright 2015 Dean Wild 109 | 110 | Licensed under the Apache License, Version 2.0 (the "License"); 111 | you may not use this file except in compliance with the License. 112 | You may obtain a copy of the License at 113 | 114 | http://www.apache.org/licenses/LICENSE-2.0 115 | 116 | Unless required by applicable law or agreed to in writing, software 117 | distributed under the License is distributed on an "AS IS" BASIS, 118 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 119 | See the License for the specific language governing permissions and 120 | limitations under the License. 121 | 122 | 123 | 124 | 125 | 126 | [1]: https://github.com/amlcurran/ShowcaseView 127 | [2]: http://i.imgur.com/rFHENgz.gif 128 | [3]: https://code.google.com/p/android-flowtextview/ 129 | [4]: https://img.shields.io/github/release/deano2390/MaterialShowcaseView.svg?label=JitPack 130 | [5]: https://jitpack.io/#deano2390/MaterialShowcaseView 131 | [6]: https://medium.com/@yashgirdhar/android-material-showcase-view-part-1-22abd5c65b85 132 | [7]: https://1bucketlist.blogspot.com/2017/03/android-material-showcase-view-1.html 133 | [8]: https://blog.fossasia.org/tag/material-showcase-view/ 134 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | google() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.6.4' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | mavenCentral() 16 | jcenter() 17 | maven { url "https://jitpack.io" } 18 | google() 19 | } 20 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deano2390/MaterialShowcaseView/528080516c72c2c440ed58675a6be30096a458d7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Mar 29 13:11:06 GMT 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /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 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 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 Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | minSdkVersion 12 10 | targetSdkVersion 28 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | } 19 | 20 | dependencies { 21 | implementation fileTree(dir: 'libs', include: ['*.jar']) 22 | } 23 | 24 | afterEvaluate { 25 | publishing { 26 | publications { 27 | release(MavenPublication) { 28 | from components.release 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /library/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/deanwild/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/CircularRevealAnimationFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ObjectAnimator; 6 | import android.annotation.TargetApi; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.view.View; 10 | import android.view.ViewAnimationUtils; 11 | import android.view.animation.AccelerateDecelerateInterpolator; 12 | 13 | 14 | public class CircularRevealAnimationFactory implements IAnimationFactory { 15 | 16 | private static final String ALPHA = "alpha"; 17 | private static final float INVISIBLE = 0f; 18 | private static final float VISIBLE = 1f; 19 | 20 | private final AccelerateDecelerateInterpolator interpolator; 21 | 22 | public CircularRevealAnimationFactory() { 23 | interpolator = new AccelerateDecelerateInterpolator(); 24 | } 25 | 26 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 27 | @Override 28 | public void animateInView(View target, Point point, long duration, final AnimationStartListener listener) { 29 | Animator animator = ViewAnimationUtils.createCircularReveal(target, point.x, point.y, 0, 30 | target.getWidth() > target.getHeight() ? target.getWidth() : target.getHeight()); 31 | animator.setDuration(duration).addListener(new Animator.AnimatorListener() { 32 | @Override 33 | public void onAnimationStart(Animator animation) { 34 | listener.onAnimationStart(); 35 | } 36 | 37 | @Override 38 | public void onAnimationEnd(Animator animation) { 39 | 40 | } 41 | 42 | @Override 43 | public void onAnimationCancel(Animator animation) { 44 | 45 | } 46 | 47 | @Override 48 | public void onAnimationRepeat(Animator animation) { 49 | 50 | } 51 | }); 52 | 53 | animator.start(); 54 | } 55 | 56 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 57 | @Override 58 | public void animateOutView(View target, Point point, long duration, final AnimationEndListener listener) { 59 | Animator animator = ViewAnimationUtils.createCircularReveal(target, point.x, point.y, 60 | target.getWidth() > target.getHeight() ? target.getWidth() : target.getHeight(), 0); 61 | animator.setDuration(duration).addListener(new Animator.AnimatorListener() { 62 | @Override 63 | public void onAnimationStart(Animator animation) { 64 | 65 | } 66 | 67 | @Override 68 | public void onAnimationEnd(Animator animation) { 69 | listener.onAnimationEnd(); 70 | } 71 | 72 | @Override 73 | public void onAnimationCancel(Animator animation) { 74 | 75 | } 76 | 77 | @Override 78 | public void onAnimationRepeat(Animator animation) { 79 | 80 | } 81 | }); 82 | 83 | animator.start(); 84 | } 85 | 86 | @Override 87 | public void animateTargetToPoint(MaterialShowcaseView showcaseView, Point point) { 88 | AnimatorSet set = new AnimatorSet(); 89 | ObjectAnimator xAnimator = ObjectAnimator.ofInt(showcaseView, "showcaseX", point.x); 90 | ObjectAnimator yAnimator = ObjectAnimator.ofInt(showcaseView, "showcaseY", point.y); 91 | set.playTogether(xAnimator, yAnimator); 92 | set.setInterpolator(interpolator); 93 | set.start(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/FadeAnimationFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.animation.Animator; 4 | import android.animation.AnimatorSet; 5 | import android.animation.ObjectAnimator; 6 | import android.annotation.TargetApi; 7 | import android.graphics.Point; 8 | import android.os.Build; 9 | import android.view.View; 10 | import android.view.ViewAnimationUtils; 11 | import android.view.animation.AccelerateDecelerateInterpolator; 12 | 13 | 14 | public class FadeAnimationFactory implements IAnimationFactory{ 15 | 16 | private static final String ALPHA = "alpha"; 17 | private static final float INVISIBLE = 0f; 18 | private static final float VISIBLE = 1f; 19 | 20 | private final AccelerateDecelerateInterpolator interpolator; 21 | 22 | public FadeAnimationFactory() { 23 | interpolator = new AccelerateDecelerateInterpolator(); 24 | } 25 | 26 | @Override 27 | public void animateInView(View target, Point point, long duration, final AnimationStartListener listener) { 28 | ObjectAnimator oa = ObjectAnimator.ofFloat(target, ALPHA, INVISIBLE, VISIBLE); 29 | oa.setDuration(duration).addListener(new Animator.AnimatorListener() { 30 | @Override 31 | public void onAnimationStart(Animator animator) { 32 | listener.onAnimationStart(); 33 | } 34 | 35 | @Override 36 | public void onAnimationEnd(Animator animator) { 37 | } 38 | 39 | @Override 40 | public void onAnimationCancel(Animator animator) { 41 | } 42 | 43 | @Override 44 | public void onAnimationRepeat(Animator animator) { 45 | } 46 | }); 47 | oa.start(); 48 | } 49 | 50 | @Override 51 | public void animateOutView(View target, Point point, long duration, final AnimationEndListener listener) { 52 | ObjectAnimator oa = ObjectAnimator.ofFloat(target, ALPHA, INVISIBLE); 53 | oa.setDuration(duration).addListener(new Animator.AnimatorListener() { 54 | @Override 55 | public void onAnimationStart(Animator animator) { 56 | } 57 | 58 | @Override 59 | public void onAnimationEnd(Animator animator) { 60 | listener.onAnimationEnd(); 61 | } 62 | 63 | @Override 64 | public void onAnimationCancel(Animator animator) { 65 | } 66 | 67 | @Override 68 | public void onAnimationRepeat(Animator animator) { 69 | } 70 | }); 71 | oa.start(); 72 | } 73 | 74 | @Override 75 | public void animateTargetToPoint(MaterialShowcaseView showcaseView, Point point) { 76 | AnimatorSet set = new AnimatorSet(); 77 | ObjectAnimator xAnimator = ObjectAnimator.ofInt(showcaseView, "showcaseX", point.x); 78 | ObjectAnimator yAnimator = ObjectAnimator.ofInt(showcaseView, "showcaseY", point.y); 79 | set.playTogether(xAnimator, yAnimator); 80 | set.setInterpolator(interpolator); 81 | set.start(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/IAnimationFactory.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.graphics.Point; 4 | import android.view.View; 5 | 6 | 7 | public interface IAnimationFactory { 8 | 9 | void animateInView(View target, Point point, long duration, AnimationStartListener listener); 10 | 11 | void animateOutView(View target, Point point, long duration, AnimationEndListener listener); 12 | 13 | void animateTargetToPoint(MaterialShowcaseView showcaseView, Point point); 14 | 15 | public interface AnimationStartListener { 16 | void onAnimationStart(); 17 | } 18 | 19 | public interface AnimationEndListener { 20 | void onAnimationEnd(); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/IDetachedListener.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | 4 | public interface IDetachedListener { 5 | void onShowcaseDetached(MaterialShowcaseView showcaseView, boolean wasDismissed, boolean wasSkipped); 6 | } 7 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/IShowcaseListener.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | 4 | public interface IShowcaseListener { 5 | void onShowcaseDisplayed(MaterialShowcaseView showcaseView); 6 | void onShowcaseDismissed(MaterialShowcaseView showcaseView); 7 | } 8 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/MaterialShowcaseSequence.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.app.Activity; 4 | import android.view.View; 5 | 6 | import java.util.LinkedList; 7 | import java.util.Queue; 8 | 9 | 10 | public class MaterialShowcaseSequence implements IDetachedListener { 11 | 12 | PrefsManager mPrefsManager; 13 | Queue mShowcaseQueue; 14 | private boolean mSingleUse = false; 15 | Activity mActivity; 16 | private ShowcaseConfig mConfig; 17 | private int mSequencePosition = 0; 18 | 19 | private OnSequenceItemShownListener mOnItemShownListener = null; 20 | private OnSequenceItemDismissedListener mOnItemDismissedListener = null; 21 | 22 | public MaterialShowcaseSequence(Activity activity) { 23 | mActivity = activity; 24 | mShowcaseQueue = new LinkedList<>(); 25 | } 26 | 27 | public MaterialShowcaseSequence(Activity activity, String sequenceID) { 28 | this(activity); 29 | this.singleUse(sequenceID); 30 | } 31 | 32 | public MaterialShowcaseSequence addSequenceItem(View targetView, String content, String dismissText) { 33 | addSequenceItem(targetView, "", content, dismissText); 34 | return this; 35 | } 36 | 37 | public MaterialShowcaseSequence addSequenceItem(View targetView, String title, String content, String dismissText) { 38 | 39 | MaterialShowcaseView sequenceItem = new MaterialShowcaseView.Builder(mActivity) 40 | .setTarget(targetView) 41 | .setTitleText(title) 42 | .setDismissText(dismissText) 43 | .setContentText(content) 44 | .setSequence(true) 45 | .build(); 46 | 47 | if (mConfig != null) { 48 | sequenceItem.setConfig(mConfig); 49 | } 50 | 51 | mShowcaseQueue.add(sequenceItem); 52 | return this; 53 | } 54 | 55 | public MaterialShowcaseSequence addSequenceItem(MaterialShowcaseView sequenceItem) { 56 | 57 | if (mConfig != null) { 58 | sequenceItem.setConfig(mConfig); 59 | } 60 | 61 | mShowcaseQueue.add(sequenceItem); 62 | return this; 63 | } 64 | 65 | public MaterialShowcaseSequence singleUse(String sequenceID) { 66 | mSingleUse = true; 67 | mPrefsManager = new PrefsManager(mActivity, sequenceID); 68 | return this; 69 | } 70 | 71 | public void setOnItemShownListener(OnSequenceItemShownListener listener) { 72 | this.mOnItemShownListener = listener; 73 | } 74 | 75 | public void setOnItemDismissedListener(OnSequenceItemDismissedListener listener) { 76 | this.mOnItemDismissedListener = listener; 77 | } 78 | 79 | public boolean hasFired() { 80 | 81 | if (mPrefsManager.getSequenceStatus() == PrefsManager.SEQUENCE_FINISHED) { 82 | return true; 83 | } 84 | 85 | return false; 86 | } 87 | 88 | public void start() { 89 | 90 | /** 91 | * Check if we've already shot our bolt and bail out if so * 92 | */ 93 | if (mSingleUse) { 94 | if (hasFired()) { 95 | return; 96 | } 97 | 98 | /** 99 | * See if we have started this sequence before, if so then skip to the point we reached before 100 | * instead of showing the user everything from the start 101 | */ 102 | mSequencePosition = mPrefsManager.getSequenceStatus(); 103 | 104 | if (mSequencePosition > 0) { 105 | for (int i = 0; i < mSequencePosition; i++) { 106 | mShowcaseQueue.poll(); 107 | } 108 | } 109 | } 110 | 111 | 112 | // do start 113 | if (mShowcaseQueue.size() > 0) 114 | showNextItem(); 115 | } 116 | 117 | private void showNextItem() { 118 | 119 | if (mShowcaseQueue.size() > 0 && !mActivity.isFinishing()) { 120 | MaterialShowcaseView sequenceItem = mShowcaseQueue.remove(); 121 | sequenceItem.setDetachedListener(this); 122 | sequenceItem.show(mActivity); 123 | if (mOnItemShownListener != null) { 124 | mOnItemShownListener.onShow(sequenceItem, mSequencePosition); 125 | } 126 | } else { 127 | /** 128 | * We've reached the end of the sequence, save the fired state 129 | */ 130 | if (mSingleUse) { 131 | mPrefsManager.setFired(); 132 | } 133 | } 134 | } 135 | 136 | private void skipTutorial() { 137 | 138 | mShowcaseQueue.clear(); 139 | 140 | if (mShowcaseQueue.size() > 0 && !mActivity.isFinishing()) { 141 | MaterialShowcaseView sequenceItem = mShowcaseQueue.remove(); 142 | sequenceItem.setDetachedListener(this); 143 | sequenceItem.show(mActivity); 144 | if (mOnItemShownListener != null) { 145 | mOnItemShownListener.onShow(sequenceItem, mSequencePosition); 146 | } 147 | } else { 148 | /** 149 | * We've reached the end of the sequence, save the fired state 150 | */ 151 | if (mSingleUse) { 152 | mPrefsManager.setFired(); 153 | } 154 | } 155 | } 156 | 157 | 158 | @Override 159 | public void onShowcaseDetached(MaterialShowcaseView showcaseView, boolean wasDismissed, boolean wasSkipped) { 160 | 161 | showcaseView.setDetachedListener(null); 162 | 163 | /** 164 | * We're only interested if the showcase was purposefully dismissed 165 | */ 166 | if (wasDismissed) { 167 | 168 | if (mOnItemDismissedListener != null) { 169 | mOnItemDismissedListener.onDismiss(showcaseView, mSequencePosition); 170 | } 171 | 172 | /** 173 | * If so, update the prefsManager so we can potentially resume this sequence in the future 174 | */ 175 | if (mPrefsManager != null) { 176 | mSequencePosition++; 177 | mPrefsManager.setSequenceStatus(mSequencePosition); 178 | } 179 | 180 | showNextItem(); 181 | } 182 | 183 | if(wasSkipped){ 184 | if (mOnItemDismissedListener != null) { 185 | mOnItemDismissedListener.onDismiss(showcaseView, mSequencePosition); 186 | } 187 | 188 | /** 189 | * If so, update the prefsManager so we can potentially resume this sequence in the future 190 | */ 191 | if (mPrefsManager != null) { 192 | mSequencePosition++; 193 | mPrefsManager.setSequenceStatus(mSequencePosition); 194 | } 195 | 196 | skipTutorial(); 197 | } 198 | } 199 | 200 | public void setConfig(ShowcaseConfig config) { 201 | this.mConfig = config; 202 | } 203 | 204 | public interface OnSequenceItemShownListener { 205 | void onShow(MaterialShowcaseView itemView, int position); 206 | } 207 | 208 | public interface OnSequenceItemDismissedListener { 209 | void onDismiss(MaterialShowcaseView itemView, int position); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/MaterialShowcaseView.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.graphics.Bitmap; 7 | import android.graphics.Canvas; 8 | import android.graphics.Color; 9 | import android.graphics.Paint; 10 | import android.graphics.Point; 11 | import android.graphics.PorterDuff; 12 | import android.graphics.PorterDuffXfermode; 13 | import android.graphics.Rect; 14 | import android.graphics.Typeface; 15 | import android.os.Build; 16 | import android.os.Handler; 17 | import android.text.TextUtils; 18 | import android.util.AttributeSet; 19 | import android.view.Gravity; 20 | import android.view.LayoutInflater; 21 | import android.view.MotionEvent; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.view.ViewTreeObserver; 25 | import android.widget.FrameLayout; 26 | import android.widget.TextView; 27 | 28 | import java.util.ArrayList; 29 | import java.util.List; 30 | 31 | import uk.co.deanwild.materialshowcaseview.shape.CircleShape; 32 | import uk.co.deanwild.materialshowcaseview.shape.NoShape; 33 | import uk.co.deanwild.materialshowcaseview.shape.OvalShape; 34 | import uk.co.deanwild.materialshowcaseview.shape.RectangleShape; 35 | import uk.co.deanwild.materialshowcaseview.shape.Shape; 36 | import uk.co.deanwild.materialshowcaseview.target.Target; 37 | import uk.co.deanwild.materialshowcaseview.target.ViewTarget; 38 | 39 | 40 | /** 41 | * Helper class to show a sequence of showcase views. 42 | */ 43 | public class MaterialShowcaseView extends FrameLayout implements View.OnTouchListener, View.OnClickListener { 44 | 45 | public static final int DEFAULT_SHAPE_PADDING = 10; 46 | public static final int DEFAULT_TOOLTIP_MARGIN = 10; 47 | long DEFAULT_DELAY = 0; 48 | long DEFAULT_FADE_TIME = 300; 49 | 50 | private int mOldHeight; 51 | private int mOldWidth; 52 | private Bitmap mBitmap;// = new WeakReference<>(null); 53 | private Canvas mCanvas; 54 | private Paint mEraser; 55 | private Target mTarget; 56 | private Shape mShape; 57 | private int mXPosition; 58 | private int mYPosition; 59 | private boolean mWasDismissed = false, mWasSkipped = false; 60 | private int mShapePadding = DEFAULT_SHAPE_PADDING; 61 | private int tooltipMargin = DEFAULT_TOOLTIP_MARGIN; 62 | 63 | private View mContentBox; 64 | private TextView mTitleTextView; 65 | private TextView mContentTextView; 66 | private TextView mDismissButton; 67 | private boolean mHasCustomGravity; 68 | private TextView mSkipButton; 69 | private int mGravity; 70 | private int mContentBottomMargin; 71 | private int mContentTopMargin; 72 | private boolean mDismissOnTouch = false; 73 | private boolean mShouldRender = false; // flag to decide when we should actually render 74 | private boolean mRenderOverNav = false; 75 | private int mMaskColour; 76 | private IAnimationFactory mAnimationFactory; 77 | private boolean mShouldAnimate = true; 78 | private boolean mUseFadeAnimation = false; 79 | private long mFadeDurationInMillis = DEFAULT_FADE_TIME; 80 | private Handler mHandler; 81 | private long mDelayInMillis = DEFAULT_DELAY; 82 | private int mBottomMargin = 0; 83 | private boolean mSingleUse = false; // should display only once 84 | private PrefsManager mPrefsManager; // used to store state doe single use mode 85 | List mListeners; // external listeners who want to observe when we show and dismiss 86 | private UpdateOnGlobalLayout mLayoutListener; 87 | private IDetachedListener mDetachedListener; 88 | private boolean mTargetTouchable = false; 89 | private boolean mDismissOnTargetTouch = true; 90 | 91 | private boolean isSequence = false; 92 | 93 | private ShowcaseTooltip toolTip; 94 | private boolean toolTipShown; 95 | 96 | public MaterialShowcaseView(Context context) { 97 | super(context); 98 | init(context); 99 | } 100 | 101 | public MaterialShowcaseView(Context context, AttributeSet attrs) { 102 | super(context, attrs); 103 | init(context); 104 | } 105 | 106 | public MaterialShowcaseView(Context context, AttributeSet attrs, int defStyleAttr) { 107 | super(context, attrs, defStyleAttr); 108 | init(context); 109 | } 110 | 111 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 112 | public MaterialShowcaseView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 113 | super(context, attrs, defStyleAttr, defStyleRes); 114 | init(context); 115 | } 116 | 117 | 118 | private void init(Context context) { 119 | setWillNotDraw(false); 120 | 121 | mListeners = new ArrayList<>(); 122 | 123 | // make sure we add a global layout listener so we can adapt to changes 124 | mLayoutListener = new UpdateOnGlobalLayout(); 125 | getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener); 126 | 127 | // consume touch events 128 | setOnTouchListener(this); 129 | 130 | mMaskColour = Color.parseColor(ShowcaseConfig.DEFAULT_MASK_COLOUR); 131 | setVisibility(INVISIBLE); 132 | 133 | 134 | View contentView = LayoutInflater.from(getContext()).inflate(R.layout.showcase_content, this, true); 135 | mContentBox = contentView.findViewById(R.id.content_box); 136 | mTitleTextView = contentView.findViewById(R.id.tv_title); 137 | mContentTextView = contentView.findViewById(R.id.tv_content); 138 | mDismissButton = contentView.findViewById(R.id.tv_dismiss); 139 | mDismissButton.setOnClickListener(this); 140 | 141 | mSkipButton = contentView.findViewById(R.id.tv_skip); 142 | mSkipButton.setOnClickListener(this); 143 | } 144 | 145 | 146 | /** 147 | * Interesting drawing stuff. 148 | * We draw a block of semi transparent colour to fill the whole screen then we draw of transparency 149 | * to create a circular "viewport" through to the underlying content 150 | * 151 | * @param canvas 152 | */ 153 | @Override 154 | protected void onDraw(Canvas canvas) { 155 | super.onDraw(canvas); 156 | 157 | // don't bother drawing if we're not ready 158 | if (!mShouldRender) return; 159 | 160 | // get current dimensions 161 | final int width = getMeasuredWidth(); 162 | final int height = getMeasuredHeight(); 163 | 164 | // don't bother drawing if there is nothing to draw on 165 | if (width <= 0 || height <= 0) return; 166 | 167 | // build a new canvas if needed i.e first pass or new dimensions 168 | if (mBitmap == null || mCanvas == null || mOldHeight != height || mOldWidth != width) { 169 | 170 | if (mBitmap != null) mBitmap.recycle(); 171 | 172 | mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 173 | 174 | mCanvas = new Canvas(mBitmap); 175 | } 176 | 177 | // save our 'old' dimensions 178 | mOldWidth = width; 179 | mOldHeight = height; 180 | 181 | // clear canvas 182 | mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); 183 | 184 | // draw solid background 185 | mCanvas.drawColor(mMaskColour); 186 | 187 | // Prepare eraser Paint if needed 188 | if (mEraser == null) { 189 | mEraser = new Paint(); 190 | mEraser.setColor(0xFFFFFFFF); 191 | mEraser.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 192 | mEraser.setFlags(Paint.ANTI_ALIAS_FLAG); 193 | } 194 | 195 | // draw (erase) shape 196 | mShape.draw(mCanvas, mEraser, mXPosition, mYPosition); 197 | 198 | // Draw the bitmap on our views canvas. 199 | canvas.drawBitmap(mBitmap, 0, 0, null); 200 | } 201 | 202 | @Override 203 | protected void onDetachedFromWindow() { 204 | super.onDetachedFromWindow(); 205 | 206 | /** 207 | * If we're being detached from the window without the mWasDismissed flag then we weren't purposefully dismissed 208 | * Probably due to an orientation change or user backed out of activity. 209 | * Ensure we reset the flag so the showcase display again. 210 | */ 211 | if (!mWasDismissed && mSingleUse && mPrefsManager != null) { 212 | mPrefsManager.resetShowcase(); 213 | } 214 | 215 | 216 | notifyOnDismissed(); 217 | 218 | } 219 | 220 | @Override 221 | public boolean onTouch(View v, MotionEvent event) { 222 | if (mDismissOnTouch) { 223 | hide(); 224 | } 225 | if (mTargetTouchable && mTarget.getBounds().contains((int) event.getX(), (int) event.getY())) { 226 | if (mDismissOnTargetTouch) { 227 | hide(); 228 | } 229 | return false; 230 | } 231 | return true; 232 | } 233 | 234 | 235 | private void notifyOnDisplayed() { 236 | 237 | 238 | if (mListeners != null) { 239 | for (IShowcaseListener listener : mListeners) { 240 | listener.onShowcaseDisplayed(this); 241 | } 242 | } 243 | } 244 | 245 | private void notifyOnDismissed() { 246 | if (mListeners != null) { 247 | for (IShowcaseListener listener : mListeners) { 248 | listener.onShowcaseDismissed(this); 249 | } 250 | 251 | mListeners.clear(); 252 | mListeners = null; 253 | } 254 | 255 | /** 256 | * internal listener used by sequence for storing progress within the sequence 257 | */ 258 | if (mDetachedListener != null) { 259 | mDetachedListener.onShowcaseDetached(this, mWasDismissed, mWasSkipped); 260 | } 261 | } 262 | 263 | /** 264 | * Dismiss button clicked 265 | * 266 | * @param v 267 | */ 268 | @Override 269 | public void onClick(View v) { 270 | if (v.getId() == R.id.tv_dismiss) { 271 | hide(); 272 | } else if (v.getId() == R.id.tv_skip) { 273 | skip(); 274 | } 275 | } 276 | 277 | /** 278 | * Overrides the automatic handling of gravity and sets it to a specific one. Due to this, 279 | * margins are also reset to zero. 280 | * 281 | * @param gravity 282 | */ 283 | public void setGravity(int gravity) { 284 | mHasCustomGravity = Gravity.NO_GRAVITY != gravity; 285 | if (mHasCustomGravity) { 286 | mGravity = gravity; 287 | mContentTopMargin = mContentBottomMargin = 0; 288 | } 289 | applyLayoutParams(); 290 | } 291 | 292 | /** 293 | * Tells us about the "Target" which is the view we want to anchor to. 294 | * We figure out where it is on screen and (optionally) how big it is. 295 | * We also figure out whether to place our content and dismiss button above or below it. 296 | * 297 | * @param target 298 | */ 299 | public void setTarget(Target target) { 300 | mTarget = target; 301 | 302 | // update dismiss button state 303 | updateDismissButton(); 304 | 305 | if (mTarget != null) { 306 | 307 | /** 308 | * If we're on lollipop then make sure we don't draw over the nav bar 309 | */ 310 | if (!mRenderOverNav && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 311 | 312 | 313 | mBottomMargin = getSoftButtonsBarSizePort(); 314 | 315 | 316 | FrameLayout.LayoutParams contentLP = (LayoutParams) getLayoutParams(); 317 | 318 | if (contentLP != null && contentLP.bottomMargin != mBottomMargin) 319 | contentLP.bottomMargin = mBottomMargin; 320 | } 321 | 322 | // apply the target position 323 | Point targetPoint = mTarget.getPoint(); 324 | Rect targetBounds = mTarget.getBounds(); 325 | setPosition(targetPoint); 326 | 327 | // now figure out whether to put content above or below it 328 | int height = getMeasuredHeight(); 329 | int midPoint = height / 2; 330 | int yPos = targetPoint.y; 331 | 332 | int radius = Math.max(targetBounds.height(), targetBounds.width()) / 2; 333 | if (mShape != null) { 334 | mShape.updateTarget(mTarget); 335 | radius = mShape.getHeight() / 2; 336 | } 337 | 338 | // If there's no custom gravity in place, we'll do automatic gravity calculation. 339 | if (!mHasCustomGravity) { 340 | if (yPos > midPoint) { 341 | // target is in lower half of screen, we'll sit above it 342 | mContentTopMargin = 0; 343 | mContentBottomMargin = (height - yPos) + radius + mShapePadding; 344 | mGravity = Gravity.BOTTOM; 345 | } else { 346 | // target is in upper half of screen, we'll sit below it 347 | mContentTopMargin = yPos + radius + mShapePadding; 348 | mContentBottomMargin = 0; 349 | mGravity = Gravity.TOP; 350 | } 351 | } 352 | } 353 | 354 | applyLayoutParams(); 355 | } 356 | 357 | private void applyLayoutParams() { 358 | 359 | if (mContentBox != null && mContentBox.getLayoutParams() != null) { 360 | FrameLayout.LayoutParams contentLP = (LayoutParams) mContentBox.getLayoutParams(); 361 | 362 | boolean layoutParamsChanged = false; 363 | 364 | if (contentLP.bottomMargin != mContentBottomMargin) { 365 | contentLP.bottomMargin = mContentBottomMargin; 366 | layoutParamsChanged = true; 367 | } 368 | 369 | if (contentLP.topMargin != mContentTopMargin) { 370 | contentLP.topMargin = mContentTopMargin; 371 | layoutParamsChanged = true; 372 | } 373 | 374 | if (contentLP.gravity != mGravity) { 375 | contentLP.gravity = mGravity; 376 | layoutParamsChanged = true; 377 | } 378 | 379 | /** 380 | * Only apply the layout params if we've actually changed them, otherwise we'll get stuck in a layout loop 381 | */ 382 | if (layoutParamsChanged) 383 | mContentBox.setLayoutParams(contentLP); 384 | 385 | updateToolTip(); 386 | } 387 | } 388 | 389 | void updateToolTip() { 390 | /** 391 | * Adjust tooltip gravity if needed 392 | */ 393 | if (toolTip != null) { 394 | 395 | if (!toolTipShown) { 396 | toolTipShown = true; 397 | 398 | int shapeDiameter = mShape.getTotalRadius() * 2; 399 | int toolTipDistance = (shapeDiameter - mTarget.getBounds().height()) / 2; 400 | toolTipDistance += tooltipMargin; 401 | 402 | toolTip.show(toolTipDistance); 403 | } 404 | 405 | if (mGravity == Gravity.BOTTOM) { 406 | toolTip.position(ShowcaseTooltip.Position.TOP); 407 | } else { 408 | toolTip.position(ShowcaseTooltip.Position.BOTTOM); 409 | } 410 | } 411 | } 412 | 413 | /** 414 | * SETTERS 415 | */ 416 | 417 | void setPosition(Point point) { 418 | setPosition(point.x, point.y); 419 | } 420 | 421 | void setPosition(int x, int y) { 422 | mXPosition = x; 423 | mYPosition = y; 424 | } 425 | 426 | private void setTitleText(CharSequence contentText) { 427 | if (mTitleTextView != null && !contentText.equals("")) { 428 | mContentTextView.setAlpha(0.5F); 429 | mTitleTextView.setText(contentText); 430 | } 431 | } 432 | 433 | private void setContentText(CharSequence contentText) { 434 | if (mContentTextView != null) { 435 | mContentTextView.setText(contentText); 436 | } 437 | } 438 | 439 | 440 | private void setToolTip(ShowcaseTooltip toolTip) { 441 | this.toolTip = toolTip; 442 | } 443 | 444 | 445 | private void setIsSequence(Boolean isSequenceB) { 446 | isSequence = isSequenceB; 447 | } 448 | 449 | private void setDismissText(CharSequence dismissText) { 450 | if (mDismissButton != null) { 451 | mDismissButton.setText(dismissText); 452 | updateDismissButton(); 453 | } 454 | } 455 | 456 | private void setSkipText(CharSequence skipText) { 457 | if (mSkipButton != null) { 458 | mSkipButton.setText(skipText); 459 | updateSkipButton(); 460 | } 461 | } 462 | 463 | private void setDismissStyle(Typeface dismissStyle) { 464 | if (mDismissButton != null) { 465 | mDismissButton.setTypeface(dismissStyle); 466 | updateDismissButton(); 467 | } 468 | } 469 | 470 | private void setSkipStyle(Typeface skipStyle) { 471 | if (mSkipButton != null) { 472 | mSkipButton.setTypeface(skipStyle); 473 | updateSkipButton(); 474 | } 475 | } 476 | 477 | private void setTitleTextColor(int textColour) { 478 | if (mTitleTextView != null) { 479 | mTitleTextView.setTextColor(textColour); 480 | } 481 | } 482 | 483 | private void setContentTextColor(int textColour) { 484 | if (mContentTextView != null) { 485 | mContentTextView.setTextColor(textColour); 486 | } 487 | } 488 | 489 | private void setDismissTextColor(int textColour) { 490 | if (mDismissButton != null) { 491 | mDismissButton.setTextColor(textColour); 492 | } 493 | } 494 | 495 | private void setShapePadding(int padding) { 496 | mShapePadding = padding; 497 | } 498 | 499 | private void setTooltipMargin(int margin) { 500 | tooltipMargin = margin; 501 | } 502 | 503 | private void setDismissOnTouch(boolean dismissOnTouch) { 504 | mDismissOnTouch = dismissOnTouch; 505 | } 506 | 507 | private void setShouldRender(boolean shouldRender) { 508 | mShouldRender = shouldRender; 509 | } 510 | 511 | private void setMaskColour(int maskColour) { 512 | mMaskColour = maskColour; 513 | } 514 | 515 | private void setDelay(long delayInMillis) { 516 | mDelayInMillis = delayInMillis; 517 | } 518 | 519 | private void setFadeDuration(long fadeDurationInMillis) { 520 | mFadeDurationInMillis = fadeDurationInMillis; 521 | } 522 | 523 | private void setTargetTouchable(boolean targetTouchable) { 524 | mTargetTouchable = targetTouchable; 525 | } 526 | 527 | private void setDismissOnTargetTouch(boolean dismissOnTargetTouch) { 528 | mDismissOnTargetTouch = dismissOnTargetTouch; 529 | } 530 | 531 | private void setUseFadeAnimation(boolean useFadeAnimation) { 532 | mUseFadeAnimation = useFadeAnimation; 533 | } 534 | 535 | public void addShowcaseListener(IShowcaseListener showcaseListener) { 536 | if (mListeners != null) 537 | mListeners.add(showcaseListener); 538 | } 539 | 540 | public void removeShowcaseListener(MaterialShowcaseSequence showcaseListener) { 541 | 542 | if ((mListeners != null) && mListeners.contains(showcaseListener)) { 543 | mListeners.remove(showcaseListener); 544 | } 545 | } 546 | 547 | void setDetachedListener(IDetachedListener detachedListener) { 548 | mDetachedListener = detachedListener; 549 | } 550 | 551 | public void setShape(Shape mShape) { 552 | this.mShape = mShape; 553 | } 554 | 555 | public void setAnimationFactory(IAnimationFactory animationFactory) { 556 | this.mAnimationFactory = animationFactory; 557 | } 558 | 559 | /** 560 | * Set properties based on a config object 561 | * 562 | * @param config 563 | */ 564 | public void setConfig(ShowcaseConfig config) { 565 | 566 | if (config.getDelay() > -1) { 567 | setDelay(config.getDelay()); 568 | } 569 | 570 | if (config.getFadeDuration() > 0) { 571 | setFadeDuration(config.getFadeDuration()); 572 | } 573 | 574 | setContentTextColor(config.getContentTextColor()); 575 | 576 | setDismissTextColor(config.getDismissTextColor()); 577 | 578 | setMaskColour(config.getMaskColor()); 579 | 580 | if (config.getDismissTextStyle() != null) { 581 | setDismissStyle(config.getDismissTextStyle()); 582 | } 583 | 584 | if (config.getShape() != null) { 585 | setShape(config.getShape()); 586 | } 587 | 588 | if (config.getShapePadding() > -1) { 589 | setShapePadding(config.getShapePadding()); 590 | } 591 | 592 | if (config.getRenderOverNavigationBar() != null) { 593 | setRenderOverNavigationBar(config.getRenderOverNavigationBar()); 594 | } 595 | } 596 | 597 | void updateDismissButton() { 598 | // hide or show button 599 | if (mDismissButton != null) { 600 | if (TextUtils.isEmpty(mDismissButton.getText())) { 601 | mDismissButton.setVisibility(GONE); 602 | } else { 603 | mDismissButton.setVisibility(VISIBLE); 604 | } 605 | } 606 | } 607 | 608 | void updateSkipButton() { 609 | // hide or show button 610 | if (mSkipButton != null) { 611 | if (TextUtils.isEmpty(mSkipButton.getText())) { 612 | mSkipButton.setVisibility(GONE); 613 | } else { 614 | mSkipButton.setVisibility(VISIBLE); 615 | } 616 | } 617 | } 618 | 619 | public boolean hasFired() { 620 | return mPrefsManager.hasFired(); 621 | } 622 | 623 | /** 624 | * REDRAW LISTENER - this ensures we redraw after activity finishes laying out 625 | */ 626 | private class UpdateOnGlobalLayout implements ViewTreeObserver.OnGlobalLayoutListener { 627 | 628 | @Override 629 | public void onGlobalLayout() { 630 | setTarget(mTarget); 631 | } 632 | } 633 | 634 | 635 | /** 636 | * BUILDER CLASS 637 | * Gives us a builder utility class with a fluent API for eaily configuring showcase views 638 | */ 639 | public static class Builder { 640 | private static final int CIRCLE_SHAPE = 0; 641 | private static final int RECTANGLE_SHAPE = 1; 642 | private static final int NO_SHAPE = 2; 643 | private static final int OVAL_SHAPE = 3; 644 | 645 | private boolean fullWidth = false; 646 | private int shapeType = CIRCLE_SHAPE; 647 | 648 | final MaterialShowcaseView showcaseView; 649 | 650 | private final Activity activity; 651 | 652 | public Builder(Activity activity) { 653 | this.activity = activity; 654 | 655 | showcaseView = new MaterialShowcaseView(activity); 656 | } 657 | 658 | /** 659 | * Enforces a user-specified gravity instead of relying on the library to do that. 660 | */ 661 | public Builder setGravity(int gravity) { 662 | showcaseView.setGravity(gravity); 663 | return this; 664 | } 665 | 666 | /** 667 | * Set the title text shown on the ShowcaseView. 668 | */ 669 | public Builder setTarget(View target) { 670 | showcaseView.setTarget(new ViewTarget(target)); 671 | return this; 672 | } 673 | 674 | public Builder setSequence(Boolean isSequence) { 675 | showcaseView.setIsSequence(isSequence); 676 | return this; 677 | } 678 | 679 | /** 680 | * Set the dismiss button properties 681 | */ 682 | public Builder setDismissText(int resId) { 683 | return setDismissText(activity.getString(resId)); 684 | } 685 | 686 | public Builder setDismissText(CharSequence dismissText) { 687 | showcaseView.setDismissText(dismissText); 688 | return this; 689 | } 690 | 691 | public Builder setDismissStyle(Typeface dismissStyle) { 692 | showcaseView.setDismissStyle(dismissStyle); 693 | return this; 694 | } 695 | 696 | 697 | /** 698 | * Set the skip button properties 699 | */ 700 | public Builder setSkipText(int resId) { 701 | return setSkipText(activity.getString(resId)); 702 | } 703 | 704 | public Builder setSkipText(CharSequence skipText) { 705 | showcaseView.setSkipText(skipText); 706 | return this; 707 | } 708 | 709 | public Builder setSkipStyle(Typeface skipStyle) { 710 | showcaseView.setSkipStyle(skipStyle); 711 | return this; 712 | } 713 | 714 | /** 715 | * Set the content text shown on the ShowcaseView. 716 | */ 717 | public Builder setContentText(int resId) { 718 | return setContentText(activity.getString(resId)); 719 | } 720 | 721 | /** 722 | * Set the descriptive text shown on the ShowcaseView. 723 | */ 724 | public Builder setContentText(CharSequence text) { 725 | showcaseView.setContentText(text); 726 | return this; 727 | } 728 | 729 | /** 730 | * Set the title text shown on the ShowcaseView. 731 | */ 732 | public Builder setTitleText(int resId) { 733 | return setTitleText(activity.getString(resId)); 734 | } 735 | 736 | /** 737 | * Set the descriptive text shown on the ShowcaseView as the title. 738 | */ 739 | public Builder setTitleText(CharSequence text) { 740 | showcaseView.setTitleText(text); 741 | return this; 742 | } 743 | 744 | 745 | /** 746 | * Tooltip mode config options 747 | * 748 | * @param toolTip 749 | */ 750 | public Builder setToolTip(ShowcaseTooltip toolTip) { 751 | showcaseView.setToolTip(toolTip); 752 | return this; 753 | } 754 | 755 | 756 | /** 757 | * Set whether or not the target view can be touched while the showcase is visible. 758 | *

759 | * False by default. 760 | */ 761 | public Builder setTargetTouchable(boolean targetTouchable) { 762 | showcaseView.setTargetTouchable(targetTouchable); 763 | return this; 764 | } 765 | 766 | /** 767 | * Set whether or not the showcase should dismiss when the target is touched. 768 | *

769 | * True by default. 770 | */ 771 | public Builder setDismissOnTargetTouch(boolean dismissOnTargetTouch) { 772 | showcaseView.setDismissOnTargetTouch(dismissOnTargetTouch); 773 | return this; 774 | } 775 | 776 | public Builder setDismissOnTouch(boolean dismissOnTouch) { 777 | showcaseView.setDismissOnTouch(dismissOnTouch); 778 | return this; 779 | } 780 | 781 | public Builder setMaskColour(int maskColour) { 782 | showcaseView.setMaskColour(maskColour); 783 | return this; 784 | } 785 | 786 | public Builder setTitleTextColor(int textColour) { 787 | showcaseView.setTitleTextColor(textColour); 788 | return this; 789 | } 790 | 791 | public Builder setContentTextColor(int textColour) { 792 | showcaseView.setContentTextColor(textColour); 793 | return this; 794 | } 795 | 796 | public Builder setDismissTextColor(int textColour) { 797 | showcaseView.setDismissTextColor(textColour); 798 | return this; 799 | } 800 | 801 | public Builder setDelay(int delayInMillis) { 802 | showcaseView.setDelay(delayInMillis); 803 | return this; 804 | } 805 | 806 | public Builder setFadeDuration(int fadeDurationInMillis) { 807 | showcaseView.setFadeDuration(fadeDurationInMillis); 808 | return this; 809 | } 810 | 811 | public Builder setListener(IShowcaseListener listener) { 812 | showcaseView.addShowcaseListener(listener); 813 | return this; 814 | } 815 | 816 | public Builder singleUse(String showcaseID) { 817 | showcaseView.singleUse(showcaseID); 818 | return this; 819 | } 820 | 821 | public Builder setShape(Shape shape) { 822 | showcaseView.setShape(shape); 823 | return this; 824 | } 825 | 826 | public Builder withCircleShape() { 827 | shapeType = CIRCLE_SHAPE; 828 | return this; 829 | } 830 | 831 | public Builder withOvalShape() { 832 | shapeType = OVAL_SHAPE; 833 | return this; 834 | } 835 | 836 | public Builder withoutShape() { 837 | shapeType = NO_SHAPE; 838 | return this; 839 | } 840 | 841 | public Builder setShapePadding(int padding) { 842 | showcaseView.setShapePadding(padding); 843 | return this; 844 | } 845 | 846 | public Builder setTooltipMargin(int margin) { 847 | showcaseView.setTooltipMargin(margin); 848 | return this; 849 | } 850 | 851 | public Builder withRectangleShape() { 852 | return withRectangleShape(false); 853 | } 854 | 855 | public Builder withRectangleShape(boolean fullWidth) { 856 | this.shapeType = RECTANGLE_SHAPE; 857 | this.fullWidth = fullWidth; 858 | return this; 859 | } 860 | 861 | public Builder renderOverNavigationBar() { 862 | // Note: This only has an effect in Lollipop or above. 863 | showcaseView.setRenderOverNavigationBar(true); 864 | return this; 865 | } 866 | 867 | public Builder useFadeAnimation() { 868 | showcaseView.setUseFadeAnimation(true); 869 | return this; 870 | } 871 | 872 | public MaterialShowcaseView build() { 873 | if (showcaseView.mShape == null) { 874 | switch (shapeType) { 875 | case RECTANGLE_SHAPE: { 876 | showcaseView.setShape(new RectangleShape(showcaseView.mTarget.getBounds(), fullWidth)); 877 | break; 878 | } 879 | default: 880 | case CIRCLE_SHAPE: { 881 | showcaseView.setShape(new CircleShape(showcaseView.mTarget)); 882 | break; 883 | } 884 | case NO_SHAPE: { 885 | showcaseView.setShape(new NoShape()); 886 | break; 887 | } 888 | case OVAL_SHAPE: { 889 | showcaseView.setShape(new OvalShape(showcaseView.mTarget)); 890 | break; 891 | } 892 | } 893 | } 894 | 895 | if (showcaseView.mAnimationFactory == null) { 896 | // create our animation factory 897 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !showcaseView.mUseFadeAnimation) { 898 | showcaseView.setAnimationFactory(new CircularRevealAnimationFactory()); 899 | } else { 900 | showcaseView.setAnimationFactory(new FadeAnimationFactory()); 901 | } 902 | } 903 | 904 | showcaseView.mShape.setPadding(showcaseView.mShapePadding); 905 | 906 | return showcaseView; 907 | } 908 | 909 | public MaterialShowcaseView show() { 910 | build().show(activity); 911 | return showcaseView; 912 | } 913 | } 914 | 915 | private void singleUse(String showcaseID) { 916 | mSingleUse = true; 917 | mPrefsManager = new PrefsManager(getContext(), showcaseID); 918 | } 919 | 920 | public void removeFromWindow() { 921 | if (getParent() != null && getParent() instanceof ViewGroup) { 922 | ((ViewGroup) getParent()).removeView(this); 923 | } 924 | 925 | if (mBitmap != null) { 926 | mBitmap.recycle(); 927 | mBitmap = null; 928 | } 929 | 930 | mEraser = null; 931 | mAnimationFactory = null; 932 | mCanvas = null; 933 | mHandler = null; 934 | 935 | getViewTreeObserver().removeGlobalOnLayoutListener(mLayoutListener); 936 | mLayoutListener = null; 937 | 938 | if (mPrefsManager != null) 939 | mPrefsManager.close(); 940 | 941 | mPrefsManager = null; 942 | 943 | 944 | } 945 | 946 | 947 | /** 948 | * Reveal the showcaseview. Returns a boolean telling us whether we actually did show anything 949 | * 950 | * @param activity 951 | * @return 952 | */ 953 | public boolean show(final Activity activity) { 954 | 955 | /** 956 | * if we're in single use mode and have already shot our bolt then do nothing 957 | */ 958 | if (mSingleUse) { 959 | if (mPrefsManager.hasFired()) { 960 | return false; 961 | } else { 962 | mPrefsManager.setFired(); 963 | } 964 | } 965 | 966 | ((ViewGroup) activity.getWindow().getDecorView()).addView(this); 967 | 968 | setShouldRender(true); 969 | 970 | 971 | if (toolTip != null) { 972 | 973 | if (!(mTarget instanceof ViewTarget)) { 974 | throw new RuntimeException("The target must be of type: " + ViewTarget.class.getCanonicalName()); 975 | } 976 | 977 | ViewTarget viewTarget = (ViewTarget) mTarget; 978 | 979 | toolTip.configureTarget(this, viewTarget.getView()); 980 | 981 | } 982 | 983 | 984 | mHandler = new Handler(); 985 | mHandler.postDelayed(new Runnable() { 986 | @Override 987 | public void run() { 988 | boolean attached; 989 | // taken from https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-master-dev/core/src/main/java/androidx/core/view/ViewCompat.java#3310 990 | if (Build.VERSION.SDK_INT >= 19) { 991 | attached = isAttachedToWindow(); 992 | } else { 993 | attached = getWindowToken() != null; 994 | } 995 | if (mShouldAnimate && attached) { 996 | fadeIn(); 997 | } else { 998 | setVisibility(VISIBLE); 999 | notifyOnDisplayed(); 1000 | } 1001 | } 1002 | }, mDelayInMillis); 1003 | 1004 | updateDismissButton(); 1005 | 1006 | return true; 1007 | } 1008 | 1009 | 1010 | public void hide() { 1011 | 1012 | /** 1013 | * This flag is used to indicate to onDetachedFromWindow that the showcase view was dismissed purposefully (by the user or programmatically) 1014 | */ 1015 | mWasDismissed = true; 1016 | 1017 | if (mShouldAnimate) { 1018 | animateOut(); 1019 | } else { 1020 | removeFromWindow(); 1021 | } 1022 | } 1023 | 1024 | 1025 | public void skip() { 1026 | 1027 | /** 1028 | * This flag is used to indicate to onDetachedFromWindow that the showcase view was skipped purposefully (by the user or programmatically) 1029 | */ 1030 | mWasSkipped = true; 1031 | 1032 | if (mShouldAnimate) { 1033 | animateOut(); 1034 | } else { 1035 | removeFromWindow(); 1036 | } 1037 | } 1038 | 1039 | public void fadeIn() { 1040 | setVisibility(INVISIBLE); 1041 | mAnimationFactory.animateInView(this, mTarget.getPoint(), mFadeDurationInMillis, 1042 | new IAnimationFactory.AnimationStartListener() { 1043 | @Override 1044 | public void onAnimationStart() { 1045 | setVisibility(View.VISIBLE); 1046 | notifyOnDisplayed(); 1047 | } 1048 | } 1049 | ); 1050 | } 1051 | 1052 | public void animateOut() { 1053 | 1054 | mAnimationFactory.animateOutView(this, mTarget.getPoint(), mFadeDurationInMillis, new IAnimationFactory.AnimationEndListener() { 1055 | @Override 1056 | public void onAnimationEnd() { 1057 | setVisibility(INVISIBLE); 1058 | removeFromWindow(); 1059 | } 1060 | }); 1061 | } 1062 | 1063 | public void resetSingleUse() { 1064 | if (mSingleUse && mPrefsManager != null) mPrefsManager.resetShowcase(); 1065 | } 1066 | 1067 | /** 1068 | * Static helper method for resetting single use flag 1069 | * 1070 | * @param context 1071 | * @param showcaseID 1072 | */ 1073 | public static void resetSingleUse(Context context, String showcaseID) { 1074 | PrefsManager.resetShowcase(context, showcaseID); 1075 | } 1076 | 1077 | /** 1078 | * Static helper method for resetting all single use flags 1079 | * 1080 | * @param context 1081 | */ 1082 | public static void resetAll(Context context) { 1083 | PrefsManager.resetAll(context); 1084 | } 1085 | 1086 | 1087 | public int getSoftButtonsBarSizePort() { 1088 | 1089 | int resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); 1090 | if (resourceId > 0) { 1091 | return getResources().getDimensionPixelSize(resourceId); 1092 | } 1093 | 1094 | return 0; 1095 | 1096 | } 1097 | 1098 | private void setRenderOverNavigationBar(boolean mRenderOverNav) { 1099 | this.mRenderOverNav = mRenderOverNav; 1100 | } 1101 | } 1102 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/PrefsManager.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | 7 | public class PrefsManager { 8 | 9 | public static int SEQUENCE_NEVER_STARTED = 0; 10 | public static int SEQUENCE_FINISHED = -1; 11 | 12 | 13 | private static final String PREFS_NAME = "material_showcaseview_prefs"; 14 | private static final String STATUS = "status_"; 15 | private String showcaseID = null; 16 | private Context context; 17 | 18 | public PrefsManager(Context context, String showcaseID) { 19 | this.context = context; 20 | this.showcaseID = showcaseID; 21 | } 22 | 23 | 24 | /*** 25 | * METHODS FOR INDIVIDUAL SHOWCASE VIEWS 26 | */ 27 | boolean hasFired() { 28 | int status = getSequenceStatus(); 29 | return (status == SEQUENCE_FINISHED); 30 | } 31 | 32 | void setFired() { 33 | setSequenceStatus(SEQUENCE_FINISHED); 34 | } 35 | 36 | /*** 37 | * METHODS FOR SHOWCASE SEQUENCES 38 | */ 39 | int getSequenceStatus() { 40 | return context 41 | .getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) 42 | .getInt(STATUS + showcaseID, SEQUENCE_NEVER_STARTED); 43 | 44 | } 45 | 46 | void setSequenceStatus(int status) { 47 | SharedPreferences internal = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 48 | internal.edit().putInt(STATUS + showcaseID, status).apply(); 49 | } 50 | 51 | 52 | public void resetShowcase() { 53 | resetShowcase(context, showcaseID); 54 | } 55 | 56 | static void resetShowcase(Context context, String showcaseID) { 57 | SharedPreferences internal = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 58 | internal.edit().putInt(STATUS + showcaseID, SEQUENCE_NEVER_STARTED).apply(); 59 | } 60 | 61 | public static void resetAll(Context context) { 62 | SharedPreferences internal = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); 63 | internal.edit().clear().apply(); 64 | } 65 | 66 | public void close() { 67 | context = null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/ShowcaseConfig.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | import android.graphics.Color; 4 | import android.graphics.Typeface; 5 | 6 | import uk.co.deanwild.materialshowcaseview.shape.CircleShape; 7 | import uk.co.deanwild.materialshowcaseview.shape.Shape; 8 | 9 | 10 | public class ShowcaseConfig { 11 | 12 | public static final String DEFAULT_MASK_COLOUR = "#dd335075"; 13 | 14 | private long mDelay = -1; 15 | private int mMaskColour; 16 | private Typeface mDismissTextStyle; 17 | 18 | private int mContentTextColor; 19 | private int mDismissTextColor; 20 | private long mFadeDuration = -1; 21 | private Shape mShape = null; 22 | private int mShapePadding = -1; 23 | private Boolean renderOverNav; 24 | 25 | public ShowcaseConfig() { 26 | mMaskColour = Color.parseColor(ShowcaseConfig.DEFAULT_MASK_COLOUR); 27 | mContentTextColor = Color.parseColor("#ffffff"); 28 | mDismissTextColor = Color.parseColor("#ffffff"); 29 | } 30 | 31 | public long getDelay() { 32 | return mDelay; 33 | } 34 | 35 | public void setDelay(long delay) { 36 | this.mDelay = delay; 37 | } 38 | 39 | public int getMaskColor() { 40 | return mMaskColour; 41 | } 42 | 43 | public void setMaskColor(int maskColor) { 44 | mMaskColour = maskColor; 45 | } 46 | 47 | public int getContentTextColor() { 48 | return mContentTextColor; 49 | } 50 | 51 | public void setContentTextColor(int mContentTextColor) { 52 | this.mContentTextColor = mContentTextColor; 53 | } 54 | 55 | public int getDismissTextColor() { 56 | return mDismissTextColor; 57 | } 58 | 59 | public void setDismissTextColor(int dismissTextColor) { 60 | this.mDismissTextColor = dismissTextColor; 61 | } 62 | 63 | public Typeface getDismissTextStyle() { 64 | return mDismissTextStyle; 65 | } 66 | 67 | public void setDismissTextStyle(Typeface dismissTextStyle) { 68 | this.mDismissTextStyle = dismissTextStyle; 69 | } 70 | 71 | public long getFadeDuration() { 72 | return mFadeDuration; 73 | } 74 | 75 | public void setFadeDuration(long fadeDuration) { 76 | this.mFadeDuration = fadeDuration; 77 | } 78 | 79 | public Shape getShape() { 80 | return mShape; 81 | } 82 | 83 | public void setShape(Shape shape) { 84 | this.mShape = shape; 85 | } 86 | 87 | public void setShapePadding(int padding) { 88 | this.mShapePadding = padding; 89 | } 90 | 91 | public int getShapePadding() { 92 | return mShapePadding; 93 | } 94 | 95 | public Boolean getRenderOverNavigationBar() { 96 | return renderOverNav; 97 | } 98 | 99 | public void setRenderOverNavigationBar(boolean renderOverNav) { 100 | this.renderOverNav = renderOverNav; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/ShowcaseTooltip.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview; 2 | 3 | 4 | import android.animation.Animator; 5 | import android.animation.AnimatorListenerAdapter; 6 | import android.app.Activity; 7 | import android.app.DialogFragment; 8 | import android.app.Fragment; 9 | import android.content.Context; 10 | import android.content.ContextWrapper; 11 | import android.graphics.Canvas; 12 | import android.graphics.Color; 13 | import android.graphics.Paint; 14 | import android.graphics.Path; 15 | import android.graphics.Point; 16 | import android.graphics.Rect; 17 | import android.graphics.RectF; 18 | import android.graphics.Typeface; 19 | 20 | 21 | import android.text.Html; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.view.ViewTreeObserver; 25 | import android.view.Window; 26 | import android.widget.FrameLayout; 27 | import android.widget.TextView; 28 | 29 | import java.util.Arrays; 30 | 31 | /** 32 | * Base on original code by florentchampigny 33 | * https://github.com/florent37/ViewTooltip 34 | */ 35 | 36 | public class ShowcaseTooltip { 37 | 38 | private View rootView; 39 | private View view; 40 | private TooltipView tooltip_view; 41 | 42 | 43 | private ShowcaseTooltip(Context context){ 44 | MyContext myContext = new MyContext(getActivityContext(context)); 45 | this.tooltip_view = new TooltipView(myContext.getContext()); 46 | } 47 | 48 | public static ShowcaseTooltip build(Context context) { 49 | return new ShowcaseTooltip(context); 50 | } 51 | 52 | public void configureTarget(ViewGroup rootView, View view) { 53 | this.rootView = rootView; 54 | this.view = view; 55 | } 56 | 57 | private static Activity getActivityContext(Context context) { 58 | while (context instanceof ContextWrapper) { 59 | if (context instanceof Activity) { 60 | return (Activity) context; 61 | } 62 | context = ((ContextWrapper) context).getBaseContext(); 63 | } 64 | return null; 65 | } 66 | 67 | public ShowcaseTooltip position(Position position) { 68 | this.tooltip_view.setPosition(position); 69 | return this; 70 | } 71 | 72 | public ShowcaseTooltip customView(View customView) { 73 | this.tooltip_view.setCustomView(customView); 74 | return this; 75 | } 76 | 77 | public ShowcaseTooltip customView(int viewId) { 78 | this.tooltip_view.setCustomView(((Activity) view.getContext()).findViewById(viewId)); 79 | return this; 80 | } 81 | 82 | public ShowcaseTooltip arrowWidth(int arrowWidth) { 83 | this.tooltip_view.setArrowWidth(arrowWidth); 84 | return this; 85 | } 86 | 87 | public ShowcaseTooltip arrowHeight(int arrowHeight) { 88 | this.tooltip_view.setArrowHeight(arrowHeight); 89 | return this; 90 | } 91 | 92 | public ShowcaseTooltip arrowSourceMargin(int arrowSourceMargin) { 93 | this.tooltip_view.setArrowSourceMargin(arrowSourceMargin); 94 | return this; 95 | } 96 | 97 | public ShowcaseTooltip arrowTargetMargin(int arrowTargetMargin) { 98 | this.tooltip_view.setArrowTargetMargin(arrowTargetMargin); 99 | return this; 100 | } 101 | 102 | public ShowcaseTooltip align(ALIGN align) { 103 | this.tooltip_view.setAlign(align); 104 | return this; 105 | } 106 | 107 | public TooltipView show(final int margin) { 108 | final Context activityContext = tooltip_view.getContext(); 109 | if (activityContext != null && activityContext instanceof Activity) { 110 | final ViewGroup decorView = rootView != null ? 111 | (ViewGroup) rootView : 112 | (ViewGroup) ((Activity) activityContext).getWindow().getDecorView(); 113 | 114 | view.postDelayed(new Runnable() { 115 | @Override 116 | public void run() { 117 | final Rect rect = new Rect(); 118 | view.getGlobalVisibleRect(rect); 119 | 120 | final Rect rootGlobalRect = new Rect(); 121 | final Point rootGlobalOffset = new Point(); 122 | decorView.getGlobalVisibleRect(rootGlobalRect, rootGlobalOffset); 123 | 124 | int[] location = new int[2]; 125 | view.getLocationOnScreen(location); 126 | rect.left = location[0]; 127 | if (rootGlobalOffset != null) { 128 | rect.top -= rootGlobalOffset.y; 129 | rect.bottom -= rootGlobalOffset.y; 130 | rect.left -= rootGlobalOffset.x; 131 | rect.right -= rootGlobalOffset.x; 132 | } 133 | 134 | // fixes bottom mode 135 | rect.top -= margin; 136 | 137 | // fixes top mode 138 | rect.bottom += margin; 139 | 140 | decorView.addView(tooltip_view, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 141 | 142 | tooltip_view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 143 | @Override 144 | public boolean onPreDraw() { 145 | 146 | tooltip_view.setup(rect, decorView.getWidth()); 147 | 148 | tooltip_view.getViewTreeObserver().removeOnPreDrawListener(this); 149 | 150 | return false; 151 | } 152 | }); 153 | } 154 | }, 100); 155 | } 156 | return tooltip_view; 157 | } 158 | 159 | public ShowcaseTooltip color(int color) { 160 | this.tooltip_view.setColor(color); 161 | return this; 162 | } 163 | 164 | public ShowcaseTooltip color(Paint paint) { 165 | this.tooltip_view.setPaint(paint); 166 | return this; 167 | } 168 | 169 | public ShowcaseTooltip onDisplay(ListenerDisplay listener) { 170 | this.tooltip_view.setListenerDisplay(listener); 171 | return this; 172 | } 173 | 174 | public ShowcaseTooltip padding(int left, int top, int right, int bottom) { 175 | this.tooltip_view.paddingTop = top; 176 | this.tooltip_view.paddingBottom = bottom; 177 | this.tooltip_view.paddingLeft = left; 178 | this.tooltip_view.paddingRight = right; 179 | return this; 180 | } 181 | 182 | public ShowcaseTooltip animation(TooltipAnimation tooltipAnimation) { 183 | this.tooltip_view.setTooltipAnimation(tooltipAnimation); 184 | return this; 185 | } 186 | 187 | public ShowcaseTooltip text(String text) { 188 | this.tooltip_view.setText(text); 189 | return this; 190 | } 191 | 192 | public ShowcaseTooltip text(int text) { 193 | this.tooltip_view.setText(text); 194 | return this; 195 | } 196 | 197 | public ShowcaseTooltip corner(int corner) { 198 | this.tooltip_view.setCorner(corner); 199 | return this; 200 | } 201 | 202 | public ShowcaseTooltip textColor(int textColor) { 203 | this.tooltip_view.setTextColor(textColor); 204 | return this; 205 | } 206 | 207 | public ShowcaseTooltip textTypeFace(Typeface typeface) { 208 | this.tooltip_view.setTextTypeFace(typeface); 209 | return this; 210 | } 211 | 212 | public ShowcaseTooltip textSize(int unit, float textSize) { 213 | this.tooltip_view.setTextSize(unit, textSize); 214 | return this; 215 | } 216 | 217 | public ShowcaseTooltip setTextGravity(int textGravity) { 218 | this.tooltip_view.setTextGravity(textGravity); 219 | return this; 220 | } 221 | 222 | public ShowcaseTooltip distanceWithView(int distance) { 223 | this.tooltip_view.setDistanceWithView(distance); 224 | return this; 225 | } 226 | 227 | public ShowcaseTooltip border(int color, float width) { 228 | Paint borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 229 | borderPaint.setColor(color); 230 | borderPaint.setStyle(Paint.Style.STROKE); 231 | borderPaint.setStrokeWidth(width); 232 | this.tooltip_view.setBorderPaint(borderPaint); 233 | return this; 234 | } 235 | 236 | public enum Position { 237 | LEFT, 238 | RIGHT, 239 | TOP, 240 | BOTTOM, 241 | } 242 | 243 | public enum ALIGN { 244 | START, 245 | CENTER, 246 | END 247 | } 248 | 249 | public interface TooltipAnimation { 250 | void animateEnter(View view, Animator.AnimatorListener animatorListener); 251 | 252 | void animateExit(View view, Animator.AnimatorListener animatorListener); 253 | } 254 | 255 | public interface ListenerDisplay { 256 | void onDisplay(View view); 257 | } 258 | 259 | public static class FadeTooltipAnimation implements TooltipAnimation { 260 | 261 | private long fadeDuration = 400; 262 | 263 | public FadeTooltipAnimation() { 264 | } 265 | 266 | public FadeTooltipAnimation(long fadeDuration) { 267 | this.fadeDuration = fadeDuration; 268 | } 269 | 270 | @Override 271 | public void animateEnter(View view, Animator.AnimatorListener animatorListener) { 272 | view.setAlpha(0); 273 | view.animate().alpha(1).setDuration(fadeDuration).setListener(animatorListener); 274 | } 275 | 276 | @Override 277 | public void animateExit(View view, Animator.AnimatorListener animatorListener) { 278 | view.animate().alpha(0).setDuration(fadeDuration).setListener(animatorListener); 279 | } 280 | } 281 | 282 | public static class TooltipView extends FrameLayout { 283 | 284 | private static final int MARGIN_SCREEN_BORDER_TOOLTIP = 30; 285 | private int arrowHeight = 15; 286 | private int arrowWidth = 15; 287 | private int arrowSourceMargin = 0; 288 | private int arrowTargetMargin = 0; 289 | protected View childView; 290 | private int color = Color.parseColor("#FFFFFF"); 291 | private Path bubblePath; 292 | private Paint bubblePaint; 293 | private Paint borderPaint; 294 | private Position position = Position.BOTTOM; 295 | private ALIGN align = ALIGN.CENTER; 296 | 297 | private ListenerDisplay listenerDisplay; 298 | 299 | private TooltipAnimation tooltipAnimation = new FadeTooltipAnimation(); 300 | 301 | private int corner = 30; 302 | 303 | private int paddingTop = 20; 304 | private int paddingBottom = 30; 305 | private int paddingRight = 60; 306 | private int paddingLeft = 60; 307 | 308 | private Rect viewRect; 309 | private int distanceWithView = 0; 310 | 311 | public TooltipView(Context context) { 312 | super(context); 313 | 314 | setWillNotDraw(false); 315 | 316 | this.childView = new TextView(context); 317 | ((TextView) childView).setTextColor(Color.BLACK); 318 | addView(childView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 319 | childView.setPadding(0, 0, 0, 0); 320 | 321 | bubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 322 | bubblePaint.setColor(color); 323 | bubblePaint.setStyle(Paint.Style.FILL); 324 | 325 | borderPaint = null; 326 | 327 | setLayerType(LAYER_TYPE_SOFTWARE, bubblePaint); 328 | 329 | } 330 | 331 | public void setCustomView(View customView) { 332 | this.removeView(childView); 333 | this.childView = customView; 334 | addView(childView, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 335 | } 336 | 337 | public void setColor(int color) { 338 | this.color = color; 339 | bubblePaint.setColor(color); 340 | postInvalidate(); 341 | } 342 | 343 | public void setPaint(Paint paint) { 344 | bubblePaint = paint; 345 | setLayerType(LAYER_TYPE_SOFTWARE, paint); 346 | postInvalidate(); 347 | } 348 | 349 | public void setPosition(Position position) { 350 | this.position = position; 351 | switch (position) { 352 | case TOP: 353 | setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom + arrowHeight); 354 | break; 355 | case BOTTOM: 356 | setPadding(paddingLeft, paddingTop + arrowHeight, paddingRight, paddingBottom); 357 | break; 358 | case LEFT: 359 | setPadding(paddingLeft, paddingTop, paddingRight + arrowHeight, paddingBottom); 360 | break; 361 | case RIGHT: 362 | setPadding(paddingLeft + arrowHeight, paddingTop, paddingRight, paddingBottom); 363 | break; 364 | } 365 | postInvalidate(); 366 | } 367 | 368 | public void setAlign(ALIGN align) { 369 | this.align = align; 370 | postInvalidate(); 371 | } 372 | 373 | public void setText(String text) { 374 | if (childView instanceof TextView) { 375 | ((TextView) this.childView).setText(Html.fromHtml(text)); 376 | } 377 | postInvalidate(); 378 | } 379 | 380 | public void setText(int text) { 381 | if (childView instanceof TextView) { 382 | ((TextView) this.childView).setText(text); 383 | } 384 | postInvalidate(); 385 | } 386 | 387 | public void setTextColor(int textColor) { 388 | if (childView instanceof TextView) { 389 | ((TextView) this.childView).setTextColor(textColor); 390 | } 391 | postInvalidate(); 392 | } 393 | 394 | public int getArrowHeight() { 395 | return arrowHeight; 396 | } 397 | 398 | public void setArrowHeight(int arrowHeight) { 399 | this.arrowHeight = arrowHeight; 400 | postInvalidate(); 401 | } 402 | 403 | public int getArrowWidth() { 404 | return arrowWidth; 405 | } 406 | 407 | public void setArrowWidth(int arrowWidth) { 408 | this.arrowWidth = arrowWidth; 409 | postInvalidate(); 410 | } 411 | 412 | public int getArrowSourceMargin() { 413 | return arrowSourceMargin; 414 | } 415 | 416 | public void setArrowSourceMargin(int arrowSourceMargin) { 417 | this.arrowSourceMargin = arrowSourceMargin; 418 | postInvalidate(); 419 | } 420 | 421 | public int getArrowTargetMargin() { 422 | return arrowTargetMargin; 423 | } 424 | 425 | public void setArrowTargetMargin(int arrowTargetMargin) { 426 | this.arrowTargetMargin = arrowTargetMargin; 427 | postInvalidate(); 428 | } 429 | 430 | public void setTextTypeFace(Typeface textTypeFace) { 431 | if (childView instanceof TextView) { 432 | ((TextView) this.childView).setTypeface(textTypeFace); 433 | } 434 | postInvalidate(); 435 | } 436 | 437 | public void setTextSize(int unit, float size) { 438 | if (childView instanceof TextView) { 439 | ((TextView) this.childView).setTextSize(unit, size); 440 | } 441 | postInvalidate(); 442 | } 443 | 444 | public void setTextGravity(int textGravity) { 445 | if (childView instanceof TextView) { 446 | ((TextView) this.childView).setGravity(textGravity); 447 | } 448 | postInvalidate(); 449 | } 450 | 451 | public void setCorner(int corner) { 452 | this.corner = corner; 453 | } 454 | 455 | @Override 456 | protected void onSizeChanged(int width, int height, int oldw, int oldh) { 457 | super.onSizeChanged(width, height, oldw, oldh); 458 | 459 | bubblePath = drawBubble(new RectF(0, 0, width, height), corner, corner, corner, corner); 460 | } 461 | 462 | @Override 463 | protected void onDraw(Canvas canvas) { 464 | super.onDraw(canvas); 465 | 466 | if (bubblePath != null) { 467 | canvas.drawPath(bubblePath, bubblePaint); 468 | if (borderPaint != null) { 469 | canvas.drawPath(bubblePath, borderPaint); 470 | } 471 | } 472 | } 473 | 474 | public void setListenerDisplay(ListenerDisplay listener) { 475 | this.listenerDisplay = listener; 476 | } 477 | 478 | public void setTooltipAnimation(TooltipAnimation tooltipAnimation) { 479 | this.tooltipAnimation = tooltipAnimation; 480 | } 481 | 482 | protected void startEnterAnimation() { 483 | tooltipAnimation.animateEnter(this, new AnimatorListenerAdapter() { 484 | @Override 485 | public void onAnimationEnd(Animator animation) { 486 | super.onAnimationEnd(animation); 487 | if (listenerDisplay != null) { 488 | listenerDisplay.onDisplay(TooltipView.this); 489 | } 490 | } 491 | }); 492 | } 493 | 494 | public void setupPosition(Rect rect) { 495 | 496 | int x, y; 497 | 498 | if (position == Position.LEFT || position == Position.RIGHT) { 499 | if (position == Position.LEFT) { 500 | x = rect.left - getWidth() - distanceWithView; 501 | } else { 502 | x = rect.right + distanceWithView; 503 | } 504 | y = rect.top + getAlignOffset(getHeight(), rect.height()); 505 | } else { 506 | if (position == Position.BOTTOM) { 507 | y = rect.bottom + distanceWithView; 508 | } else { // top 509 | y = rect.top - getHeight() - distanceWithView; 510 | } 511 | x = rect.left + getAlignOffset(getWidth(), rect.width()); 512 | } 513 | 514 | setTranslationX(x); 515 | setTranslationY(y); 516 | } 517 | 518 | private int getAlignOffset(int myLength, int hisLength) { 519 | switch (align) { 520 | case END: 521 | return hisLength - myLength; 522 | case CENTER: 523 | return (hisLength - myLength) / 2; 524 | } 525 | return 0; 526 | } 527 | 528 | private Path drawBubble(RectF myRect, float topLeftDiameter, float topRightDiameter, float bottomRightDiameter, float bottomLeftDiameter) { 529 | final Path path = new Path(); 530 | 531 | if (viewRect == null) 532 | return path; 533 | 534 | topLeftDiameter = topLeftDiameter < 0 ? 0 : topLeftDiameter; 535 | topRightDiameter = topRightDiameter < 0 ? 0 : topRightDiameter; 536 | bottomLeftDiameter = bottomLeftDiameter < 0 ? 0 : bottomLeftDiameter; 537 | bottomRightDiameter = bottomRightDiameter < 0 ? 0 : bottomRightDiameter; 538 | 539 | float spacingLeft = 30; 540 | final float spacingTop = this.position == Position.BOTTOM ? arrowHeight : 0; 541 | float spacingRight = 30; 542 | final float spacingBottom = this.position == Position.TOP ? arrowHeight : 0; 543 | 544 | final float left = spacingLeft + myRect.left; 545 | final float top = spacingTop + myRect.top; 546 | final float right = myRect.right - spacingRight; 547 | final float bottom = myRect.bottom - spacingBottom; 548 | final float centerX = viewRect.centerX() - getX(); 549 | 550 | final float arrowSourceX = (Arrays.asList(Position.TOP, Position.BOTTOM).contains(this.position)) 551 | ? centerX + arrowSourceMargin 552 | : centerX; 553 | final float arrowTargetX = (Arrays.asList(Position.TOP, Position.BOTTOM).contains(this.position)) 554 | ? centerX + arrowTargetMargin 555 | : centerX; 556 | final float arrowSourceY = (Arrays.asList(Position.RIGHT, Position.LEFT).contains(this.position)) 557 | ? bottom / 2f - arrowSourceMargin 558 | : bottom / 2f; 559 | final float arrowTargetY = (Arrays.asList(Position.RIGHT, Position.LEFT).contains(this.position)) 560 | ? bottom / 2f - arrowTargetMargin 561 | : bottom / 2f; 562 | 563 | path.moveTo(left + topLeftDiameter / 2f, top); 564 | //LEFT, TOP 565 | 566 | if (position == Position.BOTTOM) { 567 | path.lineTo(arrowSourceX - arrowWidth, top); 568 | path.lineTo(arrowTargetX, myRect.top); 569 | path.lineTo(arrowSourceX + arrowWidth, top); 570 | } 571 | path.lineTo(right - topRightDiameter / 2f, top); 572 | 573 | path.quadTo(right, top, right, top + topRightDiameter / 2); 574 | //RIGHT, TOP 575 | 576 | if (position == Position.LEFT) { 577 | path.lineTo(right, arrowSourceY - arrowWidth); 578 | path.lineTo(myRect.right, arrowTargetY); 579 | path.lineTo(right, arrowSourceY + arrowWidth); 580 | } 581 | path.lineTo(right, bottom - bottomRightDiameter / 2); 582 | 583 | path.quadTo(right, bottom, right - bottomRightDiameter / 2, bottom); 584 | //RIGHT, BOTTOM 585 | 586 | if (position == Position.TOP) { 587 | path.lineTo(arrowSourceX + arrowWidth, bottom); 588 | path.lineTo(arrowTargetX, myRect.bottom); 589 | path.lineTo(arrowSourceX - arrowWidth, bottom); 590 | } 591 | path.lineTo(left + bottomLeftDiameter / 2, bottom); 592 | 593 | path.quadTo(left, bottom, left, bottom - bottomLeftDiameter / 2); 594 | //LEFT, BOTTOM 595 | 596 | if (position == Position.RIGHT) { 597 | path.lineTo(left, arrowSourceY + arrowWidth); 598 | path.lineTo(myRect.left, arrowTargetY); 599 | path.lineTo(left, arrowSourceY - arrowWidth); 600 | } 601 | path.lineTo(left, top + topLeftDiameter / 2); 602 | 603 | path.quadTo(left, top, left + topLeftDiameter / 2, top); 604 | 605 | path.close(); 606 | 607 | return path; 608 | } 609 | 610 | public boolean adjustSize(Rect rect, int screenWidth) { 611 | 612 | final Rect r = new Rect(); 613 | getGlobalVisibleRect(r); 614 | 615 | boolean changed = false; 616 | final ViewGroup.LayoutParams layoutParams = getLayoutParams(); 617 | if (position == Position.LEFT && getWidth() > rect.left) { 618 | layoutParams.width = rect.left - MARGIN_SCREEN_BORDER_TOOLTIP - distanceWithView; 619 | changed = true; 620 | } else if (position == Position.RIGHT && rect.right + getWidth() > screenWidth) { 621 | layoutParams.width = screenWidth - rect.right - MARGIN_SCREEN_BORDER_TOOLTIP - distanceWithView; 622 | changed = true; 623 | } else if (position == Position.TOP || position == Position.BOTTOM) { 624 | int adjustedLeft = rect.left; 625 | int adjustedRight = rect.right; 626 | 627 | if ((rect.centerX() + getWidth() / 2f) > screenWidth) { 628 | float diff = (rect.centerX() + getWidth() / 2f) - screenWidth; 629 | 630 | adjustedLeft -= diff; 631 | adjustedRight -= diff; 632 | 633 | setAlign(ALIGN.CENTER); 634 | changed = true; 635 | } else if ((rect.centerX() - getWidth() / 2f) < 0) { 636 | float diff = -(rect.centerX() - getWidth() / 2f); 637 | 638 | adjustedLeft += diff; 639 | adjustedRight += diff; 640 | 641 | setAlign(ALIGN.CENTER); 642 | changed = true; 643 | } 644 | 645 | if (adjustedLeft < 0) { 646 | adjustedLeft = 0; 647 | } 648 | 649 | if (adjustedRight > screenWidth) { 650 | adjustedRight = screenWidth; 651 | } 652 | 653 | rect.left = adjustedLeft; 654 | rect.right = adjustedRight; 655 | } 656 | 657 | setLayoutParams(layoutParams); 658 | postInvalidate(); 659 | return changed; 660 | } 661 | 662 | private void onSetup(Rect myRect) { 663 | setupPosition(myRect); 664 | bubblePath = drawBubble(new RectF(0, 0, getWidth(), getHeight()), corner, corner, corner, corner); 665 | startEnterAnimation(); 666 | } 667 | 668 | public void setup(final Rect viewRect, int screenWidth) { 669 | this.viewRect = new Rect(viewRect); 670 | final Rect myRect = new Rect(viewRect); 671 | 672 | final boolean changed = adjustSize(myRect, screenWidth); 673 | if (!changed) { 674 | onSetup(myRect); 675 | } else { 676 | getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { 677 | @Override 678 | public boolean onPreDraw() { 679 | onSetup(myRect); 680 | getViewTreeObserver().removeOnPreDrawListener(this); 681 | return false; 682 | } 683 | }); 684 | } 685 | } 686 | 687 | public void removeNow() { 688 | if (getParent() != null) { 689 | final ViewGroup parent = ((ViewGroup) getParent()); 690 | parent.removeView(TooltipView.this); 691 | } 692 | } 693 | 694 | public void closeNow() { 695 | removeNow(); 696 | } 697 | 698 | public void setDistanceWithView(int distanceWithView) { 699 | this.distanceWithView = distanceWithView; 700 | } 701 | 702 | public void setBorderPaint(Paint borderPaint) { 703 | this.borderPaint = borderPaint; 704 | postInvalidate(); 705 | } 706 | } 707 | 708 | public static class MyContext { 709 | private Fragment fragment; 710 | private Context context; 711 | private Activity activity; 712 | 713 | public MyContext(Activity activity) { 714 | this.activity = activity; 715 | } 716 | 717 | public MyContext(Fragment fragment) { 718 | this.fragment = fragment; 719 | } 720 | 721 | public MyContext(Context context) { 722 | this.context = context; 723 | } 724 | 725 | public Context getContext() { 726 | if (activity != null) { 727 | return activity; 728 | } else { 729 | return ((Context) fragment.getActivity()); 730 | } 731 | } 732 | 733 | public Activity getActivity() { 734 | if (activity != null) { 735 | return activity; 736 | } else { 737 | return fragment.getActivity(); 738 | } 739 | } 740 | 741 | 742 | public Window getWindow() { 743 | if (activity != null) { 744 | return activity.getWindow(); 745 | } else { 746 | if (fragment instanceof DialogFragment) { 747 | return ((DialogFragment) fragment).getDialog().getWindow(); 748 | } 749 | return fragment.getActivity().getWindow(); 750 | } 751 | } 752 | } 753 | } 754 | 755 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/shape/CircleShape.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.shape; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Rect; 6 | 7 | import uk.co.deanwild.materialshowcaseview.target.Target; 8 | 9 | /** 10 | * Circular shape for target. 11 | */ 12 | public class CircleShape implements Shape { 13 | 14 | private int radius = 200; 15 | private boolean adjustToTarget = true; 16 | private int padding; 17 | 18 | public CircleShape() { 19 | } 20 | 21 | public CircleShape(int radius) { 22 | this.radius = radius; 23 | } 24 | 25 | public CircleShape(Rect bounds) { 26 | this(getPreferredRadius(bounds)); 27 | } 28 | 29 | public CircleShape(Target target) { 30 | this(target.getBounds()); 31 | } 32 | 33 | public void setAdjustToTarget(boolean adjustToTarget) { 34 | this.adjustToTarget = adjustToTarget; 35 | } 36 | 37 | public boolean isAdjustToTarget() { 38 | return adjustToTarget; 39 | } 40 | 41 | public int getRadius() { 42 | return radius; 43 | } 44 | 45 | public void setRadius(int radius) { 46 | this.radius = radius; 47 | } 48 | 49 | @Override 50 | public void draw(Canvas canvas, Paint paint, int x, int y) { 51 | if (radius > 0) { 52 | canvas.drawCircle(x, y, radius + padding, paint); 53 | } 54 | } 55 | 56 | @Override 57 | public void updateTarget(Target target) { 58 | if (adjustToTarget) 59 | radius = getPreferredRadius(target.getBounds()); 60 | } 61 | 62 | @Override 63 | public int getTotalRadius() { 64 | return radius + padding; 65 | } 66 | 67 | @Override 68 | public void setPadding(int padding) { 69 | this.padding = padding; 70 | } 71 | 72 | @Override 73 | public int getWidth() { 74 | return radius * 2; 75 | } 76 | 77 | @Override 78 | public int getHeight() { 79 | return radius * 2; 80 | } 81 | 82 | public static int getPreferredRadius(Rect bounds) { 83 | return Math.max(bounds.width(), bounds.height()) / 2; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/shape/NoShape.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.shape; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | 6 | import uk.co.deanwild.materialshowcaseview.target.Target; 7 | 8 | /** 9 | * A Shape implementation that draws nothing. 10 | */ 11 | public class NoShape implements Shape { 12 | 13 | @Override 14 | public void updateTarget(Target target) { 15 | // do nothing 16 | } 17 | 18 | @Override 19 | public int getTotalRadius() { 20 | return 0; 21 | } 22 | 23 | @Override 24 | public void setPadding(int padding) { 25 | // do nothing 26 | } 27 | 28 | @Override 29 | public void draw(Canvas canvas, Paint paint, int x, int y) { 30 | // do nothing 31 | } 32 | 33 | @Override 34 | public int getWidth() { 35 | return 0; 36 | } 37 | 38 | @Override 39 | public int getHeight() { 40 | return 0; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/shape/OvalShape.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.shape; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Rect; 6 | import android.graphics.RectF; 7 | 8 | import uk.co.deanwild.materialshowcaseview.target.Target; 9 | 10 | public class OvalShape implements Shape { 11 | private int radius; 12 | private boolean adjustToTarget; 13 | private int padding; 14 | 15 | public OvalShape() { 16 | this.radius = 200; 17 | this.adjustToTarget = true; 18 | } 19 | 20 | public OvalShape(int radius) { 21 | this.radius = 200; 22 | this.adjustToTarget = true; 23 | this.radius = radius; 24 | } 25 | 26 | public OvalShape(Rect bounds) { 27 | this(getPreferredRadius(bounds)); 28 | } 29 | 30 | public OvalShape(Target target) { 31 | this(target.getBounds()); 32 | } 33 | 34 | public static int getPreferredRadius(Rect bounds) { 35 | return Math.max(bounds.width(), bounds.height()) / 2; 36 | } 37 | 38 | public boolean isAdjustToTarget() { 39 | return this.adjustToTarget; 40 | } 41 | 42 | public void setAdjustToTarget(boolean adjustToTarget) { 43 | this.adjustToTarget = adjustToTarget; 44 | } 45 | 46 | public int getRadius() { 47 | return this.radius; 48 | } 49 | 50 | public void setRadius(int radius) { 51 | this.radius = radius; 52 | } 53 | 54 | public void draw(Canvas canvas, Paint paint, int x, int y) { 55 | if (this.radius > 0) { 56 | float rad = (float) (this.radius + padding); 57 | RectF rectF = new RectF(x - rad, y - rad / 2, x + rad, y + rad / 2); 58 | canvas.drawOval(rectF, paint); 59 | } 60 | 61 | } 62 | 63 | public void updateTarget(Target target) { 64 | if (this.adjustToTarget) { 65 | this.radius = getPreferredRadius(target.getBounds()); 66 | } 67 | 68 | } 69 | 70 | @Override 71 | public int getTotalRadius() { 72 | return radius + padding; 73 | } 74 | 75 | @Override 76 | public void setPadding(int padding) { 77 | this.padding = padding; 78 | } 79 | 80 | public int getWidth() { 81 | return this.radius * 2; 82 | } 83 | 84 | public int getHeight() { 85 | return this.radius; 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/shape/RectangleShape.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.shape; 2 | 3 | 4 | import android.graphics.Canvas; 5 | import android.graphics.Paint; 6 | import android.graphics.Rect; 7 | 8 | import uk.co.deanwild.materialshowcaseview.target.Target; 9 | 10 | public class RectangleShape implements Shape { 11 | 12 | private boolean fullWidth = false; 13 | 14 | private int width = 0; 15 | private int height = 0; 16 | private boolean adjustToTarget = true; 17 | 18 | private Rect rect; 19 | private int padding; 20 | 21 | public RectangleShape(int width, int height) { 22 | this.width = width; 23 | this.height = height; 24 | init(); 25 | } 26 | 27 | public RectangleShape(Rect bounds) { 28 | this(bounds, false); 29 | } 30 | 31 | public RectangleShape(Rect bounds, boolean fullWidth) { 32 | this.fullWidth = fullWidth; 33 | height = bounds.height(); 34 | if (fullWidth) 35 | width = Integer.MAX_VALUE; 36 | else width = bounds.width(); 37 | init(); 38 | } 39 | 40 | public boolean isAdjustToTarget() { 41 | return adjustToTarget; 42 | } 43 | 44 | public void setAdjustToTarget(boolean adjustToTarget) { 45 | this.adjustToTarget = adjustToTarget; 46 | } 47 | 48 | private void init() { 49 | rect = new Rect(-width / 2, -height / 2, width / 2, height / 2); 50 | } 51 | 52 | @Override 53 | public void draw(Canvas canvas, Paint paint, int x, int y) { 54 | if (!rect.isEmpty()) { 55 | canvas.drawRect( 56 | rect.left + x - padding, 57 | rect.top + y - padding, 58 | rect.right + x + padding, 59 | rect.bottom + y + padding, 60 | paint 61 | ); 62 | } 63 | } 64 | 65 | @Override 66 | public void updateTarget(Target target) { 67 | if (adjustToTarget) { 68 | Rect bounds = target.getBounds(); 69 | height = bounds.height(); 70 | if (fullWidth) 71 | width = Integer.MAX_VALUE; 72 | else width = bounds.width(); 73 | init(); 74 | } 75 | } 76 | 77 | @Override 78 | public int getTotalRadius() { 79 | return (height / 2) + padding; 80 | } 81 | 82 | @Override 83 | public void setPadding(int padding) { 84 | this.padding = padding; 85 | } 86 | 87 | @Override 88 | public int getWidth() { 89 | return width; 90 | } 91 | 92 | @Override 93 | public int getHeight() { 94 | return height; 95 | } 96 | } -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/shape/Shape.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.shape; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | 6 | import uk.co.deanwild.materialshowcaseview.target.Target; 7 | 8 | /** 9 | * Specifies a shape of the target (e.g circle, rectangle). 10 | * Implementations of this interface will be responsible to draw the shape 11 | * at specified center point (x, y). 12 | */ 13 | public interface Shape { 14 | 15 | /** 16 | * Draw shape on the canvas with the center at (x, y) using Paint object provided. 17 | */ 18 | void draw(Canvas canvas, Paint paint, int x, int y); 19 | 20 | /** 21 | * Get width of the shape. 22 | */ 23 | int getWidth(); 24 | 25 | /** 26 | * Get height of the shape. 27 | */ 28 | int getHeight(); 29 | 30 | /** 31 | * Update shape bounds if necessary 32 | */ 33 | void updateTarget(Target target); 34 | 35 | int getTotalRadius(); 36 | 37 | void setPadding(int padding); 38 | } 39 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/target/Target.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.target; 2 | 3 | import android.graphics.Point; 4 | import android.graphics.Rect; 5 | 6 | 7 | public interface Target { 8 | Target NONE = new Target() { 9 | @Override 10 | public Point getPoint() { 11 | return new Point(1000000, 1000000); 12 | } 13 | 14 | @Override 15 | public Rect getBounds() { 16 | Point p = getPoint(); 17 | return new Rect(p.x - 190, p.y - 190, p.x + 190, p.y + 190); 18 | } 19 | }; 20 | 21 | Point getPoint(); 22 | 23 | Rect getBounds(); 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/java/uk/co/deanwild/materialshowcaseview/target/ViewTarget.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseview.target; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Point; 5 | import android.graphics.Rect; 6 | import android.view.View; 7 | 8 | 9 | public class ViewTarget implements Target { 10 | 11 | private final View mView; 12 | 13 | public ViewTarget(View view) { 14 | mView = view; 15 | } 16 | 17 | public ViewTarget(int viewId, Activity activity) { 18 | mView = activity.findViewById(viewId); 19 | } 20 | 21 | @Override 22 | public Point getPoint() { 23 | int[] location = new int[2]; 24 | mView.getLocationInWindow(location); 25 | int x = location[0] + mView.getWidth() / 2; 26 | int y = location[1] + mView.getHeight() / 2; 27 | return new Point(x, y); 28 | } 29 | 30 | @Override 31 | public Rect getBounds() { 32 | int[] location = new int[2]; 33 | mView.getLocationInWindow(location); 34 | return new Rect( 35 | location[0], 36 | location[1], 37 | location[0] + mView.getMeasuredWidth(), 38 | location[1] + mView.getMeasuredHeight() 39 | ); 40 | } 41 | 42 | public View getView() { 43 | return mView; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /library/src/main/res/layout/showcase_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 18 | 19 | 26 | 27 | 41 | 42 | 56 | 57 | -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | applicationId "uk.co.deanwild.materialshowcaseviewsample" 8 | minSdkVersion 14 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support:design:28.0.0' 25 | implementation project(':library') 26 | } 27 | -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/deanwild/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /sample/src/main/java/uk/co/deanwild/materialshowcaseviewsample/CustomExample.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseviewsample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.Toast; 10 | 11 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; 12 | 13 | 14 | public class CustomExample extends AppCompatActivity implements View.OnClickListener { 15 | 16 | private Button mButtonShow; 17 | private Button mButtonReset; 18 | 19 | private static final String SHOWCASE_ID = "custom example"; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | 24 | super.onCreate(savedInstanceState); 25 | setContentView(R.layout.activity_custom_example); 26 | mButtonShow = findViewById(R.id.btn_show); 27 | mButtonShow.setOnClickListener(this); 28 | 29 | mButtonReset = findViewById(R.id.btn_reset); 30 | mButtonReset.setOnClickListener(this); 31 | 32 | presentShowcaseView(1000); // one second delay 33 | } 34 | 35 | @Override 36 | public boolean onCreateOptionsMenu(Menu menu) { 37 | getMenuInflater().inflate(R.menu.activity_custom_example, menu); 38 | return super.onCreateOptionsMenu(menu); 39 | } 40 | 41 | @Override 42 | public boolean onOptionsItemSelected(MenuItem item) { 43 | 44 | if (item.getItemId() == R.id.menu_sample_action) { 45 | View view = findViewById(R.id.menu_sample_action); 46 | new MaterialShowcaseView.Builder(this) 47 | .setTarget(view) 48 | .setShapePadding(96) 49 | .setDismissText("GOT IT") 50 | .setContentText("Example of how to setup a MaterialShowcaseView for menu items in action bar.") 51 | .setContentTextColor(getResources().getColor(R.color.green)) 52 | .setMaskColour(getResources().getColor(R.color.purple)) 53 | .show(); 54 | } 55 | 56 | return super.onOptionsItemSelected(item); 57 | } 58 | 59 | @Override 60 | public void onClick(View v) { 61 | 62 | if (v.getId() == R.id.btn_show) { 63 | 64 | presentShowcaseView(0); 65 | 66 | } else if (v.getId() == R.id.btn_reset) { 67 | 68 | MaterialShowcaseView.resetSingleUse(this, SHOWCASE_ID); 69 | Toast.makeText(this, "Showcase reset", Toast.LENGTH_SHORT).show(); 70 | } 71 | 72 | } 73 | 74 | private void presentShowcaseView(int withDelay) { 75 | new MaterialShowcaseView.Builder(this) 76 | .setTarget(mButtonShow) 77 | .setContentText("This is some amazing feature you should know about") 78 | .setDismissText("GOT IT") 79 | .setDismissOnTouch(true) 80 | .setContentTextColor(getResources().getColor(R.color.green)) 81 | .setMaskColour(getResources().getColor(R.color.purple)) 82 | .setDelay(withDelay) // optional but starting animations immediately in onCreate can make them choppy 83 | .singleUse(SHOWCASE_ID) // provide a unique ID used to ensure it is only shown once 84 | .show(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /sample/src/main/java/uk/co/deanwild/materialshowcaseviewsample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseviewsample; 2 | 3 | 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.support.v7.app.AppCompatActivity; 7 | import android.view.View; 8 | import android.widget.Button; 9 | import android.widget.Toast; 10 | 11 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; 12 | 13 | public class MainActivity extends AppCompatActivity implements View.OnClickListener { 14 | 15 | @Override 16 | protected void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_main); 19 | Button button = findViewById(R.id.btn_simple_example); 20 | button.setOnClickListener(this); 21 | button = findViewById(R.id.btn_custom_example); 22 | button.setOnClickListener(this); 23 | button = findViewById(R.id.btn_sequence_example); 24 | button.setOnClickListener(this); 25 | button = findViewById(R.id.btn_tooltip_example); 26 | button.setOnClickListener(this); 27 | button = findViewById(R.id.btn_reset_all); 28 | button.setOnClickListener(this); 29 | 30 | } 31 | 32 | @Override 33 | public void onClick(View v) { 34 | 35 | Intent intent = null; 36 | 37 | switch (v.getId()) { 38 | case R.id.btn_simple_example: 39 | intent = new Intent(this, SimpleSingleExample.class); 40 | break; 41 | 42 | case R.id.btn_custom_example: 43 | intent = new Intent(this, CustomExample.class); 44 | break; 45 | 46 | case R.id.btn_sequence_example: 47 | intent = new Intent(this, SequenceExample.class); 48 | break; 49 | 50 | case R.id.btn_tooltip_example: 51 | intent = new Intent(this, TooltipExample.class); 52 | break; 53 | 54 | case R.id.btn_reset_all: 55 | MaterialShowcaseView.resetAll(this); 56 | Toast.makeText(this, "All Showcases reset", Toast.LENGTH_SHORT).show(); 57 | break; 58 | } 59 | 60 | if (intent != null) { 61 | startActivity(intent); 62 | } 63 | } 64 | 65 | 66 | } 67 | -------------------------------------------------------------------------------- /sample/src/main/java/uk/co/deanwild/materialshowcaseviewsample/SequenceExample.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseviewsample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.Toast; 8 | 9 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence; 10 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; 11 | import uk.co.deanwild.materialshowcaseview.ShowcaseConfig; 12 | 13 | 14 | public class SequenceExample extends AppCompatActivity implements View.OnClickListener { 15 | 16 | private Button mButtonOne; 17 | private Button mButtonTwo; 18 | private Button mButtonThree; 19 | 20 | private Button mButtonReset; 21 | 22 | private static final String SHOWCASE_ID = "sequence example"; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | 27 | super.onCreate(savedInstanceState); 28 | setContentView(R.layout.activity_sequence_example); 29 | mButtonOne = findViewById(R.id.btn_one); 30 | mButtonOne.setOnClickListener(this); 31 | 32 | mButtonTwo = findViewById(R.id.btn_two); 33 | mButtonTwo.setOnClickListener(this); 34 | 35 | mButtonThree = findViewById(R.id.btn_three); 36 | mButtonThree.setOnClickListener(this); 37 | 38 | mButtonReset = findViewById(R.id.btn_reset); 39 | mButtonReset.setOnClickListener(this); 40 | 41 | presentShowcaseSequence(); // one second delay 42 | } 43 | 44 | @Override 45 | public void onClick(View v) { 46 | 47 | if (v.getId() == R.id.btn_one || v.getId() == R.id.btn_two || v.getId() == R.id.btn_three) { 48 | 49 | presentShowcaseSequence(); 50 | 51 | } else if (v.getId() == R.id.btn_reset) { 52 | 53 | MaterialShowcaseView.resetSingleUse(this, SHOWCASE_ID); 54 | Toast.makeText(this, "Showcase reset", Toast.LENGTH_SHORT).show(); 55 | } 56 | 57 | } 58 | 59 | private void presentShowcaseSequence() { 60 | 61 | ShowcaseConfig config = new ShowcaseConfig(); 62 | config.setDelay(500); // half second between each showcase view 63 | 64 | MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(this, SHOWCASE_ID); 65 | 66 | sequence.setOnItemShownListener(new MaterialShowcaseSequence.OnSequenceItemShownListener() { 67 | @Override 68 | public void onShow(MaterialShowcaseView itemView, int position) { 69 | Toast.makeText(itemView.getContext(), "Item #" + position, Toast.LENGTH_SHORT).show(); 70 | } 71 | }); 72 | 73 | sequence.setConfig(config); 74 | 75 | sequence.addSequenceItem(mButtonOne, "This is button one", "GOT IT"); 76 | 77 | sequence.addSequenceItem( 78 | new MaterialShowcaseView.Builder(this) 79 | .setSkipText("SKIP") 80 | .setTarget(mButtonTwo) 81 | .setDismissText("GOT IT") 82 | .setContentText("This is button two") 83 | .withRectangleShape(true) 84 | .build() 85 | ); 86 | 87 | sequence.addSequenceItem( 88 | new MaterialShowcaseView.Builder(this) 89 | .setTarget(mButtonThree) 90 | .setDismissText("GOT IT") 91 | .setContentText("This is button three") 92 | .withRectangleShape() 93 | .build() 94 | ); 95 | 96 | sequence.start(); 97 | 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /sample/src/main/java/uk/co/deanwild/materialshowcaseviewsample/SimpleSingleExample.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseviewsample; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.Toast; 8 | 9 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; 10 | import uk.co.deanwild.materialshowcaseview.shape.OvalShape; 11 | 12 | 13 | public class SimpleSingleExample extends AppCompatActivity implements View.OnClickListener { 14 | 15 | private Button mButtonShow; 16 | private Button mButtonReset; 17 | 18 | private static final String SHOWCASE_ID = "simple example"; 19 | 20 | @Override 21 | protected void onCreate(Bundle savedInstanceState) { 22 | 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_simple_single_example); 25 | mButtonShow = (Button) findViewById(R.id.btn_show); 26 | mButtonShow.setOnClickListener(this); 27 | 28 | mButtonReset = (Button) findViewById(R.id.btn_reset); 29 | mButtonReset.setOnClickListener(this); 30 | 31 | presentShowcaseView(1000); // one second delay 32 | } 33 | 34 | @Override 35 | public void onClick(View v) { 36 | 37 | if (v.getId() == R.id.btn_show) { 38 | 39 | presentShowcaseView(0); 40 | 41 | } else if (v.getId() == R.id.btn_reset) { 42 | 43 | MaterialShowcaseView.resetSingleUse(this, SHOWCASE_ID); 44 | Toast.makeText(this, "Showcase reset", Toast.LENGTH_SHORT).show(); 45 | } 46 | 47 | } 48 | 49 | private void presentShowcaseView(int withDelay) { 50 | new MaterialShowcaseView.Builder(this) 51 | .setTarget(mButtonShow) 52 | .setShape(new OvalShape()) 53 | .setTitleText("Hello") 54 | .setDismissText("GOT IT") 55 | .setContentText("This is some amazing feature you should know about") 56 | .setDelay(withDelay) // optional but starting animations immediately in onCreate can make them choppy 57 | .singleUse(SHOWCASE_ID) // provide a unique ID used to ensure it is only shown once 58 | // .useFadeAnimation() // remove comment if you want to use fade animations for Lollipop & up 59 | .show(); 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /sample/src/main/java/uk/co/deanwild/materialshowcaseviewsample/TooltipExample.java: -------------------------------------------------------------------------------- 1 | package uk.co.deanwild.materialshowcaseviewsample; 2 | 3 | import android.app.Activity; 4 | import android.graphics.Color; 5 | import android.os.Bundle; 6 | import android.support.design.widget.FloatingActionButton; 7 | import android.support.v7.widget.Toolbar; 8 | import android.view.View; 9 | import android.widget.Button; 10 | import android.widget.Toast; 11 | 12 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence; 13 | import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; 14 | import uk.co.deanwild.materialshowcaseview.ShowcaseConfig; 15 | import uk.co.deanwild.materialshowcaseview.ShowcaseTooltip; 16 | 17 | 18 | public class TooltipExample extends Activity implements View.OnClickListener { 19 | 20 | private Button mButtonShow; 21 | private Button mButtonReset; 22 | private FloatingActionButton fab; 23 | private Toolbar toolbar; 24 | 25 | private static final String SHOWCASE_ID = "tooltip example"; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | 30 | super.onCreate(savedInstanceState); 31 | setContentView(R.layout.activity_tooltip_example); 32 | mButtonShow = findViewById(R.id.btn_show); 33 | mButtonShow.setOnClickListener(this); 34 | 35 | mButtonReset = findViewById(R.id.btn_reset); 36 | mButtonReset.setOnClickListener(this); 37 | 38 | fab = findViewById(R.id.fab); 39 | fab.setOnClickListener(this); 40 | 41 | toolbar = findViewById(R.id.toolbar); 42 | 43 | presentShowcaseView(); // one second delay 44 | } 45 | 46 | @Override 47 | public void onClick(View v) { 48 | 49 | if (v.getId() == R.id.btn_show) { 50 | 51 | presentShowcaseView(); 52 | 53 | } else if (v.getId() == R.id.btn_reset) { 54 | 55 | MaterialShowcaseView.resetSingleUse(this, SHOWCASE_ID); 56 | Toast.makeText(this, "Showcase reset", Toast.LENGTH_SHORT).show(); 57 | } else if (v.getId() == R.id.fab) { 58 | 59 | } 60 | 61 | } 62 | 63 | void presentShowcaseView() { 64 | 65 | 66 | ShowcaseConfig config = new ShowcaseConfig(); 67 | config.setDelay(500); 68 | 69 | MaterialShowcaseSequence sequence = new MaterialShowcaseSequence(this, SHOWCASE_ID); 70 | 71 | //sequence.setConfig(config); 72 | 73 | ShowcaseTooltip toolTip1 = ShowcaseTooltip.build(this) 74 | .corner(30) 75 | .textColor(Color.parseColor("#007686")) 76 | .text("This is a very funky tooltip

This is a very long sentence to test how this tooltip behaves with longer strings.

Tap anywhere to continue"); 77 | 78 | 79 | sequence.addSequenceItem( 80 | 81 | new MaterialShowcaseView.Builder(this) 82 | .setTarget(toolbar) 83 | .setToolTip(toolTip1) 84 | .withRectangleShape() 85 | .setTooltipMargin(30) 86 | .setShapePadding(50) 87 | .setDismissOnTouch(true) 88 | .setMaskColour(getResources().getColor(R.color.tooltip_mask)) 89 | .build() 90 | ); 91 | 92 | 93 | ShowcaseTooltip toolTip2 = ShowcaseTooltip.build(this) 94 | .corner(30) 95 | .textColor(Color.parseColor("#007686")) 96 | .text("This is another very funky tooltip"); 97 | 98 | sequence.addSequenceItem( 99 | new MaterialShowcaseView.Builder(this) 100 | .setTarget(fab) 101 | .setToolTip(toolTip2) 102 | .setTooltipMargin(30) 103 | .setShapePadding(50) 104 | .setDismissOnTouch(true) 105 | .setMaskColour(getResources().getColor(R.color.tooltip_mask)) 106 | .build() 107 | ); 108 | 109 | sequence.start(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_android_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deano2390/MaterialShowcaseView/528080516c72c2c440ed58675a6be30096a458d7/sample/src/main/res/drawable-xxhdpi/ic_android_white_24dp.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_edit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deano2390/MaterialShowcaseView/528080516c72c2c440ed58675a6be30096a458d7/sample/src/main/res/drawable-xxhdpi/ic_edit.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_custom_example.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 |