├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── .travis.yml ├── README.md ├── androidTutorialBubbles ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── lovoo │ │ └── tutorialbubbles │ │ ├── LayoutManagedTutorialScreen.java │ │ ├── TutorialScreen.java │ │ ├── WindowManagedTutorialScreen.java │ │ ├── layout │ │ ├── BubbleDrawable.java │ │ └── TutorialScreenContainerLayout.java │ │ └── utils │ │ ├── Utils.java │ │ └── Vector2D.java │ └── res │ └── values │ ├── colors.xml │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screen1.png ├── screen2.png ├── settings.gradle └── tutorialsDemoApp ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── lovoo │ └── tutorialbubbledemo │ └── ApplicationTest.java ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── lovoo │ │ └── tutorialbubbledemo │ │ └── MainActivity.java └── res │ ├── layout │ ├── activity_main.xml │ ├── button_tutorial_layout.xml │ ├── content_main.xml │ └── fab_tutorial_layout.xml │ ├── menu │ └── menu_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── test └── java └── com └── lovoo └── tutorialbubbledemo └── ExampleUnitTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | TutorialBubbleDemo -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 26 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Abstraction issuesJava 46 | 47 | 48 | Android Lint 49 | 50 | 51 | Assignment issuesJava 52 | 53 | 54 | Bitwise operation issuesJava 55 | 56 | 57 | Class metricsJava 58 | 59 | 60 | Class structureJava 61 | 62 | 63 | Cloning issuesJava 64 | 65 | 66 | Code maturity issuesJava 67 | 68 | 69 | Code style issuesJava 70 | 71 | 72 | Compiler issuesJava 73 | 74 | 75 | Concurrency annotation issuesJava 76 | 77 | 78 | Control flow issuesJava 79 | 80 | 81 | Data flow issuesJava 82 | 83 | 84 | Declaration redundancyJava 85 | 86 | 87 | Dependency issuesJava 88 | 89 | 90 | Encapsulation issuesJava 91 | 92 | 93 | Error handlingJava 94 | 95 | 96 | Finalization issuesJava 97 | 98 | 99 | GPath inspectionsGroovy 100 | 101 | 102 | General 103 | 104 | 105 | GeneralJava 106 | 107 | 108 | Google Cloud Endpoints 109 | 110 | 111 | Groovy 112 | 113 | 114 | ImportsJava 115 | 116 | 117 | Inheritance issuesJava 118 | 119 | 120 | Initialization issuesJava 121 | 122 | 123 | Internationalization issues 124 | 125 | 126 | Internationalization issuesJava 127 | 128 | 129 | J2ME issuesJava 130 | 131 | 132 | JUnit issues 133 | 134 | 135 | JUnit issuesJava 136 | 137 | 138 | Java 139 | 140 | 141 | Java language level issuesJava 142 | 143 | 144 | Java language level migration aidsJava 145 | 146 | 147 | JavaBeans issuesJava 148 | 149 | 150 | JavaFX 151 | 152 | 153 | Javadoc issuesJava 154 | 155 | 156 | Language Injection 157 | 158 | 159 | Logging issuesJava 160 | 161 | 162 | Manifest 163 | 164 | 165 | Maven 166 | 167 | 168 | Memory issuesJava 169 | 170 | 171 | Method metricsJava 172 | 173 | 174 | Modularization issuesJava 175 | 176 | 177 | Naming ConventionsGroovy 178 | 179 | 180 | Naming conventionsJava 181 | 182 | 183 | Numeric issuesJava 184 | 185 | 186 | Packaging issuesJava 187 | 188 | 189 | Performance issuesJava 190 | 191 | 192 | Portability issuesJava 193 | 194 | 195 | Probable bugsGroovy 196 | 197 | 198 | Probable bugsJava 199 | 200 | 201 | Properties Files 202 | 203 | 204 | Resource management issuesJava 205 | 206 | 207 | Security issuesJava 208 | 209 | 210 | Serialization issuesJava 211 | 212 | 213 | StyleGroovy 214 | 215 | 216 | TestNG 217 | 218 | 219 | Threading issuesGroovy 220 | 221 | 222 | Threading issuesJava 223 | 224 | 225 | Verbose or redundant code constructsJava 226 | 227 | 228 | Visibility issuesJava 229 | 230 | 231 | toString() issuesJava 232 | 233 | 234 | 235 | 236 | Android 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 258 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | sudo: false 4 | 5 | branches: 6 | only: master 7 | 8 | script: gradle :androidTutorialBubbles:assembleRelease 9 | 10 | android: 11 | components: 12 | - platform-tools 13 | - tools 14 | - build-tools-23.0.1 15 | - android-23 16 | 17 | - extra-android-support 18 | - extra-android-m2repository 19 | - extra-google-m2repository 20 | - sys-img-armeabi-v7a-android-19 21 | - sys-img-x86-android-17 22 | 23 | after_success: 24 | # run bintray upload straight after deploy 25 | - gradle bintrayUpload -PbintrayUser=$USER -PbintrayKey=$KEY -PdryRun=false 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidTutorialBubbles [![Build Status](https://travis-ci.org/Lovoo/android-tutorial-bubbles.svg)](https://travis-ci.org/Lovoo/android-tutorial-bubbles) [![Download](https://api.bintray.com/packages/lovoo/maven/AndroidTutorialBubbles/images/download.svg) ](https://bintray.com/lovoo/maven/AndroidTutorialBubbles/_latestVersion) 2 | A little ui framework that displays a styled tutorial bubble, which positions and scales itself based on a given anchor view. 3 | 4 | ###Usage 5 | 6 | If you have some element within your layout, that should be explained with a small tutorial, this small library may be your way to go! 7 | 8 | __Features:__ 9 | * displays a styleable bubble with custom XML layout 10 | * popup bubble scales itself according to its content and relative to the given anchor view and the available screen space 11 | * bubble displays a dynamically drawn funnel that points toward the anchor view 12 | * anchor view or any other view can be highlighted while the background is dimmed 13 | * simple builder-pattern with chaining config calls 14 | 15 | The library uses two approaches to display the tutorial bubble. If you supply a parent view within the builder, the parent will be used to draw the tutorial. 16 | If no parent view is set and you set `android.permission.SYSTEM_ALERT_WINDOW` permission in your manifest instead, the system window will be used to draw the bubble. In the later case, you'll need to relay the `onResume()` and `onPause()` events from either an activity or fragment to your tutorial. 17 | 18 | Check out the code example in the demo project. 19 | 20 | ###Screenshots 21 | 22 | 23 | 24 | 25 | 26 | Licence 27 | 28 | Copyright (c) 2015, LOVOO GmbH 29 | All rights reserved. 30 | 31 | Redistribution and use in source and binary forms, with or without 32 | modification, are permitted provided that the following conditions are met: 33 | 34 | * Redistributions of source code must retain the above copyright notice, this 35 | list of conditions and the following disclaimer. 36 | 37 | * Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the documentation 39 | and/or other materials provided with the distribution. 40 | 41 | * Neither the name of LOVOO GmbH nor the names of its 42 | contributors may be used to endorse or promote products derived from 43 | this software without specific prior written permission. 44 | 45 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 46 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 47 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 48 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 49 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 50 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 51 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOEVER 52 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 53 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 54 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 55 | -------------------------------------------------------------------------------- /androidTutorialBubbles/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /androidTutorialBubbles/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | mavenCentral() 3 | jcenter() 4 | } 5 | 6 | 7 | apply plugin: 'com.android.library' 8 | apply plugin: 'findbugs' 9 | apply plugin: 'com.novoda.bintray-release' 10 | 11 | findbugs { 12 | toolVersion = "3.0.1" 13 | } 14 | 15 | android { 16 | compileSdkVersion 23 17 | buildToolsVersion "23.0.1" 18 | 19 | defaultConfig { 20 | minSdkVersion 14 21 | targetSdkVersion 23 22 | versionCode 1 23 | versionName "1.0.0" 24 | } 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | } 32 | 33 | dependencies { 34 | compile fileTree(dir: 'libs', include: ['*.jar']) 35 | 36 | findbugs configurations.findbugsPlugins.dependencies 37 | findbugsPlugins 'com.mebigfatguy.fb-contrib:fb-contrib:6.0.1' 38 | compile 'com.google.code.findbugs:jsr305:2.0.1' 39 | } 40 | 41 | publish { 42 | userOrg = 'lovoo' 43 | groupId = 'com.lovoo' 44 | artifactId = 'AndroidTutorialBubbles' 45 | version = '1.0.0' 46 | description = 'A little ui framework that displays a styled tutorial bubble, which positions and scales itself based on a given anchor view.' 47 | website = 'https://github.com/Lovoo/android-tutorial-bubbles' 48 | issueTracker = 'https://github.com/Lovoo/android-tutorial-bubbles/issues' 49 | repository = 'https://github.com/Lovoo/android-tutorial-bubbles.git' 50 | } 51 | -------------------------------------------------------------------------------- /androidTutorialBubbles/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 /usr/local/opt/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 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/LayoutManagedTutorialScreen.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles; 2 | 3 | import android.view.View; 4 | import android.view.ViewGroup; 5 | 6 | import com.lovoo.tutorialbubbles.layout.TutorialScreenContainerLayout; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | 11 | 12 | /** 13 | * This implementation of TutorialScreen uses the layout hierarchy to show the tutorial. 14 | * In order to work a mParent {@code ViewGroup} must be supplied, in which the tutorial screen will be added to. 15 | * 16 | * @author Johannes Braun 17 | */ 18 | class LayoutManagedTutorialScreen extends TutorialScreen { 19 | 20 | private static final String TAG = LayoutManagedTutorialScreen.class.getSimpleName(); 21 | 22 | @Nullable 23 | private final View mParent; 24 | @Nonnull 25 | private View mContainerLayout; 26 | private boolean mIsShowing; 27 | 28 | public LayoutManagedTutorialScreen ( @Nonnull TutorialBuilder builder ) { 29 | super(builder); 30 | mParent = builder.mParentContainer; 31 | init(builder); 32 | } 33 | 34 | @Override 35 | protected void init ( TutorialBuilder builder ) { 36 | mContainerLayout = createContainerLayoutWithTutorial(builder); 37 | mContainerLayout.setLayerType(View.LAYER_TYPE_HARDWARE, null); 38 | mContainerLayout.setClickable(true); 39 | } 40 | 41 | @Override 42 | protected TutorialScreenContainerLayout.TutorialScreenDimension getTutorialDimensions () { 43 | if(mParent != null) { 44 | return new TutorialScreenContainerLayout.TutorialScreenDimension(mParent.getMeasuredWidth(), mParent.getMeasuredHeight(), false); 45 | } else { 46 | return new TutorialScreenContainerLayout.TutorialScreenDimension(0, 0, false); 47 | } 48 | } 49 | 50 | @Override 51 | public void showTutorial () { 52 | super.showTutorial(); 53 | addLayout(); 54 | } 55 | 56 | @Override 57 | public void dismissTutorial () { 58 | super.dismissTutorial(); 59 | removeLayout(); 60 | } 61 | 62 | @Override 63 | public void setDismissible ( boolean dismissible ) { 64 | if (dismissible) { 65 | mContainerLayout.setOnClickListener(new View.OnClickListener() { 66 | @Override 67 | public void onClick ( View v ) { 68 | dismissTutorial(); 69 | } 70 | }); 71 | } else { 72 | mContainerLayout.setOnClickListener(null); 73 | mContainerLayout.setClickable(true); 74 | } 75 | } 76 | 77 | @Override 78 | public boolean isShowing () { 79 | return mIsShowing; 80 | } 81 | 82 | @Override 83 | public void onPause () { 84 | removeLayout(); 85 | } 86 | 87 | @Override 88 | public void onResume () { 89 | addLayout(); 90 | } 91 | 92 | private void addLayout () { 93 | if (mParent instanceof ViewGroup) { 94 | if(mContainerLayout.getParent() != null){ 95 | return; 96 | } 97 | if(mShouldShow) { 98 | mIsShowing = true; 99 | ViewGroup viewGroup = (ViewGroup) mParent; 100 | viewGroup.addView(mContainerLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 101 | } 102 | } 103 | } 104 | 105 | private void removeLayout () { 106 | if (mParent instanceof ViewGroup) { 107 | mIsShowing = false; 108 | ViewGroup viewGroup = (ViewGroup) mParent; 109 | viewGroup.removeView(mContainerLayout); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/TutorialScreen.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.WindowManager; 9 | 10 | import com.lovoo.tutorialbubbles.layout.TutorialScreenContainerLayout; 11 | import com.lovoo.tutorialbubbles.utils.Utils; 12 | 13 | import java.util.ArrayList; 14 | 15 | import javax.annotation.CheckForNull; 16 | import javax.annotation.Nonnull; 17 | 18 | /** 19 | * Abstract class that handles creation and display state of a tutorial. 20 | * supports to internal strategies: 21 | * 25 | * if SYSTEM_ALERT_WINDOW is set and no parent layout is defined, {@link WindowManager} is used. 26 | * In that case {@link #onPause()} and {@link #onResume()} must be called from the outside android mContext in order to correctly show and dismiss 27 | * the tutorial when the app moves from foreground to background 28 | * 29 | * @author Johannes Braun 30 | */ 31 | public abstract class TutorialScreen { 32 | 33 | //region members 34 | @Nonnull 35 | protected Context mContext; 36 | protected boolean mShouldShow = false; 37 | //endregion 38 | 39 | private TutorialScreen () { 40 | } 41 | 42 | TutorialScreen ( TutorialBuilder builder ) { 43 | mContext = builder.mContext; 44 | } 45 | 46 | /** 47 | * Creates and inits the tutorial. 48 | * 49 | * @param builder the builder to init this instance 50 | */ 51 | protected abstract void init ( TutorialBuilder builder ); 52 | 53 | protected final View createContainerLayoutWithTutorial ( TutorialBuilder builder ) { 54 | View tutorialLayout = LayoutInflater.from(mContext).inflate(builder.mTutorialLayoutRes, null); 55 | 56 | // run callback for inflated layout, if set 57 | if (builder.mTutorialLayoutInflatedListener != null) { 58 | builder.mTutorialLayoutInflatedListener.onLayoutInflated(tutorialLayout); 59 | } 60 | 61 | TutorialScreenContainerLayout containerLayout = new TutorialScreenContainerLayout(mContext); 62 | containerLayout.init(tutorialLayout, builder.mAnchorView, getTutorialDimensions()); 63 | if (builder.mFunnelWidth != null) { 64 | containerLayout.setFunnelWidth(builder.mFunnelWidth); 65 | } 66 | if (builder.mFunnelLength != null) { 67 | containerLayout.setFunnelLength(builder.mFunnelLength); 68 | } 69 | if (builder.mBackgroundColor != null) { 70 | containerLayout.setTutorialBackgroundColor(builder.mBackgroundColor); 71 | } 72 | 73 | if (builder.mOffset != null) { 74 | containerLayout.setOffestFromAnchor(builder.mOffset); 75 | } 76 | 77 | containerLayout.setHighlightViews(builder.mHighlightViews); 78 | 79 | int padding = Utils.dpToPx(mContext, 15); 80 | containerLayout.setPadding(padding, padding, padding, padding); 81 | 82 | if (builder.mDismissible != null && builder.mDismissible) { 83 | containerLayout.setOnClickListener(new View.OnClickListener() { 84 | @Override 85 | public void onClick ( View v ) { 86 | dismissTutorial(); 87 | } 88 | }); 89 | } 90 | 91 | return containerLayout; 92 | } 93 | 94 | //region public methods 95 | 96 | /** 97 | * displays the tutorial on the screen. 98 | */ 99 | public void showTutorial () { 100 | mShouldShow = true; 101 | } 102 | 103 | /** 104 | * dismisses the {@code TutorialScreen} by cleaning up the {@code WindowManager}. 105 | */ 106 | public void dismissTutorial () { 107 | mShouldShow = false; 108 | } 109 | 110 | /** 111 | * callback that should be called from Android {@code Activity} or {@code Fragment}. 112 | * removes {@code TutorialScreen} 113 | */ 114 | public abstract void onPause (); 115 | 116 | /** 117 | * callback that should be called from Android {@code Activity} or {@code Fragment}. 118 | * readdes {@code TutorialScreen} 119 | */ 120 | public abstract void onResume (); 121 | 122 | /** 123 | * alter dismiss status after creation. 124 | * 125 | * @param dismissible true if dismissible by pressing anywhere outside of the layout, false otherwise 126 | */ 127 | public abstract void setDismissible ( boolean dismissible ); 128 | 129 | /** 130 | * returns visible state of the popup. 131 | * 132 | * @return true if currently showing, false otherwise 133 | */ 134 | public abstract boolean isShowing (); 135 | //endregion 136 | 137 | //region inner classes 138 | 139 | /** 140 | * implement this interface if you want to react to layout inflation. 141 | */ 142 | public interface OnTutorialLayoutInflatedListener { 143 | /** 144 | * is invoked right after layout inflation. 145 | * 146 | * @param view the inflated view 147 | */ 148 | void onLayoutInflated ( View view ); 149 | } 150 | 151 | protected abstract TutorialScreenContainerLayout.TutorialScreenDimension getTutorialDimensions (); 152 | 153 | /** 154 | * builder class that composes an TutorialScreen Instance. 155 | */ 156 | public static class TutorialBuilder { 157 | 158 | @Nonnull 159 | protected Context mContext; 160 | @Nonnull 161 | protected final Integer mTutorialLayoutRes; 162 | @Nonnull 163 | protected final View mAnchorView; 164 | @CheckForNull 165 | protected OnTutorialLayoutInflatedListener mTutorialLayoutInflatedListener; 166 | @CheckForNull 167 | protected Integer mFunnelWidth; 168 | @CheckForNull 169 | protected Integer mFunnelLength; 170 | @CheckForNull 171 | protected Integer mBackgroundColor; 172 | @CheckForNull 173 | protected Boolean mDismissible; 174 | @Nonnull 175 | protected ArrayList mHighlightViews; 176 | @CheckForNull 177 | protected View mParentContainer; 178 | @CheckForNull 179 | Integer mOffset; 180 | 181 | /** 182 | * creates a builder to config and return a {@link TutorialScreen}. 183 | * 184 | * @param tutorialLayoutRes a layout resource 185 | * @param anchorView a view at which the layout resource will be displayed to 186 | */ 187 | public TutorialBuilder ( @Nonnull Integer tutorialLayoutRes, @Nonnull View anchorView ) { 188 | this.mContext = anchorView.getContext(); 189 | this.mTutorialLayoutRes = tutorialLayoutRes; 190 | this.mAnchorView = anchorView; 191 | this.mHighlightViews = new ArrayList<>(); 192 | } 193 | 194 | /** 195 | * called after all configuation is done. 196 | * 197 | * @return TutorialScreen Instance 198 | */ 199 | @CheckForNull 200 | public TutorialScreen build () { 201 | if (mParentContainer != null) { 202 | return new LayoutManagedTutorialScreen(this); 203 | } else if (hasAppWindowManagerPermission()) { 204 | return new WindowManagedTutorialScreen(this); 205 | } 206 | return null; 207 | } 208 | 209 | /** 210 | * set a listener that will be called each time the layout resource is inflated. 211 | * 212 | * @param listener the listener 213 | * @return this builder 214 | */ 215 | public TutorialBuilder setOnTutorialLayoutInflatedListener ( OnTutorialLayoutInflatedListener listener ) { 216 | this.mTutorialLayoutInflatedListener = listener; 217 | return this; 218 | } 219 | 220 | /** 221 | * sets the width of the funnel. 222 | * 223 | * @param funnelWidth width in px 224 | * @return this builder 225 | */ 226 | public TutorialBuilder setFunnelWidth ( int funnelWidth ) { 227 | this.mFunnelWidth = funnelWidth; 228 | return this; 229 | } 230 | 231 | /** 232 | * sets the width of the funnel. 233 | * 234 | * @param funnelLength length in px 235 | * @return this builder 236 | */ 237 | public TutorialBuilder setFunnelLength ( int funnelLength ) { 238 | this.mFunnelLength = funnelLength; 239 | return this; 240 | } 241 | 242 | /** 243 | * sets the color of the tutorial as a color resource. 244 | * 245 | * @param color resource int 246 | * @return this builder 247 | */ 248 | public TutorialBuilder setTutorialBackgroundColor ( int color ) { 249 | this.mBackgroundColor = color; 250 | return this; 251 | } 252 | 253 | /** 254 | * sets wether the tutorial popup can be dismissed by pressing somewhere next to the layout. 255 | * 256 | * @param dismissible true if dismissible, false otherwise 257 | * @return this builder 258 | */ 259 | public TutorialBuilder setDismissible ( Boolean dismissible ) { 260 | this.mDismissible = dismissible; 261 | return this; 262 | } 263 | 264 | /** 265 | * adds a view that wont be dimmed by the background. 266 | * 267 | * @param view the highlighted view 268 | * @param useViewBoundsAsMask true if the rectangular bounds of the view can be used, false if drawing cache should be used 269 | * (slower but non rectangular views will be supported) 270 | * @return this builder 271 | */ 272 | public TutorialBuilder addHighlightView ( View view, boolean useViewBoundsAsMask ) { 273 | this.mHighlightViews.add(new HighlightView(view, useViewBoundsAsMask)); 274 | return this; 275 | } 276 | 277 | /** 278 | * sets the parent layout, at which the {@code TutorialScreen} will be added to. 279 | * 280 | * @param parent should be a viewgroup 281 | * @return this builder 282 | */ 283 | public TutorialBuilder setParentLayout ( View parent ) { 284 | mParentContainer = parent; 285 | return this; 286 | } 287 | 288 | /** 289 | * sets the offset from the anchor view. 290 | * 291 | * @param offset offset in px 292 | * @return this builder 293 | */ 294 | public TutorialBuilder setTutorialOffsetFromAnchor ( int offset ) { 295 | this.mOffset = offset; 296 | return this; 297 | } 298 | 299 | private boolean hasAppWindowManagerPermission () { 300 | try { 301 | PackageInfo info = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), PackageManager.GET_PERMISSIONS); 302 | if (info.requestedPermissions != null) { 303 | for (String p : info.requestedPermissions) { 304 | if (p.equals("android.permission.SYSTEM_ALERT_WINDOW")) { 305 | return true; 306 | } 307 | } 308 | } 309 | } catch (Exception e) { 310 | e.printStackTrace(); 311 | } 312 | return false; 313 | } 314 | } 315 | 316 | public static class HighlightView { 317 | public final View mView; 318 | public final boolean mUseViewBoundsAsMask; 319 | 320 | public HighlightView ( View mView, boolean mUseViewBoundsAsMask ) { 321 | this.mView = mView; 322 | this.mUseViewBoundsAsMask = mUseViewBoundsAsMask; 323 | } 324 | } 325 | 326 | //endregion 327 | } 328 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/WindowManagedTutorialScreen.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles; 2 | 3 | import android.content.Context; 4 | import android.graphics.PixelFormat; 5 | import android.view.View; 6 | import android.view.WindowManager; 7 | 8 | import com.lovoo.tutorialbubbles.utils.Utils; 9 | import com.lovoo.tutorialbubbles.layout.TutorialScreenContainerLayout; 10 | 11 | import javax.annotation.CheckForNull; 12 | import javax.annotation.Nonnull; 13 | 14 | /** 15 | * This implementation of TutorialScreen uses androids {@link WindowManager} to display a tutorial popup. 16 | * It requires SYSTEM_ALERT_WINDOW permission in order to work. 17 | * 18 | * @author Johannes Braun 19 | */ 20 | public class WindowManagedTutorialScreen extends TutorialScreen { 21 | 22 | public static final String TAG = WindowManagedTutorialScreen.class.getSimpleName(); 23 | 24 | @Nonnull 25 | private WindowManager mWindowManager; 26 | @CheckForNull 27 | private WindowEntry mAddedView; 28 | 29 | protected WindowManagedTutorialScreen ( @Nonnull TutorialBuilder builder ) { 30 | super(builder); 31 | mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 32 | init(builder); 33 | } 34 | 35 | @Override 36 | protected void init ( TutorialBuilder builder ) { 37 | View containerLayout = createContainerLayoutWithTutorial(builder); 38 | 39 | int flags; 40 | if (Utils.isWindowTranslucent(mContext)) { 41 | flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 42 | | WindowManager.LayoutParams.FLAG_FULLSCREEN 43 | | WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; 44 | } else { 45 | flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 46 | | WindowManager.LayoutParams.FLAG_FULLSCREEN; 47 | } 48 | 49 | WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, 50 | WindowManager.LayoutParams.MATCH_PARENT, 51 | WindowManager.LayoutParams.TYPE_PHONE, 52 | flags, 53 | PixelFormat.TRANSLUCENT); 54 | 55 | mAddedView = new WindowEntry(containerLayout, params, false); 56 | } 57 | 58 | @Override 59 | protected TutorialScreenContainerLayout.TutorialScreenDimension getTutorialDimensions () { 60 | return new TutorialScreenContainerLayout.TutorialScreenDimension(Utils.getDisplayWidth(mContext), Utils.getDisplayHeight(mContext), true); 61 | } 62 | 63 | @Override 64 | public void showTutorial () { 65 | super.showTutorial(); 66 | addViewsToWindow(); 67 | } 68 | 69 | @Override 70 | public void dismissTutorial () { 71 | removeViewsFromWindow(); 72 | super.dismissTutorial(); 73 | } 74 | 75 | @Override 76 | public boolean isShowing () { 77 | if (mAddedView == null) { 78 | return false; 79 | } 80 | return mAddedView.isAdded; 81 | } 82 | 83 | @Override 84 | public void onPause () { 85 | removeViewsFromWindow(); 86 | } 87 | 88 | @Override 89 | public void onResume () { 90 | addViewsToWindow(); 91 | } 92 | 93 | @Override 94 | public void setDismissible ( boolean dismissible ) { 95 | if (mAddedView != null) { 96 | if (dismissible) { 97 | mAddedView.view.setOnClickListener(new View.OnClickListener() { 98 | @Override 99 | public void onClick ( View v ) { 100 | dismissTutorial(); 101 | } 102 | }); 103 | } else { 104 | mAddedView.view.setOnClickListener(null); 105 | } 106 | } 107 | } 108 | 109 | //region privat and protected internal methods 110 | private void addViewsToWindow () { 111 | if (!mShouldShow || mAddedView == null) { 112 | return; 113 | } 114 | 115 | if (!mAddedView.isAdded) { 116 | mWindowManager.addView(mAddedView.view, mAddedView.layoutParams); 117 | mAddedView.isAdded = true; 118 | } 119 | } 120 | 121 | private void removeViewsFromWindow () { 122 | if (mAddedView != null) { 123 | if (mAddedView.isAdded) { 124 | try { 125 | mWindowManager.removeView(mAddedView.view); 126 | } catch (IllegalArgumentException e) { 127 | e.printStackTrace(); 128 | } 129 | } 130 | mAddedView.isAdded = false; 131 | } 132 | } 133 | 134 | /** 135 | * encapsulates entry, that can be added to or removed from the WindowManager. 136 | */ 137 | protected static class WindowEntry { 138 | View view; 139 | WindowManager.LayoutParams layoutParams; 140 | boolean isAdded; 141 | 142 | public WindowEntry ( View view, WindowManager.LayoutParams layoutParams, boolean isAdded ) { 143 | this.view = view; 144 | this.layoutParams = layoutParams; 145 | this.isAdded = isAdded; 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/layout/BubbleDrawable.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles.layout; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Color; 5 | import android.graphics.ColorFilter; 6 | import android.graphics.Paint; 7 | import android.graphics.Path; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.Rect; 10 | import android.graphics.RectF; 11 | import android.graphics.drawable.Drawable; 12 | import android.view.Gravity; 13 | 14 | import com.lovoo.tutorialbubbles.utils.Vector2D; 15 | 16 | import javax.annotation.Nonnull; 17 | 18 | /** 19 | * Drawable that draws an Comic-Style-Bubble. You have to config this Drawable by code with 20 | * {@code BubbleDrawable.build()} or {@code reBuild()}. All configs can be chained. 21 | *

22 | * Created by mariokreussel on 02.06.14. 23 | */ 24 | public class BubbleDrawable extends Drawable { 25 | 26 | /** 27 | * Configuration class for {@link BubbleDrawable}. 28 | * Provides all setter as chains. You have to call 29 | * {@code finish()} to get your created Drawable instance. 30 | */ 31 | public static class BubbleBuilder { 32 | 33 | private BubbleDrawable mDrawable; 34 | 35 | private BubbleBuilder ( BubbleDrawable drawable ) { 36 | mDrawable = drawable; 37 | mDrawable.initPaint(); 38 | } 39 | 40 | /** 41 | * Set Bubble background color. 42 | * @param color target value 43 | * @return current instance 44 | */ 45 | public BubbleBuilder setBubbleColor ( int color ) { 46 | mDrawable.mBubblePaint.setColor(color); 47 | return this; 48 | } 49 | 50 | /** 51 | * Set Edge color. 52 | * @param color target value 53 | * @return current instance 54 | */ 55 | public BubbleBuilder setEdgeColor ( int color ) { 56 | mDrawable.mEdgePaint.setColor(color); 57 | return this; 58 | } 59 | 60 | /** 61 | * Set Edge stroke thickness. 62 | * @param thickness target value, {@code 0 or less} for no edge 63 | * @return current instance 64 | */ 65 | public BubbleBuilder setEdgeThickness ( float thickness ) { 66 | mDrawable.mEdgePaint.setStrokeWidth(thickness); 67 | return this; 68 | } 69 | 70 | /** 71 | * Set Bubble round corner values in pixel. 72 | * @param corner target value 73 | * @return current instance 74 | */ 75 | public BubbleBuilder setBubbleCorner ( int corner ) { 76 | mDrawable.mBubbleCorner = corner; 77 | return this; 78 | } 79 | 80 | /** 81 | * Set width of the Bubble Funnel. 82 | * @param width target value 83 | * @return current instance 84 | */ 85 | public BubbleBuilder setFunnelWidth ( int width ) { 86 | mDrawable.mFunnelWidth = width; 87 | return this; 88 | } 89 | 90 | /** 91 | * Set relative middle point for the Funnel. 92 | * Value should be between {@code [0, 1]}. 93 | * @param middlePointRelative target value 94 | * @return current instance 95 | */ 96 | public BubbleBuilder setFunnelPointRelative ( float middlePointRelative ) { 97 | mDrawable.mFunnelStartRelative = middlePointRelative; 98 | return this; 99 | } 100 | 101 | /** 102 | * Set gravity of the Bubble Funnel. 103 | * @param gravity target value 104 | * @return current instance 105 | */ 106 | public BubbleBuilder setFunnelGravity ( int gravity ) { 107 | mDrawable.mFunnelGravity = gravity; 108 | return this; 109 | } 110 | 111 | /** 112 | * Set the Bubble Funnel Vector. 113 | * Vector starts at funnel middle point and create an triangle with the funnel width. 114 | * @param x target value for x 115 | * @param y target value for y 116 | * @return current instance 117 | */ 118 | public BubbleBuilder setFunnelVector ( int x, int y ) { 119 | mDrawable.mFunnelVector = new Vector2D(x, y); 120 | return this; 121 | } 122 | 123 | /** 124 | * Finish current configuration. 125 | * @return created or re-configured Drawable 126 | */ 127 | public BubbleDrawable build () { 128 | mDrawable.initPath(); 129 | return mDrawable; 130 | } 131 | } 132 | 133 | //region members 134 | private Vector2D mFunnelVector; 135 | private int mFunnelWidth; 136 | private float mFunnelStartRelative; 137 | private int mFunnelGravity; 138 | 139 | private int mBubbleCorner; 140 | 141 | private Path mBubblePath; 142 | private Paint mBubblePaint; 143 | private Paint mEdgePaint; 144 | //endregion 145 | 146 | /** 147 | * Create a new default instance of an BubbleDrawable. 148 | * Only for private usage. Please use {@code BubbleDrawable.createBubbleBuilder()}. 149 | */ 150 | private BubbleDrawable () { 151 | super(); 152 | mFunnelGravity = Gravity.CENTER; 153 | initPaint(); 154 | initPath(); 155 | } 156 | 157 | //region init 158 | private void initPaint () { 159 | if (mBubblePaint == null) { 160 | mBubblePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 161 | mBubblePaint.setStyle(Paint.Style.FILL); 162 | mBubblePaint.setColor(Color.WHITE); 163 | mBubbleCorner = 6; 164 | } 165 | 166 | if (mEdgePaint == null) { 167 | mEdgePaint = new Paint(Paint.ANTI_ALIAS_FLAG); 168 | mEdgePaint.setStyle(Paint.Style.STROKE); 169 | mEdgePaint.setColor(Color.BLACK); 170 | mEdgePaint.setStrokeWidth(2); 171 | } 172 | } 173 | 174 | private void initPath () { 175 | 176 | mBubblePath = new Path(); 177 | 178 | Rect r = new Rect(getBounds()); 179 | 180 | if (mEdgePaint != null) { 181 | int strokeHalfSize = (int) (mEdgePaint.getStrokeWidth() / 2f); 182 | r.left += strokeHalfSize; 183 | r.top += strokeHalfSize; 184 | r.right -= strokeHalfSize; 185 | r.bottom -= strokeHalfSize; 186 | } 187 | 188 | switch (mFunnelGravity) { 189 | case Gravity.LEFT: 190 | r.left += Math.abs(mFunnelVector.x); 191 | break; 192 | case Gravity.RIGHT: 193 | r.right -= Math.abs(mFunnelVector.x); 194 | break; 195 | case Gravity.TOP: 196 | r.top += Math.abs(mFunnelVector.y); 197 | break; 198 | case Gravity.BOTTOM: 199 | r.bottom -= Math.abs(mFunnelVector.y); 200 | break; 201 | default: 202 | break; 203 | } 204 | 205 | //start 206 | mBubblePath.moveTo(r.left + mBubbleCorner, r.top); 207 | 208 | intersectFunnel(r, Gravity.TOP); 209 | 210 | //top horizontal line. 211 | mBubblePath.lineTo(r.right - mBubbleCorner, r.top); 212 | 213 | //top right arc 214 | int arc = mBubbleCorner * 2; 215 | mBubblePath.arcTo(new RectF(r.right - arc, r.top, r.right, r.top + arc), 270, 90); 216 | 217 | intersectFunnel(r, Gravity.RIGHT); 218 | 219 | //right vertical line. 220 | mBubblePath.lineTo(r.right, r.bottom - mBubbleCorner); 221 | 222 | //bottom right arc. 223 | mBubblePath.arcTo(new RectF(r.right - arc, r.bottom - arc, r.right, r.bottom), 0, 90); 224 | 225 | intersectFunnel(r, Gravity.BOTTOM); 226 | 227 | //bottom horizontal line. 228 | mBubblePath.lineTo(r.left + mBubbleCorner, r.bottom); 229 | 230 | //bottom left arc. 231 | mBubblePath.arcTo(new RectF(r.left, r.bottom - arc, r.left + arc, r.bottom), 90, 90); 232 | 233 | intersectFunnel(r, Gravity.LEFT); 234 | 235 | //left horizontal line. 236 | mBubblePath.lineTo(r.left, r.top + mBubbleCorner); 237 | 238 | //top right arc. 239 | mBubblePath.arcTo(new RectF(r.left, r.top, r.left + arc, r.top + arc), 180, 90); 240 | 241 | mBubblePath.close(); 242 | } 243 | 244 | private void intersectFunnel ( Rect r, int gravity ) { 245 | if (gravity != mFunnelGravity) { 246 | return; 247 | } 248 | 249 | float lineToX; 250 | float lineToY; 251 | float halfFunnelSize = mFunnelWidth / 2f; 252 | 253 | switch (gravity) { 254 | case Gravity.LEFT: 255 | lineToX = r.left; 256 | lineToY = ((r.bottom - (mBubbleCorner * 2)) * mFunnelStartRelative) + halfFunnelSize + mBubbleCorner; 257 | mBubblePath.lineTo(lineToX, lineToY); 258 | 259 | lineToX -= Math.abs(mFunnelVector.x); 260 | lineToY += mFunnelVector.y - halfFunnelSize; 261 | mBubblePath.lineTo(lineToX, lineToY); 262 | 263 | lineToX = r.left; 264 | lineToY = lineToY - mFunnelVector.y - halfFunnelSize; 265 | mBubblePath.lineTo(lineToX, lineToY); 266 | break; 267 | 268 | case Gravity.RIGHT: 269 | lineToX = r.right; 270 | lineToY = ((r.bottom - (mBubbleCorner * 2)) * mFunnelStartRelative) - halfFunnelSize + mBubbleCorner; 271 | mBubblePath.lineTo(lineToX, lineToY); 272 | 273 | lineToX += Math.abs(mFunnelVector.x); 274 | lineToY += mFunnelVector.y + halfFunnelSize; 275 | mBubblePath.lineTo(lineToX, lineToY); 276 | 277 | lineToX = r.right; 278 | lineToY = lineToY - mFunnelVector.y + halfFunnelSize; 279 | mBubblePath.lineTo(lineToX, lineToY); 280 | break; 281 | 282 | case Gravity.TOP: 283 | lineToX = ((r.right - (mBubbleCorner * 2)) * mFunnelStartRelative) - halfFunnelSize + mBubbleCorner; 284 | lineToY = r.top; 285 | mBubblePath.lineTo(lineToX, lineToY); 286 | 287 | lineToX += mFunnelVector.x + halfFunnelSize; 288 | lineToY -= Math.abs(mFunnelVector.y); 289 | mBubblePath.lineTo(lineToX, lineToY); 290 | 291 | lineToX = lineToX - mFunnelVector.x + halfFunnelSize; 292 | lineToY = r.top; 293 | mBubblePath.lineTo(lineToX, lineToY); 294 | break; 295 | 296 | case Gravity.BOTTOM: 297 | lineToX = ((r.right - (mBubbleCorner * 2)) * mFunnelStartRelative) + halfFunnelSize + mBubbleCorner; 298 | lineToY = r.bottom; 299 | mBubblePath.lineTo(lineToX, lineToY); 300 | 301 | lineToX += mFunnelVector.x - halfFunnelSize; 302 | lineToY += Math.abs(mFunnelVector.y); 303 | mBubblePath.lineTo(lineToX, lineToY); 304 | 305 | lineToX = lineToX - mFunnelVector.x - halfFunnelSize; 306 | lineToY = r.bottom; 307 | mBubblePath.lineTo(lineToX, lineToY); 308 | break; 309 | default: 310 | break; 311 | } 312 | } 313 | //endregion 314 | 315 | //region getter 316 | public static BubbleBuilder createBubbleBuilder () { 317 | return new BubbleBuilder(new BubbleDrawable()); 318 | } 319 | 320 | public BubbleBuilder getBubbleBuilder () { 321 | return new BubbleBuilder(this); 322 | } 323 | 324 | public int getBubbleColor () { 325 | initPaint(); 326 | return mBubblePaint.getColor(); 327 | } 328 | 329 | public int getEdgeColor () { 330 | initPaint(); 331 | return mEdgePaint.getColor(); 332 | } 333 | 334 | public float getEdgeThickness () { 335 | initPaint(); 336 | return mEdgePaint.getStrokeWidth(); 337 | } 338 | 339 | public int getBubbleCorner () { 340 | return mBubbleCorner; 341 | } 342 | 343 | public int getFunnelWidth () { 344 | return mFunnelWidth; 345 | } 346 | 347 | public float getFunnelStart () { 348 | return mFunnelStartRelative; 349 | } 350 | 351 | public int getFunnelGravity () { 352 | return mFunnelGravity; 353 | } 354 | 355 | public Vector2D getFunnelVector () { 356 | return mFunnelVector; 357 | } 358 | //endregion 359 | 360 | //region implemented methods 361 | @Override 362 | protected void onBoundsChange ( Rect bounds ) { 363 | super.onBoundsChange(bounds); 364 | initPath(); 365 | } 366 | 367 | @Override 368 | public void setAlpha ( int alpha ) { 369 | initPaint(); 370 | 371 | mBubblePaint.setAlpha(alpha); 372 | mEdgePaint.setAlpha(alpha); 373 | } 374 | 375 | @Override 376 | public void setColorFilter ( ColorFilter cf ) { 377 | initPaint(); 378 | 379 | mBubblePaint.setColorFilter(cf); 380 | mEdgePaint.setColorFilter(cf); 381 | } 382 | 383 | @Override 384 | public int getOpacity () { 385 | initPaint(); 386 | 387 | if (mBubblePaint.getColorFilter() != null || mEdgePaint.getColorFilter() != null) { 388 | // can not define result color 389 | return PixelFormat.TRANSLUCENT; 390 | } 391 | 392 | int alphaBackground = mBubblePaint.getColor() >>> 24; 393 | int alphaEdge = mEdgePaint.getColor() >>> 24; 394 | 395 | if (alphaBackground + alphaEdge == 0) { 396 | // both colors ar transparent 397 | return PixelFormat.TRANSPARENT; 398 | } 399 | 400 | // drawable always draws opaque, translucent and transparent parts 401 | return PixelFormat.TRANSLUCENT; 402 | } 403 | 404 | @Override 405 | public void draw ( @Nonnull Canvas canvas ) { 406 | if (mBubblePath == null || mBubblePaint == null) { 407 | return; 408 | } 409 | 410 | canvas.drawPath(mBubblePath, mBubblePaint); 411 | 412 | if (mEdgePaint != null && mEdgePaint.getStrokeWidth() > 0f) { 413 | canvas.drawPath(mBubblePath, mEdgePaint); 414 | } 415 | } 416 | //endregion 417 | } 418 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/layout/TutorialScreenContainerLayout.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles.layout; 2 | 3 | import android.animation.LayoutTransition; 4 | import android.content.Context; 5 | import android.graphics.Bitmap; 6 | import android.graphics.Canvas; 7 | import android.graphics.Color; 8 | import android.graphics.Paint; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.PorterDuffXfermode; 11 | import android.graphics.Rect; 12 | import android.graphics.drawable.ColorDrawable; 13 | import android.os.Build; 14 | import android.util.AttributeSet; 15 | import android.view.Gravity; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.ViewGroup; 19 | 20 | import com.lovoo.tutorialbubbles.R; 21 | import com.lovoo.tutorialbubbles.TutorialScreen; 22 | import com.lovoo.tutorialbubbles.utils.Utils; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.Comparator; 27 | import java.util.HashMap; 28 | import java.util.Iterator; 29 | import java.util.LinkedHashMap; 30 | import java.util.LinkedList; 31 | import java.util.List; 32 | import java.util.Map; 33 | 34 | import javax.annotation.CheckForNull; 35 | import javax.annotation.Nonnull; 36 | import javax.annotation.Nullable; 37 | 38 | /** 39 | * this ViewGroup serves as container layout for a tutorial layout and supplies this functionality. 40 | * 46 | * 47 | * @author Johannes Braun 48 | */ 49 | public class TutorialScreenContainerLayout extends ViewGroup { 50 | 51 | private static final boolean DEBUG = false; 52 | 53 | private static final int DEFAULT_FUNNEL_WIDTH = 25; 54 | private static final int DEFAULT_FUNNEL_LENGTH = 20; 55 | private static final int DEFAULT_BUBBLE_CORNER_RADIUS = 5; 56 | private static final int DEFAULT_OFFSET_FROM_ANCHOR = 5; 57 | 58 | @CheckForNull 59 | private View mAnchor; 60 | 61 | @Nonnull 62 | private Rect mAnchorBounds; 63 | 64 | private LinkedHashMap mDisplayAreas; 65 | private int mDesiredTutorialScreenWidth; 66 | private int mDesiredTutorialScreenHeight; 67 | private int mHalfTutorialScreenWidth; 68 | private int mHalfTutroialScreenHeight; 69 | 70 | private int mFunnelLength; 71 | private int mFunnelWidth; 72 | 73 | private HashMap mDebugPaints; 74 | private Paint mAnchourDebugPaint; 75 | private Paint mClearPaint; 76 | 77 | private int mOffestFromAnchor; 78 | private int[] mInitialTutorialPadding; 79 | private int mTutorialBackgroundColor; 80 | private int mBubbleCornerRadius; 81 | 82 | private int mDisplayWidth; 83 | private int mDisplayHeight; 84 | private int mStatusbarHeight; 85 | private boolean mIsWindowTranslucent; 86 | private boolean mIsWindowManaged; 87 | 88 | @Nonnull 89 | private ArrayList mHightlightViews; 90 | private ChildPos mChildPos; 91 | 92 | private OnAttachStateChangeListener mAnchorDetachListener; 93 | private boolean mAnchorIsDetached; 94 | 95 | public TutorialScreenContainerLayout ( Context context ) { 96 | this(context, null); 97 | } 98 | 99 | public TutorialScreenContainerLayout ( Context context, AttributeSet attrs ) { 100 | this(context, attrs, 0); 101 | } 102 | 103 | public TutorialScreenContainerLayout ( Context context, AttributeSet attrs, int defStyleAttr ) { 104 | super(context, attrs, defStyleAttr); 105 | ColorDrawable colorDrawable = new ColorDrawable(getResources().getColor(android.R.color.black)); 106 | colorDrawable.setAlpha(127); 107 | if (Build.VERSION.SDK_INT > 15) { 108 | setBackground(colorDrawable); 109 | } else { 110 | setBackgroundDrawable(colorDrawable); 111 | } 112 | 113 | mStatusbarHeight = Utils.getSystemStatusBarHeight(context); 114 | mDisplayWidth = Utils.getDisplayWidth(context); 115 | mDisplayHeight = Utils.getDisplayHeight(context); 116 | mIsWindowTranslucent = Utils.isWindowTranslucent(context); 117 | 118 | mDisplayAreas = new LinkedHashMap<>(); 119 | mAnchorBounds = new Rect(); 120 | 121 | mFunnelLength = Utils.dpToPx(context, DEFAULT_FUNNEL_LENGTH); 122 | mFunnelWidth = Utils.dpToPx(context, DEFAULT_FUNNEL_WIDTH); 123 | mTutorialBackgroundColor = getResources().getColor(R.color.tooltip_background); 124 | mBubbleCornerRadius = Utils.dpToPx(context, DEFAULT_BUBBLE_CORNER_RADIUS); 125 | mOffestFromAnchor = Utils.dpToPx(context, DEFAULT_OFFSET_FROM_ANCHOR); 126 | 127 | mClearPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 128 | mClearPaint.setStyle(Paint.Style.FILL); 129 | mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 130 | 131 | mHightlightViews = new ArrayList<>(); 132 | 133 | if (DEBUG) { 134 | mDebugPaints = new HashMap<>(); 135 | 136 | Paint leftPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 137 | leftPaint.setStyle(Paint.Style.FILL); 138 | leftPaint.setColor(getResources().getColor(R.color.transparentYellow)); 139 | mDebugPaints.put(Gravity.LEFT, leftPaint); 140 | 141 | Paint topPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 142 | topPaint.setStyle(Paint.Style.FILL); 143 | topPaint.setColor(getResources().getColor(R.color.transparentRed)); 144 | mDebugPaints.put(Gravity.TOP, topPaint); 145 | 146 | // Paint rightPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 147 | // rightPaint.setStyle(Paint.Style.FILL); 148 | // rightPaint.setColor(getResources().getColor(R.color.transparentBlue)); 149 | // mDebugPaints.put(Gravity.RIGHT, rightPaint); 150 | 151 | Paint bottomPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 152 | bottomPaint.setStyle(Paint.Style.FILL); 153 | bottomPaint.setColor(getResources().getColor(R.color.transparentGreen)); 154 | mDebugPaints.put(Gravity.BOTTOM, bottomPaint); 155 | 156 | mAnchourDebugPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 157 | mAnchourDebugPaint.setStyle(Paint.Style.FILL); 158 | mAnchourDebugPaint.setColor(getResources().getColor(R.color.notification_bubble_female)); 159 | } 160 | 161 | setLayoutTransition(new LayoutTransition()); 162 | 163 | } 164 | 165 | /** 166 | * inits the view. 167 | * 168 | * @param tutorial an inflated view that will be added to the screen 169 | * @param anchor a anchor view 170 | */ 171 | public void init ( View tutorial, View anchor, TutorialScreenDimension dimensions ) { 172 | this.mAnchor = anchor; 173 | 174 | calcDisplayableAreas(); 175 | 176 | mAnchorDetachListener = new OnAttachStateChangeListener() { 177 | @Override 178 | public void onViewAttachedToWindow ( View v ) { 179 | mAnchorIsDetached = false; 180 | } 181 | 182 | @Override 183 | public void onViewDetachedFromWindow ( View v ) { 184 | // joba: workaround if anchor is an optionsMenuItem 185 | // in this case, every time onCreateOptionsMenu is called, a new view is inflated 186 | // for that menu item, however we still have the old reference. For now we indicate with 187 | // a flag that we lost the current ref in onDetach. In measurement pass we don't update 188 | // the bounds as they should not have changed (except menu items change in runtime within the same fragment) 189 | mAnchorIsDetached = true; 190 | // mAnchor.removeOnAttachStateChangeListener(mAnchorDetachListener); 191 | } 192 | }; 193 | this.mAnchor.addOnAttachStateChangeListener(mAnchorDetachListener); 194 | 195 | this.mIsWindowManaged = dimensions.isWindowManaged; 196 | 197 | mInitialTutorialPadding = new int[]{tutorial.getPaddingLeft(), tutorial.getPaddingTop(), 198 | tutorial.getPaddingRight(), tutorial.getPaddingBottom()}; 199 | 200 | 201 | mDesiredTutorialScreenWidth = dimensions.width; 202 | if (mIsWindowTranslucent || !dimensions.isWindowManaged) { 203 | mDesiredTutorialScreenHeight = dimensions.height; 204 | } else { 205 | mDesiredTutorialScreenHeight = dimensions.height - mStatusbarHeight; 206 | } 207 | 208 | mHalfTutorialScreenWidth = mDesiredTutorialScreenWidth / 2; 209 | mHalfTutroialScreenHeight = mDesiredTutorialScreenHeight / 2; 210 | 211 | 212 | tutorial.setClickable(true); 213 | 214 | 215 | addView(tutorial); 216 | tutorial.setVisibility(INVISIBLE); 217 | } 218 | 219 | /** 220 | * sets width of the funnel (schnippbatz) in px. 221 | * 222 | * @param funnelWidth width in px 223 | */ 224 | public void setFunnelWidth ( int funnelWidth ) { 225 | this.mFunnelWidth = funnelWidth; 226 | } 227 | 228 | /** 229 | * sets the legth of the funnel (schnippbatz) in px. 230 | * 231 | * @param funnelLength length in px 232 | */ 233 | public void setFunnelLength ( Integer funnelLength ) { 234 | this.mFunnelLength = funnelLength; 235 | } 236 | 237 | /** 238 | * sets the color of the tutorial bubble. 239 | * 240 | * @param tutorialBackgroundColor color resource 241 | */ 242 | public void setTutorialBackgroundColor ( int tutorialBackgroundColor ) { 243 | this.mTutorialBackgroundColor = tutorialBackgroundColor; 244 | } 245 | 246 | /** 247 | * sets the highlightviews. 248 | * 249 | * @param hightlightViews collection of views 250 | */ 251 | public void setHighlightViews ( @Nonnull ArrayList hightlightViews ) { 252 | boolean resetCache = false; 253 | boolean resetBackground = false; 254 | View view; 255 | for (TutorialScreen.HighlightView e : hightlightViews) { 256 | view = e.mView; 257 | Rect rect = new Rect(); 258 | view.getGlobalVisibleRect(rect); 259 | if (!mIsWindowTranslucent && mIsWindowManaged) { 260 | rect.offset(0, -Utils.getSystemStatusBarHeight(getContext())); 261 | } 262 | 263 | if (!mIsWindowManaged) { 264 | // adjust global anchor view position for viewgroups that are smaller than the display 265 | rect.offset(-(mDisplayWidth - mDesiredTutorialScreenWidth), -(mDisplayHeight - mDesiredTutorialScreenHeight)); 266 | } 267 | 268 | if (!e.mUseViewBoundsAsMask && (view.getMeasuredHeight() > 0 && view.getMeasuredWidth() > 0)) { 269 | 270 | if (view.getBackground() == null) { 271 | view.setBackgroundColor(Color.WHITE); 272 | resetBackground = true; 273 | } 274 | 275 | if (!view.isDrawingCacheEnabled()) { 276 | view.setDrawingCacheEnabled(true); 277 | resetCache = true; 278 | } 279 | view.buildDrawingCache(); 280 | Bitmap b = view.getDrawingCache(); 281 | if (b != null) { 282 | Bitmap cache = Bitmap.createBitmap(b); 283 | mHightlightViews.add(new HighlightEntry(view, rect, cache, e.mUseViewBoundsAsMask)); 284 | view.destroyDrawingCache(); 285 | } else { 286 | b = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); 287 | Canvas c = new Canvas(b); 288 | view.draw(c); 289 | mHightlightViews.add(new HighlightEntry(view, rect, b, e.mUseViewBoundsAsMask)); 290 | } 291 | } else { 292 | mHightlightViews.add(new HighlightEntry(view, rect, null, e.mUseViewBoundsAsMask)); 293 | } 294 | 295 | if (resetCache) { 296 | view.setDrawingCacheEnabled(false); 297 | resetCache = false; 298 | } 299 | if (resetBackground) { 300 | view.setBackgroundResource(0); 301 | resetBackground = false; 302 | } 303 | } 304 | } 305 | 306 | public void setOffestFromAnchor ( Integer offestFromAnchor ) { 307 | this.mOffestFromAnchor = offestFromAnchor; 308 | } 309 | 310 | @Override 311 | protected void onMeasure ( int widthMeasureSpec, int heightMeasureSpec ) { 312 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 313 | 314 | int childCount = getChildCount(); 315 | if (childCount == 0) { 316 | return; 317 | } 318 | 319 | if (childCount > 1) { 320 | throw new IllegalStateException("this view can only handle one child in layout"); 321 | } 322 | 323 | // 1. calc boxes 324 | calcDisplayableAreas(); 325 | 326 | // 2. measure the popup 327 | View tutorial = getChildAt(0); 328 | 329 | LayoutParams params = tutorial.getLayoutParams(); 330 | int specHeight, specWidth; 331 | 332 | if (params.width > 0) { 333 | specWidth = MeasureSpec.makeMeasureSpec(params.width, MeasureSpec.EXACTLY); 334 | } else { 335 | specWidth = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 336 | } 337 | 338 | if (params.height > 0) { 339 | specHeight = MeasureSpec.makeMeasureSpec(params.height, MeasureSpec.EXACTLY); 340 | } else { 341 | specHeight = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 342 | } 343 | // get how big this view want to be 344 | tutorial.measure(specWidth, specHeight); 345 | 346 | measureTutorialInLargestBox(tutorial); 347 | } 348 | 349 | @Override 350 | protected void onLayout ( boolean changed, int l, int t, int r, int b ) { 351 | View tutorial = getChildAt(0); 352 | 353 | // finally layout tutorial at calculated position 354 | if (mChildPos != null) { 355 | tutorial.layout(mChildPos.left, mChildPos.top, mChildPos.left + tutorial.getMeasuredWidth(), mChildPos.top + tutorial.getMeasuredHeight()); 356 | tutorial.setVisibility(VISIBLE); 357 | } 358 | } 359 | 360 | private void measureTutorialInLargestBox ( @Nonnull View tutorial ) { 361 | int bestGravity = 0; 362 | float bestValue = Float.MAX_VALUE; 363 | 364 | for (Map.Entry entry : mDisplayAreas.entrySet()) { 365 | Rect entryRect = entry.getValue().rect; 366 | float w = Math.abs(tutorial.getMeasuredWidth() / (float) entryRect.width()); 367 | float h = Math.abs(tutorial.getMeasuredHeight() / (float) entryRect.height()); 368 | 369 | if (w + h < bestValue) { 370 | bestValue = w + h; 371 | bestGravity = entry.getKey(); 372 | } 373 | } 374 | 375 | DisplayBox bestFittingBox = mDisplayAreas.get(bestGravity); 376 | if (bestFittingBox != null) { 377 | 378 | BubbleDrawable.BubbleBuilder bubbleBuilder = BubbleDrawable.createBubbleBuilder(); 379 | bubbleBuilder.setBubbleCorner(mBubbleCornerRadius) 380 | .setBubbleColor(mTutorialBackgroundColor) 381 | .setEdgeThickness(0f) 382 | .setFunnelWidth(mFunnelWidth); 383 | 384 | // configure bubble and tutorial views padding 385 | switch (bestFittingBox.gravity) { 386 | case Gravity.LEFT: 387 | bubbleBuilder.setFunnelGravity(Gravity.RIGHT); 388 | bubbleBuilder.setFunnelVector(mFunnelLength, 0); 389 | tutorial.setPadding(mInitialTutorialPadding[0], mInitialTutorialPadding[1], 390 | mInitialTutorialPadding[2] + mFunnelLength, mInitialTutorialPadding[3]); 391 | break; 392 | case Gravity.TOP: 393 | bubbleBuilder.setFunnelGravity(Gravity.BOTTOM); 394 | bubbleBuilder.setFunnelVector(0, mFunnelLength); 395 | tutorial.setPadding(mInitialTutorialPadding[0], mInitialTutorialPadding[1], mInitialTutorialPadding[2], 396 | mInitialTutorialPadding[3] + mFunnelLength); 397 | break; 398 | case Gravity.RIGHT: 399 | bubbleBuilder.setFunnelGravity(Gravity.LEFT); 400 | bubbleBuilder.setFunnelVector(mFunnelLength, 0); 401 | tutorial.setPadding(mInitialTutorialPadding[0] + mFunnelLength, mInitialTutorialPadding[1], 402 | mInitialTutorialPadding[2], mInitialTutorialPadding[3]); 403 | break; 404 | case Gravity.BOTTOM: 405 | bubbleBuilder.setFunnelGravity(Gravity.TOP); 406 | bubbleBuilder.setFunnelVector(0, mFunnelLength); 407 | tutorial.setPadding(mInitialTutorialPadding[0], mInitialTutorialPadding[1] + mFunnelLength, 408 | mInitialTutorialPadding[2], mInitialTutorialPadding[3]); 409 | break; 410 | default: 411 | } 412 | 413 | tutorial.measure(MeasureSpec.makeMeasureSpec(bestFittingBox.rect.width(), MeasureSpec.AT_MOST), 414 | MeasureSpec.makeMeasureSpec(bestFittingBox.rect.height(), MeasureSpec.AT_MOST)); 415 | 416 | // calculate position within the display box according to anchor 417 | mChildPos = calcInnerBoxPosition(bestFittingBox, tutorial); 418 | 419 | float funnelPosition = calcFunnelPosition(bestFittingBox.gravity, mChildPos, tutorial); 420 | bubbleBuilder.setFunnelPointRelative(funnelPosition); 421 | BubbleDrawable drawable = bubbleBuilder.build(); 422 | 423 | if (Build.VERSION.SDK_INT < 16) { 424 | tutorial.setBackgroundDrawable(drawable); 425 | } else { 426 | tutorial.setBackground(drawable); 427 | } 428 | } 429 | } 430 | 431 | private float calcFunnelPosition ( int gravity, ChildPos childPos, View tutorial ) { 432 | float relativePos = 0.5f; 433 | 434 | switch (gravity) { 435 | case Gravity.TOP: 436 | case Gravity.BOTTOM: 437 | if (mAnchorBounds.width() < tutorial.getMeasuredWidth()) { 438 | relativePos = (mAnchorBounds.exactCenterX() - childPos.left) / (tutorial.getMeasuredWidth() - mBubbleCornerRadius); 439 | 440 | float funnelOverflow = (tutorial.getMeasuredWidth() * relativePos) + (mFunnelWidth / 2); 441 | if (funnelOverflow > tutorial.getMeasuredWidth()) { 442 | relativePos = relativePos - (funnelOverflow / tutorial.getMeasuredWidth() - 1); 443 | } 444 | } 445 | break; 446 | case Gravity.LEFT: 447 | case Gravity.RIGHT: 448 | if (mAnchorBounds.height() < tutorial.getMeasuredHeight()) { 449 | relativePos = (mAnchorBounds.exactCenterY() - childPos.top) / (tutorial.getMeasuredHeight() - mBubbleCornerRadius); 450 | 451 | float funnelOverflow = (tutorial.getMeasuredHeight() * relativePos) + (mFunnelWidth / 2); 452 | if (funnelOverflow > tutorial.getMeasuredHeight()) { 453 | relativePos = relativePos - (funnelOverflow / tutorial.getMeasuredHeight() - 1); 454 | } 455 | } 456 | break; 457 | default: 458 | } 459 | return relativePos; 460 | } 461 | 462 | private ChildPos calcInnerBoxPosition ( @Nonnull DisplayBox displayBox, @Nonnull View tutorial ) { 463 | ChildPos childPos = new ChildPos(displayBox.rect.left, displayBox.rect.top); 464 | 465 | switch (displayBox.gravity) { 466 | case Gravity.LEFT: 467 | if (mAnchorBounds.bottom < mHalfTutroialScreenHeight) { 468 | // top 469 | childPos.top = displayBox.rect.top; 470 | if (childPos.top + tutorial.getMeasuredHeight() < mAnchorBounds.bottom) { 471 | childPos.top = mAnchorBounds.bottom - tutorial.getMeasuredHeight(); 472 | } 473 | } else if (mAnchorBounds.top > mHalfTutroialScreenHeight) { 474 | // bottom 475 | childPos.top = displayBox.rect.bottom - tutorial.getMeasuredHeight(); 476 | if (childPos.top > mAnchorBounds.top) { 477 | childPos.top = mAnchorBounds.top; 478 | } 479 | } else { 480 | // center 481 | childPos.top = mHalfTutroialScreenHeight - tutorial.getMeasuredHeight() / 2; 482 | } 483 | childPos.left = displayBox.rect.right - tutorial.getMeasuredWidth(); 484 | break; 485 | case Gravity.TOP: 486 | if (mAnchorBounds.right < mHalfTutorialScreenWidth) { 487 | // left 488 | childPos.left = displayBox.rect.left; 489 | if (childPos.left + tutorial.getMeasuredWidth() < mAnchorBounds.right) { 490 | childPos.left = mAnchorBounds.right - tutorial.getMeasuredWidth(); 491 | } 492 | } else if (mAnchorBounds.left > mHalfTutorialScreenWidth) { 493 | // right 494 | childPos.left = displayBox.rect.right - tutorial.getMeasuredWidth(); 495 | if (childPos.left > mAnchorBounds.left) { 496 | childPos.left = mAnchorBounds.left; 497 | } 498 | } else { 499 | // center 500 | childPos.left = mHalfTutorialScreenWidth - tutorial.getMeasuredWidth() / 2; 501 | } 502 | childPos.top = displayBox.rect.bottom - tutorial.getMeasuredHeight(); 503 | break; 504 | case Gravity.RIGHT: 505 | if (mAnchorBounds.bottom < mHalfTutroialScreenHeight) { 506 | // top 507 | childPos.top = displayBox.rect.top; 508 | if (childPos.top + tutorial.getMeasuredHeight() < mAnchorBounds.bottom) { 509 | childPos.top = mAnchorBounds.bottom - tutorial.getMeasuredHeight(); 510 | } 511 | } else if (mAnchorBounds.top > mHalfTutroialScreenHeight) { 512 | // bottom 513 | childPos.top = displayBox.rect.bottom - tutorial.getMeasuredHeight(); 514 | if (childPos.top > mAnchorBounds.top) { 515 | childPos.top = mAnchorBounds.top; 516 | } 517 | } else { 518 | // center 519 | childPos.top = mHalfTutroialScreenHeight - tutorial.getMeasuredHeight() / 2; 520 | } 521 | break; 522 | case Gravity.BOTTOM: 523 | if (mAnchorBounds.right < mHalfTutorialScreenWidth) { 524 | // left 525 | childPos.left = displayBox.rect.left; 526 | if (childPos.left + tutorial.getMeasuredWidth() < mAnchorBounds.right) { 527 | childPos.left = mAnchorBounds.right - tutorial.getMeasuredWidth(); 528 | } 529 | } else if (mAnchorBounds.left > mHalfTutorialScreenWidth) { 530 | // right 531 | childPos.left = displayBox.rect.right - tutorial.getMeasuredWidth(); 532 | if (childPos.left > mAnchorBounds.left) { 533 | childPos.left = mAnchorBounds.left; 534 | } 535 | } else { 536 | // center 537 | childPos.left = mHalfTutorialScreenWidth - tutorial.getMeasuredWidth() / 2; 538 | } 539 | break; 540 | default: 541 | } 542 | 543 | return childPos; 544 | } 545 | 546 | private void calcDisplayableAreas () { 547 | if (mAnchor == null || mAnchorIsDetached) { 548 | return; 549 | } 550 | 551 | mAnchor.getGlobalVisibleRect(mAnchorBounds); 552 | 553 | if (!mIsWindowManaged) { 554 | mAnchorBounds.offset(-(mDisplayWidth - mDesiredTutorialScreenWidth), -(mDisplayHeight - mDesiredTutorialScreenHeight)); 555 | } 556 | 557 | if (!mIsWindowTranslucent && mIsWindowManaged) { 558 | mAnchorBounds.offset(0, -mStatusbarHeight); 559 | } 560 | 561 | // left 562 | Rect left = new Rect(); 563 | left.left = getPaddingLeft(); 564 | left.top = getPaddingTop(); 565 | left.right = mAnchorBounds.left - mOffestFromAnchor; 566 | left.bottom = mDesiredTutorialScreenHeight - getPaddingBottom(); 567 | mDisplayAreas.put(Gravity.LEFT, new DisplayBox(left, Gravity.LEFT)); 568 | 569 | // top 570 | Rect top = new Rect(); 571 | top.left = getPaddingLeft(); 572 | top.top = getPaddingTop(); 573 | top.right = mDesiredTutorialScreenWidth - getPaddingRight(); 574 | top.bottom = mAnchorBounds.top - mOffestFromAnchor; 575 | mDisplayAreas.put(Gravity.TOP, new DisplayBox(top, Gravity.TOP)); 576 | 577 | // right 578 | Rect right = new Rect(); 579 | right.left = mAnchorBounds.right + mOffestFromAnchor; 580 | right.top = getPaddingTop(); 581 | right.right = mDesiredTutorialScreenWidth - getPaddingRight(); 582 | right.bottom = mDesiredTutorialScreenHeight - getPaddingBottom(); 583 | mDisplayAreas.put(Gravity.RIGHT, new DisplayBox(right, Gravity.RIGHT)); 584 | 585 | // botton 586 | Rect bottom = new Rect(); 587 | bottom.left = getPaddingLeft(); 588 | bottom.top = mAnchorBounds.bottom + mOffestFromAnchor; 589 | bottom.right = mDesiredTutorialScreenWidth - getPaddingRight(); 590 | bottom.bottom = mDesiredTutorialScreenHeight - getPaddingBottom(); 591 | mDisplayAreas.put(Gravity.BOTTOM, new DisplayBox(bottom, Gravity.BOTTOM)); 592 | 593 | mDisplayAreas = (LinkedHashMap) sortByValue(mDisplayAreas); 594 | 595 | } 596 | 597 | @Override 598 | protected void onDraw ( Canvas canvas ) { 599 | super.onDraw(canvas); 600 | 601 | for (HighlightEntry entry : mHightlightViews) { 602 | if (entry.useBoundsAsmask && entry.rect != null) { 603 | canvas.drawRect(entry.rect, mClearPaint); 604 | } else if (entry.drawingCache != null && entry.rect != null) { 605 | canvas.drawBitmap(entry.drawingCache, null, entry.rect, mClearPaint); 606 | } 607 | } 608 | 609 | if (DEBUG) { 610 | // display the displayable areas as colored boxes while debugging 611 | for (Map.Entry paints : mDebugPaints.entrySet()) { 612 | DisplayBox box = mDisplayAreas.get(paints.getKey()); 613 | if (box != null) { 614 | canvas.drawRect(box.rect, paints.getValue()); 615 | } 616 | } 617 | 618 | if (mAnchourDebugPaint != null && mAnchorBounds != null) { 619 | canvas.drawRect(mAnchorBounds, mAnchourDebugPaint); 620 | } 621 | 622 | canvas.drawRect(0, 0, 30, 30, mAnchourDebugPaint); 623 | } 624 | } 625 | 626 | @Override 627 | public boolean onInterceptTouchEvent ( MotionEvent ev ) { 628 | if (ev.getAction() == MotionEvent.ACTION_DOWN) { 629 | 630 | for (HighlightEntry entry : mHightlightViews) { 631 | if (entry.rect.contains((int) ev.getX(), (int) ev.getY())) { 632 | entry.view.performClick(); 633 | } 634 | } 635 | } 636 | return super.onInterceptTouchEvent(ev); 637 | } 638 | 639 | 640 | private Map sortByValue ( Map unsortMap ) { 641 | List list = new LinkedList(unsortMap.entrySet()); 642 | 643 | Collections.sort(list, new Comparator() { 644 | public int compare ( Object o1, Object o2 ) { 645 | return ((Comparable) ((Map.Entry) (o1)).getValue()) 646 | .compareTo(((Map.Entry) (o2)).getValue()); 647 | } 648 | }); 649 | 650 | Map sortedMap = new LinkedHashMap(); 651 | for (Iterator it = list.iterator(); it.hasNext(); ) { 652 | Map.Entry entry = (Map.Entry) it.next(); 653 | sortedMap.put(entry.getKey(), entry.getValue()); 654 | } 655 | 656 | return sortedMap; 657 | } 658 | 659 | 660 | /** 661 | * an quadrant area inside the layout based upon an anchor. 662 | */ 663 | private static class DisplayBox implements Comparable { 664 | @Nonnull 665 | Rect rect; 666 | int gravity; 667 | int area; 668 | 669 | public DisplayBox ( @Nonnull Rect rect, int gravity ) { 670 | this.rect = rect; 671 | this.area = rect.width() * rect.height(); 672 | this.gravity = gravity; 673 | } 674 | 675 | @Override 676 | public int compareTo ( DisplayBox another ) { 677 | return another.area - this.area; 678 | } 679 | 680 | @Override 681 | public String toString () { 682 | return rect + "area: " + area; 683 | } 684 | } 685 | 686 | //region inner classes 687 | private static class ChildPos { 688 | int left; 689 | int top; 690 | 691 | public ChildPos ( int left, int top ) { 692 | this.left = left; 693 | this.top = top; 694 | } 695 | } 696 | 697 | private static class HighlightEntry { 698 | Rect rect; 699 | Bitmap drawingCache; 700 | View view; 701 | boolean useBoundsAsmask; 702 | 703 | public HighlightEntry ( @Nonnull View view, @Nonnull Rect rect, @Nullable Bitmap cache, boolean useBoundsAsmask ) { 704 | this.view = view; 705 | this.useBoundsAsmask = useBoundsAsmask; 706 | this.rect = rect; 707 | this.drawingCache = cache; 708 | } 709 | } 710 | 711 | public static class TutorialScreenDimension { 712 | public final int width; 713 | public final int height; 714 | public final boolean isWindowManaged; 715 | 716 | /** 717 | * constructs dimensions class. 718 | * 719 | * @param mWidth the width, the overlaying layout should have 720 | * @param mHeight the height, the overlaying layout should have 721 | * @param isWindowManaged flag that shows whether the layout is windowManaged or not 722 | */ 723 | public TutorialScreenDimension ( int mWidth, int mHeight, boolean isWindowManaged ) { 724 | this.width = mWidth; 725 | this.height = mHeight; 726 | this.isWindowManaged = isWindowManaged; 727 | } 728 | } 729 | //endregion 730 | } 731 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/utils/Utils.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles.utils; 2 | 3 | import android.annotation.TargetApi; 4 | import android.app.Activity; 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.util.DisplayMetrics; 8 | import android.util.TypedValue; 9 | import android.view.Window; 10 | import android.view.WindowManager; 11 | 12 | /** 13 | * @author Johannes Braun 14 | */ 15 | public class Utils { 16 | 17 | private static int mScreenWidth = -1; 18 | private static int mScreenHeight = -1; 19 | 20 | public static int dpToPx ( Context context, int dp ) { 21 | return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); 22 | } 23 | 24 | public static int getSystemStatusBarHeight ( Context context ) { 25 | int result = 0; 26 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 27 | if (resourceId > 0) { 28 | result = context.getResources().getDimensionPixelSize(resourceId); 29 | } 30 | return result; 31 | } 32 | 33 | public static int getDisplayWidth ( Context context ) { 34 | if (mScreenWidth == -1) { 35 | DisplayMetrics displayMetrics = context.getResources() 36 | .getDisplayMetrics(); 37 | mScreenWidth = displayMetrics.widthPixels; 38 | } 39 | return mScreenWidth; 40 | } 41 | 42 | public static int getDisplayHeight ( Context context ) { 43 | if (mScreenHeight == -1) { 44 | DisplayMetrics displayMetrics = context.getResources() 45 | .getDisplayMetrics(); 46 | mScreenHeight = displayMetrics.heightPixels; 47 | } 48 | return mScreenHeight; 49 | } 50 | 51 | /** 52 | * Determine whether the current context has translucent mode (KITKAT and above). 53 | * @param context should be some context deriving from activity, NO application context 54 | * @return true if API-Level is KITKAT and above and translucent flag is enabled, false in any other cases 55 | */ 56 | @TargetApi(19) 57 | public static boolean isWindowTranslucent( Context context ){ 58 | if(Build.VERSION.SDK_INT < 19){ 59 | return false; 60 | } 61 | 62 | if(context == null || !(context instanceof Activity)){ 63 | return false; 64 | } 65 | 66 | Window window = ((Activity) context).getWindow(); 67 | if(window == null){ 68 | return false; 69 | } 70 | 71 | return (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) == WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/java/com/lovoo/tutorialbubbles/utils/Vector2D.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbles.utils; 2 | 3 | import android.graphics.Matrix; 4 | 5 | public class Vector2D { 6 | 7 | public float x; 8 | public float y; 9 | 10 | public Vector2D () { 11 | this.x = 0; 12 | this.y = 0; 13 | } 14 | 15 | public Vector2D ( Vector2D v ) { 16 | if (v == null) { 17 | v = new Vector2D(); 18 | } 19 | this.x = v.x; 20 | this.y = v.y; 21 | } 22 | 23 | public Vector2D ( float[] pos ) { 24 | if (pos == null || pos.length != 2) { 25 | this.x = 0; 26 | this.y = 0; 27 | } else { 28 | this.x = pos[0]; 29 | this.y = pos[1]; 30 | } 31 | } 32 | 33 | public Vector2D ( float x, float y ) { 34 | this.x = x; 35 | this.y = y; 36 | } 37 | 38 | public Vector2D set ( Vector2D other ) { 39 | if (other != null) { 40 | x = other.x; 41 | y = other.y; 42 | } 43 | return this; 44 | } 45 | 46 | public Vector2D set ( float x, float y ) { 47 | this.x = x; 48 | this.y = y; 49 | return this; 50 | } 51 | 52 | public float[] getPos () { 53 | float[] f = new float[2]; 54 | f[0] = x; 55 | f[1] = y; 56 | return f; 57 | } 58 | 59 | public float getLength () { 60 | if (x == 0f || y == 0f || x + y == 0f) { 61 | return 0f; 62 | } 63 | return (float) Math.sqrt(x * x + y * y); 64 | } 65 | 66 | public boolean isNullVector () { 67 | return (getLength() == 0f); 68 | } 69 | 70 | public void add ( Vector2D v ) { 71 | if (v != null) { 72 | x += v.x; 73 | y += v.y; 74 | } 75 | } 76 | 77 | public void subtract ( Vector2D v ) { 78 | if (v != null) { 79 | x -= v.x; 80 | y -= v.y; 81 | } 82 | } 83 | 84 | public void multiple ( Vector2D v ) { 85 | if (v != null) { 86 | x *= v.x; 87 | y *= v.y; 88 | } 89 | } 90 | 91 | public void multipleWith ( float value ) { 92 | x *= value; 93 | y *= value; 94 | } 95 | 96 | public void transform ( Matrix m ) { 97 | Vector2D.transform(this, this, m); 98 | } 99 | 100 | public void transform ( Vector2D src, Matrix m ) { 101 | Vector2D.transform(src, this, m); 102 | } 103 | 104 | public static void transform ( Vector2D src, Vector2D dst, Matrix m ) { 105 | if (src == null || dst == null || m == null) { 106 | return; 107 | } 108 | float[] p = new float[2]; 109 | p[0] = src.x; 110 | p[1] = src.y; 111 | m.mapPoints(p); 112 | dst.set(p[0], p[1]); 113 | } 114 | 115 | public static Vector2D add ( Vector2D lhs, Vector2D rhs ) { 116 | if (lhs == null || rhs == null) { 117 | return null; 118 | } 119 | return new Vector2D(lhs.x + rhs.x, lhs.y + rhs.y); 120 | } 121 | 122 | public static Vector2D subtract ( Vector2D lhs, Vector2D rhs ) { 123 | if (lhs == null || rhs == null) { 124 | return null; 125 | } 126 | return new Vector2D(lhs.x - rhs.x, lhs.y - rhs.y); 127 | } 128 | 129 | public static float getDistance ( Vector2D lhs, Vector2D rhs ) { 130 | if (lhs == null || rhs == null) { 131 | return -1f; 132 | } 133 | Vector2D delta = Vector2D.subtract(lhs, rhs); 134 | return delta.getLength(); 135 | } 136 | 137 | public static float getSignedAngleBetween ( Vector2D a, Vector2D b ) { 138 | if (a == null || b == null) { 139 | return 0f; 140 | } 141 | Vector2D na = getNormalized(a); 142 | Vector2D nb = getNormalized(b); 143 | 144 | return (float) (Math.atan2(nb.y, nb.x) - Math.atan2(na.y, na.x)); 145 | } 146 | 147 | public static boolean isNullVector ( Vector2D v ) { 148 | return (v == null || v.getLength() == 0f); 149 | } 150 | 151 | public static Vector2D getNormalized ( Vector2D v ) { 152 | if (v == null) { 153 | return null; 154 | } 155 | float l = v.getLength(); 156 | if (l == 0) { 157 | return new Vector2D(); 158 | } else { 159 | return new Vector2D(v.x / l, v.y / l); 160 | } 161 | 162 | } 163 | 164 | public static boolean pointInLine ( Vector2D d, Vector2D a, Vector2D b ) { 165 | if (((d.x - a.x) * (b.y - a.y) - (d.y - a.y) * (b.x - a.x)) > 0) { 166 | // LogUtils.log("Vector2D", String.format("Schnitt Punkt: %s positiv mit Linie %s - %s", d.toShortString(), a.toShortString(), b.toShortString())); 167 | return true; 168 | } 169 | // LogUtils.log("Vector2D", String.format("Schnitt Punkt: %s negativ mit Linie %s - %s", d.toShortString(), a.toShortString(), b.toShortString())); 170 | return false; 171 | } 172 | 173 | @Override 174 | public String toString () { 175 | return String.format("(%.4f, %.4f)", x, y); 176 | } 177 | 178 | public String toShortString () { 179 | return String.format("(%.0f, %.0f)", x, y); 180 | } 181 | 182 | } 183 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #edf6f6f6 5 | #32aadc 6 | #e63296 7 | #aaffff00 8 | #aaff1b2e 9 | #aa1d74ff 10 | #aa30ff24 11 | -------------------------------------------------------------------------------- /androidTutorialBubbles/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TutorialBubbles 3 | 4 | -------------------------------------------------------------------------------- /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 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.3.0' 9 | classpath 'com.novoda:bintray-release:0.3.4' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /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/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Oct 26 17:34:24 CET 2015 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-2.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 | -------------------------------------------------------------------------------- /screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/screen1.png -------------------------------------------------------------------------------- /screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/screen2.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':tutorialsDemoApp' 2 | include ':androidTutorialBubbles' 3 | -------------------------------------------------------------------------------- /tutorialsDemoApp/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tutorialsDemoApp/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "23.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.lovoo.tutorialbubbledemo" 9 | minSdkVersion 14 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile project(':androidTutorialBubbles') 25 | testCompile 'junit:junit:4.12' 26 | compile 'com.android.support:appcompat-v7:23.1.0' 27 | compile 'com.android.support:design:23.1.0' 28 | } 29 | -------------------------------------------------------------------------------- /tutorialsDemoApp/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 /usr/local/opt/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 | -------------------------------------------------------------------------------- /tutorialsDemoApp/src/androidTest/java/com/lovoo/tutorialbubbledemo/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbledemo; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest () { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /tutorialsDemoApp/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 9 | 10 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tutorialsDemoApp/src/main/java/com/lovoo/tutorialbubbledemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.lovoo.tutorialbubbledemo; 2 | 3 | import android.os.Bundle; 4 | import android.support.design.widget.FloatingActionButton; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.support.v7.widget.Toolbar; 7 | import android.view.Menu; 8 | import android.view.MenuItem; 9 | import android.view.View; 10 | import android.widget.Button; 11 | import android.widget.Toast; 12 | 13 | import com.lovoo.tutorialbubbles.TutorialScreen; 14 | import com.lovoo.tutorialbubbles.utils.Utils; 15 | 16 | /** 17 | * Demo activity that shows the use of the tutorial bubbles 18 | */ 19 | public class MainActivity extends AppCompatActivity { 20 | 21 | private TutorialScreen buttonTutorial; 22 | private TutorialScreen fabButtonTutorial; 23 | 24 | @Override 25 | protected void onCreate ( Bundle savedInstanceState ) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_main); 28 | Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); 29 | setSupportActionBar(toolbar); 30 | 31 | final Button explainButton = (Button) findViewById(R.id.explain_button); 32 | explainButton.setOnClickListener(new View.OnClickListener() { 33 | @Override 34 | public void onClick ( View v ) { 35 | // call this to dispplay your tutorial 36 | buttonTutorial.showTutorial(); 37 | } 38 | }); 39 | 40 | explainButton.post(new Runnable() { 41 | @Override 42 | public void run () { 43 | buttonTutorial = new TutorialScreen.TutorialBuilder(R.layout.button_tutorial_layout, explainButton) 44 | .setParentLayout(getWindow().getDecorView()) // parent layout is necessary for layout approach, use decorView or a root relative layout 45 | .setDismissible(true) // set if this bubble can be dismissed by clicking somewhere outside of its context 46 | .addHighlightView(explainButton, false) // sets the view that should be explained 47 | .setOnTutorialLayoutInflatedListener(new TutorialScreen.OnTutorialLayoutInflatedListener() { 48 | // you can use this callback to bind the bubble layout and apply logic to it 49 | @Override 50 | public void onLayoutInflated ( View view ) { 51 | // put code here for tutorial 52 | view.findViewById(R.id.tutorial_inner_button).setOnClickListener(new View.OnClickListener() { 53 | @Override 54 | public void onClick ( View v ) { 55 | Toast.makeText(MainActivity.this, "Button in bubble clicked.", Toast.LENGTH_SHORT).show(); 56 | } 57 | }); 58 | } 59 | }) 60 | .build(); 61 | 62 | } 63 | }); 64 | 65 | // another example how to further customize the bubble 66 | final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); 67 | fab.post(new Runnable() { 68 | @Override 69 | public void run () { 70 | fabButtonTutorial = new TutorialScreen.TutorialBuilder(R.layout.fab_tutorial_layout, fab) 71 | .setParentLayout(getWindow().getDecorView()) 72 | .addHighlightView(fab, false) 73 | .setTutorialBackgroundColor(getResources().getColor(R.color.transparentRed)) // set another bubble color 74 | .setFunnelLength(Utils.dpToPx(getApplicationContext(), 35)) // changes the length of the bubble funnel 75 | .setFunnelWidth(Utils.dpToPx(getApplicationContext(), 30)) // changes the width of the bubble funnel 76 | .setTutorialOffsetFromAnchor(Utils.dpToPx(getApplicationContext(), 8)) // sets the distance between anchor and bubble 77 | .setOnTutorialLayoutInflatedListener(new TutorialScreen.OnTutorialLayoutInflatedListener() { 78 | @Override 79 | public void onLayoutInflated ( View view ) { 80 | view.findViewById(R.id.tutorial_inner_button).setOnClickListener(new View.OnClickListener() { 81 | @Override 82 | public void onClick ( View v ) { 83 | fabButtonTutorial.dismissTutorial(); 84 | } 85 | }); 86 | } 87 | }) 88 | .build(); 89 | } 90 | }); 91 | 92 | fab.setOnClickListener(new View.OnClickListener() { 93 | @Override 94 | public void onClick ( View view ) { 95 | fabButtonTutorial.showTutorial(); 96 | } 97 | }); 98 | } 99 | 100 | @Override 101 | public boolean onCreateOptionsMenu ( Menu menu ) { 102 | // Inflate the menu; this adds items to the action bar if it is present. 103 | getMenuInflater().inflate(R.menu.menu_main, menu); 104 | return true; 105 | } 106 | 107 | @Override 108 | public boolean onOptionsItemSelected ( MenuItem item ) { 109 | // Handle action bar item clicks here. The action bar will 110 | // automatically handle clicks on the Home/Up button, so long 111 | // as you specify a parent activity in AndroidManifest.xml. 112 | int id = item.getItemId(); 113 | 114 | //noinspection SimplifiableIfStatement 115 | if (id == R.id.action_settings) { 116 | return true; 117 | } 118 | 119 | return super.onOptionsItemSelected(item); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /tutorialsDemoApp/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | 27 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tutorialsDemoApp/src/main/res/layout/button_tutorial_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 |