├── .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 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 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 |
257 |
258 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 [](https://travis-ci.org/Lovoo/android-tutorial-bubbles) [ ](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 | *
22 | * - uses {@link WindowManager} if SYSTEM_ALERT_WINDOW permission is set
23 | * - uses layout hierarchy if {@link TutorialBuilder#setParentLayout} is set with a proper viewgroup
24 | *
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 | *
41 | * - positioning tutorial layout within its container
42 | * - dimming background
43 | * - delegating touch events
44 | * - shows highlight views by not dimming an area where an underlying view is
45 | *
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 |
21 |
22 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
22 |
23 |
31 |
32 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/layout/fab_tutorial_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
22 |
23 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/tutorialsDemoApp/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/tutorialsDemoApp/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/tutorialsDemoApp/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/tutorialsDemoApp/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lovoo/android-tutorial-bubbles/3bc18b022e184915685e10a5dd1fa0128c75c3d0/tutorialsDemoApp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 | >
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Tutorial Bubble Demo
3 | Settings
4 |
5 | This example shows how a tutorial bubble can be used by clicking on one of the following buttons. This example draws the tutorials within the view hierarchy.
6 | Click to explain me
7 | You can layout the content of the bubble with normal layout xml files. This bubble is dismissable by clicking somewhere outside the bubble.
8 | Another example how you may customize your tutorial
9 |
10 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/tutorialsDemoApp/src/test/java/com/lovoo/tutorialbubbledemo/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.lovoo.tutorialbubbledemo;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * To work on unit tests, switch the Test Artifact in the Build Variants view.
9 | */
10 | public class ExampleUnitTest {
11 | @Test
12 | public void addition_isCorrect () throws Exception {
13 | assertEquals(4, 2 + 2);
14 | }
15 | }
--------------------------------------------------------------------------------