├── settings.gradle
├── taptargetview
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── getkeepsafe
│ │ └── taptargetview
│ │ ├── ReflectUtil.java
│ │ ├── ViewTapTarget.java
│ │ ├── UiUtil.java
│ │ ├── ViewUtil.java
│ │ ├── FloatValueAnimatorBuilder.java
│ │ ├── TapTargetSequence.java
│ │ ├── ToolbarTapTarget.java
│ │ ├── TapTarget.java
│ │ └── TapTargetView.java
└── build.gradle
├── .github
├── video.gif
├── screenshot1.png
├── screenshot2.png
├── ISSUE_TEMPLATE.md
└── CONTRIBUTING.md
├── .gitignore
├── app
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ └── styles.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-w820dp
│ │ │ └── dimens.xml
│ │ ├── drawable
│ │ │ ├── ic_arrow_back_white_24dp.xml
│ │ │ ├── ic_more_vert_black_24dp.xml
│ │ │ ├── ic_search_white_24dp.xml
│ │ │ ├── ic_directions_car_black_24dp.xml
│ │ │ └── ic_android_black_24dp.xml
│ │ ├── menu
│ │ │ └── menu_main.xml
│ │ └── layout
│ │ │ └── activity_main.xml
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── getkeepsafe
│ │ └── taptargetviewsample
│ │ └── MainActivity.java
├── build.gradle
└── proguard-rules.pro
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':taptargetview'
2 |
--------------------------------------------------------------------------------
/taptargetview/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.getkeepsafe.taptargetview; 17 | 18 | import java.lang.reflect.Field; 19 | 20 | class ReflectUtil { 21 | ReflectUtil() { 22 | } 23 | 24 | /** Returns the value of the given private field from the source object **/ 25 | static Object getPrivateField(Object source, String fieldName) 26 | throws NoSuchFieldException, IllegalAccessException { 27 | final Field objectField = source.getClass().getDeclaredField(fieldName); 28 | objectField.setAccessible(true); 29 | return objectField.get(source); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/ViewTapTarget.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.getkeepsafe.taptargetview; 17 | 18 | import android.graphics.Bitmap; 19 | import android.graphics.Canvas; 20 | import android.graphics.Rect; 21 | import android.graphics.drawable.BitmapDrawable; 22 | import android.support.annotation.Nullable; 23 | import android.view.View; 24 | 25 | class ViewTapTarget extends TapTarget { 26 | final View view; 27 | 28 | ViewTapTarget(View view, CharSequence title, @Nullable CharSequence description) { 29 | super(title, description); 30 | if (view == null) { 31 | throw new IllegalArgumentException("Given null view to target"); 32 | } 33 | this.view = view; 34 | } 35 | 36 | @Override 37 | public void onReady(final Runnable runnable) { 38 | ViewUtil.onLaidOut(view, new Runnable() { 39 | @Override 40 | public void run() { 41 | // Cache bounds 42 | final int[] location = new int[2]; 43 | view.getLocationOnScreen(location); 44 | bounds = new Rect(location[0], location[1], 45 | location[0] + view.getWidth(), location[1] + view.getHeight()); 46 | 47 | if (icon == null && view.getWidth() > 0 && view.getHeight() > 0) { 48 | final Bitmap viewBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); 49 | final Canvas canvas = new Canvas(viewBitmap); 50 | view.draw(canvas); 51 | icon = new BitmapDrawable(view.getContext().getResources(), viewBitmap); 52 | icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); 53 | } 54 | 55 | runnable.run(); 56 | } 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/UiUtil.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.getkeepsafe.taptargetview;
17 |
18 | import android.content.Context;
19 | import android.content.res.Resources;
20 | import android.os.Build;
21 | import android.support.annotation.ColorRes;
22 | import android.support.annotation.DimenRes;
23 | import android.util.TypedValue;
24 |
25 | class UiUtil {
26 | UiUtil() {
27 | }
28 |
29 | /** Returns the given pixel value in dp **/
30 | static int dp(Context context, int val) {
31 | return (int) TypedValue.applyDimension(
32 | TypedValue.COMPLEX_UNIT_DIP, val, context.getResources().getDisplayMetrics());
33 | }
34 |
35 | /** Returns the given pixel value in sp **/
36 | static int sp(Context context, int val) {
37 | return (int) TypedValue.applyDimension(
38 | TypedValue.COMPLEX_UNIT_SP, val, context.getResources().getDisplayMetrics());
39 | }
40 |
41 | /** Returns the value of the desired theme integer attribute, or -1 if not found **/
42 | static int themeIntAttr(Context context, String attr) {
43 | final Resources.Theme theme = context.getTheme();
44 | if (theme == null) {
45 | return -1;
46 | }
47 |
48 | final TypedValue value = new TypedValue();
49 | final int id = context.getResources().getIdentifier(attr, "attr", context.getPackageName());
50 |
51 | if (id == 0) {
52 | // Not found
53 | return -1;
54 | }
55 |
56 | theme.resolveAttribute(id, value, true);
57 | return value.data;
58 | }
59 |
60 | /** Modifies the alpha value of the given ARGB color **/
61 | static int setAlpha(int argb, float alpha) {
62 | if (alpha > 1.0f) {
63 | alpha = 1.0f;
64 | } else if (alpha <= 0.0f) {
65 | alpha = 0.0f;
66 | }
67 |
68 | return ((int) ((argb >>> 24) * alpha) << 24) | (argb & 0x00FFFFFF);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.getkeepsafe.taptargetview; 17 | 18 | import android.os.Build; 19 | import android.support.v4.view.ViewCompat; 20 | import android.view.View; 21 | import android.view.ViewManager; 22 | import android.view.ViewTreeObserver; 23 | 24 | class ViewUtil { 25 | ViewUtil() { 26 | } 27 | 28 | /** Returns whether or not the view has been laid out **/ 29 | private static boolean isLaidOut(View view) { 30 | return ViewCompat.isLaidOut(view) && view.getWidth() > 0 && view.getHeight() > 0; 31 | } 32 | 33 | /** Executes the given {@link java.lang.Runnable} when the view is laid out **/ 34 | static void onLaidOut(final View view, final Runnable runnable) { 35 | if (isLaidOut(view)) { 36 | runnable.run(); 37 | return; 38 | } 39 | 40 | final ViewTreeObserver observer = view.getViewTreeObserver(); 41 | observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 42 | @Override 43 | public void onGlobalLayout() { 44 | final ViewTreeObserver trueObserver; 45 | 46 | if (observer.isAlive()) { 47 | trueObserver = observer; 48 | } else { 49 | trueObserver = view.getViewTreeObserver(); 50 | } 51 | 52 | removeOnGlobalLayoutListener(trueObserver, this); 53 | 54 | runnable.run(); 55 | } 56 | }); 57 | } 58 | 59 | @SuppressWarnings("deprecation") 60 | static void removeOnGlobalLayoutListener(ViewTreeObserver observer, 61 | ViewTreeObserver.OnGlobalLayoutListener listener) { 62 | if (Build.VERSION.SDK_INT >= 16) { 63 | observer.removeOnGlobalLayoutListener(listener); 64 | } else { 65 | observer.removeGlobalOnLayoutListener(listener); 66 | } 67 | } 68 | 69 | static void removeView(ViewManager parent, View child) { 70 | if (parent == null || child == null) { 71 | return; 72 | } 73 | 74 | try { 75 | parent.removeView(child); 76 | } catch (NullPointerException ignored) { 77 | // This catch exists for modified versions of Android that have a buggy ViewGroup 78 | // implementation. See b.android.com/77639, #121 and #49 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/FloatValueAnimatorBuilder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.getkeepsafe.taptargetview; 17 | 18 | import android.animation.Animator; 19 | import android.animation.AnimatorListenerAdapter; 20 | import android.animation.TimeInterpolator; 21 | import android.animation.ValueAnimator; 22 | 23 | /** 24 | * A small wrapper around {@link ValueAnimator} to provide a builder-like interface 25 | */ 26 | class FloatValueAnimatorBuilder { 27 | final ValueAnimator animator; 28 | 29 | EndListener endListener; 30 | 31 | interface UpdateListener { 32 | void onUpdate(float lerpTime); 33 | } 34 | 35 | interface EndListener { 36 | void onEnd(); 37 | } 38 | 39 | protected FloatValueAnimatorBuilder() { 40 | this(false); 41 | } 42 | 43 | protected FloatValueAnimatorBuilder(boolean reverse) { 44 | if (reverse) { 45 | this.animator = ValueAnimator.ofFloat(1.0f, 0.0f); 46 | } else { 47 | this.animator = ValueAnimator.ofFloat(0.0f, 1.0f); 48 | } 49 | } 50 | 51 | public FloatValueAnimatorBuilder delayBy(long millis) { 52 | animator.setStartDelay(millis); 53 | return this; 54 | } 55 | 56 | public FloatValueAnimatorBuilder duration(long millis) { 57 | animator.setDuration(millis); 58 | return this; 59 | } 60 | 61 | public FloatValueAnimatorBuilder interpolator(TimeInterpolator lerper) { 62 | animator.setInterpolator(lerper); 63 | return this; 64 | } 65 | 66 | public FloatValueAnimatorBuilder repeat(int times) { 67 | animator.setRepeatCount(times); 68 | return this; 69 | } 70 | 71 | public FloatValueAnimatorBuilder onUpdate(final UpdateListener listener) { 72 | animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 73 | @Override 74 | public void onAnimationUpdate(ValueAnimator animation) { 75 | listener.onUpdate((float) animation.getAnimatedValue()); 76 | } 77 | }); 78 | return this; 79 | } 80 | 81 | public FloatValueAnimatorBuilder onEnd(final EndListener listener) { 82 | this.endListener = listener; 83 | return this; 84 | } 85 | 86 | public ValueAnimator build() { 87 | if (endListener != null) { 88 | animator.addListener(new AnimatorListenerAdapter() { 89 | @Override 90 | public void onAnimationEnd(Animator animation) { 91 | endListener.onEnd(); 92 | } 93 | }); 94 | } 95 | 96 | return animator; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /taptargetview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | apply plugin: 'com.jfrog.bintray' 4 | 5 | def libname = 'TapTargetView' 6 | group = 'com.getkeepsafe.taptargetview' 7 | version = '1.9.1' 8 | description = 'An implementation of tap targets from the Material Design guidelines for feature discovery' 9 | 10 | android { 11 | compileSdkVersion 25 12 | buildToolsVersion "25.0.2" 13 | 14 | defaultConfig { 15 | minSdkVersion 14 16 | } 17 | } 18 | 19 | def supportLibraryVersion = '25.3.0' 20 | dependencies { 21 | compile "com.android.support:support-annotations:$supportLibraryVersion" 22 | compile "com.android.support:support-compat:$supportLibraryVersion" 23 | compile "com.android.support:appcompat-v7:$supportLibraryVersion" 24 | } 25 | 26 | install { 27 | repositories.mavenInstaller { 28 | pom.project { 29 | name libname 30 | description project.description 31 | url "https://github.com/KeepSafe/$libname" 32 | inceptionYear 2016 33 | 34 | packaging 'aar' 35 | groupId project.group 36 | artifactId 'taptargetview' 37 | version project.version 38 | 39 | licenses { 40 | license { 41 | name 'The Apache Software License, Version 2.0' 42 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 43 | distribution 'repo' 44 | } 45 | } 46 | scm { 47 | connection "https://github.com/KeepSafe/${libname}.git" 48 | url "https://github.com/KeepSafe/$libname" 49 | } 50 | developers { 51 | developer { 52 | name 'Keepsafe' 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | bintray { 60 | user = project.hasProperty('bintray.user') ? project.property('bintray.user') : '' 61 | key = project.hasProperty('bintray.apikey') ? project.property('bintray.apikey') : '' 62 | configurations = ['archives'] 63 | pkg { 64 | repo = 'Android' 65 | name = libname 66 | userOrg = 'keepsafesoftware' 67 | licenses = ['Apache-2.0'] 68 | vcsUrl = "https://github.com/KeepSafe/${libname}.git" 69 | 70 | version { 71 | name = project.version 72 | desc = project.description 73 | released = new Date() 74 | vcsTag = project.version 75 | } 76 | } 77 | } 78 | 79 | // build a jar with source files 80 | task sourcesJar(type: Jar) { 81 | from android.sourceSets.main.java.srcDirs 82 | classifier = 'sources' 83 | } 84 | 85 | task javadoc(type: Javadoc) { 86 | failOnError false 87 | source = android.sourceSets.main.java.sourceFiles 88 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 89 | } 90 | 91 | // build a jar with javadoc 92 | task javadocJar(type: Jar, dependsOn: javadoc) { 93 | classifier = 'javadoc' 94 | from javadoc.destinationDir 95 | } 96 | 97 | artifacts { 98 | archives sourcesJar 99 | archives javadocJar 100 | } 101 | -------------------------------------------------------------------------------- /taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetSequence.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 Keepsafe Software, Inc. 3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | *
10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.getkeepsafe.taptargetview; 17 | 18 | import android.app.Activity; 19 | 20 | import java.util.Collections; 21 | import java.util.LinkedList; 22 | import java.util.List; 23 | import java.util.NoSuchElementException; 24 | import java.util.Queue; 25 | 26 | /** 27 | * Displays a sequence of {@link TapTargetView}s. 28 | *
29 | * Internally, a FIFO queue is held to dictate which {@link TapTarget} will be shown.
30 | */
31 | public class TapTargetSequence {
32 | private final Activity activity;
33 | private final Queue
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.getkeepsafe.taptargetview;
17 |
18 | import android.annotation.TargetApi;
19 | import android.graphics.drawable.Drawable;
20 | import android.os.Build;
21 | import android.support.annotation.IdRes;
22 | import android.support.annotation.Nullable;
23 | import android.support.v7.widget.Toolbar;
24 | import android.text.TextUtils;
25 | import android.view.View;
26 | import android.view.ViewGroup;
27 | import android.widget.ImageButton;
28 | import android.widget.ImageView;
29 |
30 | import java.util.ArrayList;
31 | import java.util.Stack;
32 |
33 | class ToolbarTapTarget extends ViewTapTarget {
34 | ToolbarTapTarget(Toolbar toolbar, @IdRes int menuItemId,
35 | CharSequence title, @Nullable CharSequence description) {
36 | super(toolbar.findViewById(menuItemId), title, description);
37 | }
38 |
39 | ToolbarTapTarget(android.widget.Toolbar toolbar, @IdRes int menuItemId,
40 | CharSequence title, @Nullable CharSequence description) {
41 | super(toolbar.findViewById(menuItemId), title, description);
42 | }
43 |
44 | ToolbarTapTarget(Toolbar toolbar, boolean findNavView,
45 | CharSequence title, @Nullable CharSequence description) {
46 | super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description);
47 | }
48 |
49 | ToolbarTapTarget(android.widget.Toolbar toolbar, boolean findNavView,
50 | CharSequence title, @Nullable CharSequence description) {
51 | super(findNavView ? findNavView(toolbar) : findOverflowView(toolbar), title, description);
52 | }
53 |
54 | private static ToolbarProxy proxyOf(Object instance) {
55 | if (instance == null) {
56 | throw new IllegalArgumentException("Given null instance");
57 | }
58 |
59 | if (instance instanceof Toolbar) {
60 | return new SupportToolbarProxy((Toolbar) instance);
61 | } else if (instance instanceof android.widget.Toolbar) {
62 | return new StandardToolbarProxy((android.widget.Toolbar) instance);
63 | }
64 |
65 | throw new IllegalStateException("Couldn't provide proper toolbar proxy instance");
66 | }
67 |
68 | private static View findNavView(Object instance) {
69 | final ToolbarProxy toolbar = proxyOf(instance);
70 |
71 | // First we try to find the view via its content description
72 | final CharSequence currentDescription = toolbar.getNavigationContentDescription();
73 | final boolean hadContentDescription = !TextUtils.isEmpty(currentDescription);
74 | final CharSequence sentinel = hadContentDescription ? currentDescription : "taptarget-findme";
75 | toolbar.setNavigationContentDescription(sentinel);
76 |
77 | final ArrayList
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.getkeepsafe.taptargetview;
17 |
18 | import android.content.Context;
19 | import android.graphics.Rect;
20 | import android.graphics.Typeface;
21 | import android.graphics.drawable.Drawable;
22 | import android.support.annotation.ColorInt;
23 | import android.support.annotation.ColorRes;
24 | import android.support.annotation.DimenRes;
25 | import android.support.annotation.IdRes;
26 | import android.support.annotation.Nullable;
27 | import android.support.v4.content.ContextCompat;
28 | import android.support.v7.widget.Toolbar;
29 | import android.view.View;
30 |
31 | /**
32 | * Describes the properties and options for a {@link TapTargetView}.
33 | *
34 | * Each tap target describes a target via a pair of bounds and icon. The bounds dictate the
35 | * location and touch area of the target, where the icon is what will be drawn within the center of
36 | * the bounds.
37 | *
38 | * This class can be extended to support various target types.
39 | *
40 | * @see ViewTapTarget ViewTapTarget for targeting standard Android views
41 | */
42 | public class TapTarget {
43 | final CharSequence title;
44 | @Nullable
45 | final CharSequence description;
46 |
47 | float outerCircleAlpha = 0.96f;
48 | int targetRadius = 44;
49 |
50 | Rect bounds;
51 | Drawable icon;
52 | Typeface typeface;
53 |
54 | @ColorRes
55 | private int outerCircleColorRes = -1;
56 | @ColorRes
57 | private int targetCircleColorRes = -1;
58 | @ColorRes
59 | private int dimColorRes = -1;
60 | @ColorRes
61 | private int titleTextColorRes = -1;
62 | @ColorRes
63 | private int descriptionTextColorRes = -1;
64 |
65 | private Integer outerCircleColor = null;
66 | private Integer targetCircleColor = null;
67 | private Integer dimColor = null;
68 | private Integer titleTextColor = null;
69 | private Integer descriptionTextColor = null;
70 |
71 | @DimenRes
72 | private int titleTextDimen = -1;
73 | @DimenRes
74 | private int descriptionTextDimen = -1;
75 |
76 | private int titleTextSize = 20;
77 | private int descriptionTextSize = 18;
78 | int id = -1;
79 |
80 | boolean drawShadow = false;
81 | boolean cancelable = true;
82 | boolean tintTarget = true;
83 | boolean transparentTarget = false;
84 |
85 | /**
86 | * Return a tap target for the overflow button from the given toolbar
87 | *
88 | * Note: This is currently experimental, use at your own risk
89 | */
90 | public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title) {
91 | return forToolbarOverflow(toolbar, title, null);
92 | }
93 |
94 | /** Return a tap target for the overflow button from the given toolbar
95 | *
96 | * Note: This is currently experimental, use at your own risk
97 | */
98 | public static TapTarget forToolbarOverflow(Toolbar toolbar, CharSequence title,
99 | @Nullable CharSequence description) {
100 | return new ToolbarTapTarget(toolbar, false, title, description);
101 | }
102 |
103 | /** Return a tap target for the overflow button from the given toolbar
104 | *
105 | * Note: This is currently experimental, use at your own risk
106 | */
107 | public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title) {
108 | return forToolbarOverflow(toolbar, title, null);
109 | }
110 |
111 | /** Return a tap target for the overflow button from the given toolbar
112 | *
113 | * Note: This is currently experimental, use at your own risk
114 | */
115 | public static TapTarget forToolbarOverflow(android.widget.Toolbar toolbar, CharSequence title,
116 | @Nullable CharSequence description) {
117 | return new ToolbarTapTarget(toolbar, false, title, description);
118 | }
119 |
120 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/
121 | public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title) {
122 | return forToolbarNavigationIcon(toolbar, title, null);
123 | }
124 |
125 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/
126 | public static TapTarget forToolbarNavigationIcon(Toolbar toolbar, CharSequence title,
127 | @Nullable CharSequence description) {
128 | return new ToolbarTapTarget(toolbar, true, title, description);
129 | }
130 |
131 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/
132 | public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title) {
133 | return forToolbarNavigationIcon(toolbar, title, null);
134 | }
135 |
136 | /** Return a tap target for the navigation button (back, up, etc) from the given toolbar **/
137 | public static TapTarget forToolbarNavigationIcon(android.widget.Toolbar toolbar, CharSequence title,
138 | @Nullable CharSequence description) {
139 | return new ToolbarTapTarget(toolbar, true, title, description);
140 | }
141 |
142 | /** Return a tap target for the menu item from the given toolbar **/
143 | public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId,
144 | CharSequence title) {
145 | return forToolbarMenuItem(toolbar, menuItemId, title, null);
146 | }
147 |
148 | /** Return a tap target for the menu item from the given toolbar **/
149 | public static TapTarget forToolbarMenuItem(Toolbar toolbar, @IdRes int menuItemId,
150 | CharSequence title, @Nullable CharSequence description) {
151 | return new ToolbarTapTarget(toolbar, menuItemId, title, description);
152 | }
153 |
154 | /** Return a tap target for the menu item from the given toolbar **/
155 | public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId,
156 | CharSequence title) {
157 | return forToolbarMenuItem(toolbar, menuItemId, title, null);
158 | }
159 |
160 | /** Return a tap target for the menu item from the given toolbar **/
161 | public static TapTarget forToolbarMenuItem(android.widget.Toolbar toolbar, @IdRes int menuItemId,
162 | CharSequence title, @Nullable CharSequence description) {
163 | return new ToolbarTapTarget(toolbar, menuItemId, title, description);
164 | }
165 |
166 | /** Return a tap target for the specified view **/
167 | public static TapTarget forView(View view, CharSequence title) {
168 | return forView(view, title, null);
169 | }
170 |
171 | /** Return a tap target for the specified view **/
172 | public static TapTarget forView(View view, CharSequence title, @Nullable CharSequence description) {
173 | return new ViewTapTarget(view, title, description);
174 | }
175 |
176 | /** Return a tap target for the specified bounds **/
177 | public static TapTarget forBounds(Rect bounds, CharSequence title) {
178 | return forBounds(bounds, title, null);
179 | }
180 |
181 | /** Return a tap target for the specified bounds **/
182 | public static TapTarget forBounds(Rect bounds, CharSequence title, @Nullable CharSequence description) {
183 | return new TapTarget(bounds, title, description);
184 | }
185 |
186 | protected TapTarget(Rect bounds, CharSequence title, @Nullable CharSequence description) {
187 | this(title, description);
188 | if (bounds == null) {
189 | throw new IllegalArgumentException("Cannot pass null bounds or title");
190 | }
191 |
192 | this.bounds = bounds;
193 | }
194 |
195 | protected TapTarget(CharSequence title, @Nullable CharSequence description) {
196 | if (title == null) {
197 | throw new IllegalArgumentException("Cannot pass null title");
198 | }
199 |
200 | this.title = title;
201 | this.description = description;
202 | }
203 |
204 | /** Specify whether the target should be transparent **/
205 | public TapTarget transparentTarget(boolean transparent) {
206 | this.transparentTarget = transparent;
207 | return this;
208 | }
209 |
210 | /** Specify the color resource for the outer circle **/
211 | public TapTarget outerCircleColor(@ColorRes int color) {
212 | this.outerCircleColorRes = color;
213 | return this;
214 | }
215 |
216 | /** Specify the color value for the outer circle **/
217 | // TODO(Hilal): In v2, this API should be cleaned up / torched
218 | public TapTarget outerCircleColorInt(@ColorInt int color) {
219 | this.outerCircleColor = color;
220 | return this;
221 | }
222 |
223 | /** Specify the alpha value [0.0, 1.0] of the outer circle **/
224 | public TapTarget outerCircleAlpha(float alpha) {
225 | if (alpha < 0.0f || alpha > 1.0f) {
226 | throw new IllegalArgumentException("Given an invalid alpha value: " + alpha);
227 | }
228 | this.outerCircleAlpha = alpha;
229 | return this;
230 | }
231 |
232 | /** Specify the color resource for the target circle **/
233 | public TapTarget targetCircleColor(@ColorRes int color) {
234 | this.targetCircleColorRes = color;
235 | return this;
236 | }
237 |
238 | /** Specify the color value for the target circle **/
239 | // TODO(Hilal): In v2, this API should be cleaned up / torched
240 | public TapTarget targetCircleColorInt(@ColorInt int color) {
241 | this.targetCircleColor = color;
242 | return this;
243 | }
244 |
245 | /** Specify the color resource for all text **/
246 | public TapTarget textColor(@ColorRes int color) {
247 | this.titleTextColorRes = color;
248 | this.descriptionTextColorRes = color;
249 | return this;
250 | }
251 |
252 | /** Specify the color value for all text **/
253 | // TODO(Hilal): In v2, this API should be cleaned up / torched
254 | public TapTarget textColorInt(@ColorInt int color) {
255 | this.titleTextColor = color;
256 | this.descriptionTextColor = color;
257 | return this;
258 | }
259 |
260 | /** Specify the color resource for the title text **/
261 | public TapTarget titleTextColor(@ColorRes int color) {
262 | this.titleTextColorRes = color;
263 | return this;
264 | }
265 |
266 | /** Specify the color value for the title text **/
267 | // TODO(Hilal): In v2, this API should be cleaned up / torched
268 | public TapTarget titleTextColorInt(@ColorInt int color) {
269 | this.titleTextColor = color;
270 | return this;
271 | }
272 |
273 | /** Specify the color resource for the description text **/
274 | public TapTarget descriptionTextColor(@ColorRes int color) {
275 | this.descriptionTextColorRes = color;
276 | return this;
277 | }
278 |
279 | /** Specify the color value for the description text **/
280 | // TODO(Hilal): In v2, this API should be cleaned up / torched
281 | public TapTarget descriptionTextColorInt(@ColorInt int color) {
282 | this.descriptionTextColor = color;
283 | return this;
284 | }
285 |
286 | /** Specify the typeface for all text **/
287 | public TapTarget textTypeface(Typeface typeface) {
288 | if (typeface == null) throw new IllegalArgumentException("Cannot use a null typeface");
289 | this.typeface = typeface;
290 | return this;
291 | }
292 |
293 | /** Specify the text size for the title in SP **/
294 | public TapTarget titleTextSize(int sp) {
295 | if (sp < 0) throw new IllegalArgumentException("Given negative text size");
296 | this.titleTextSize = sp;
297 | return this;
298 | }
299 |
300 | /** Specify the text size for the description in SP **/
301 | public TapTarget descriptionTextSize(int sp) {
302 | if (sp < 0) throw new IllegalArgumentException("Given negative text size");
303 | this.descriptionTextSize = sp;
304 | return this;
305 | }
306 |
307 | /**
308 | * Specify the text size for the title via a dimen resource
309 | *
310 | * Note: If set, this value will take precedence over the specified sp size
311 | */
312 | public TapTarget titleTextDimen(@DimenRes int dimen) {
313 | this.titleTextDimen = dimen;
314 | return this;
315 | }
316 |
317 | /**
318 | * Specify the text size for the description via a dimen resource
319 | *
320 | * Note: If set, this value will take precedence over the specified sp size
321 | */
322 | public TapTarget descriptionTextDimen(@DimenRes int dimen) {
323 | this.descriptionTextDimen = dimen;
324 | return this;
325 | }
326 |
327 | /**
328 | * Specify the color resource to use as a dim effect
329 | *
330 | * Note: The given color will have its opacity modified to 30% automatically
331 | */
332 | public TapTarget dimColor(@ColorRes int color) {
333 | this.dimColorRes = color;
334 | return this;
335 | }
336 |
337 | /**
338 | * Specify the color value to use as a dim effect
339 | *
340 | * Note: The given color will have its opacity modified to 30% automatically
341 | */
342 | // TODO(Hilal): In v2, this API should be cleaned up / torched
343 | public TapTarget dimColorInt(@ColorInt int color) {
344 | this.dimColor = color;
345 | return this;
346 | }
347 |
348 | /** Specify whether or not to draw a drop shadow around the outer circle **/
349 | public TapTarget drawShadow(boolean draw) {
350 | this.drawShadow = draw;
351 | return this;
352 | }
353 |
354 | /** Specify whether or not the target should be cancelable **/
355 | public TapTarget cancelable(boolean status) {
356 | this.cancelable = status;
357 | return this;
358 | }
359 |
360 | /** Specify whether to tint the target's icon with the outer circle's color **/
361 | public TapTarget tintTarget(boolean tint) {
362 | this.tintTarget = tint;
363 | return this;
364 | }
365 |
366 | /** Specify the icon that will be drawn in the center of the target bounds **/
367 | public TapTarget icon(Drawable icon) {
368 | return icon(icon, false);
369 | }
370 |
371 | /**
372 | * Specify the icon that will be drawn in the center of the target bounds
373 | * @param hasSetBounds Whether the drawable already has its bounds correctly set. If the
374 | * drawable does not have its bounds set, then the following bounds will
375 | * be applied:
419 | * This will only be called internally when {@link #onReady(Runnable)} invokes its runnable
420 | */
421 | public Rect bounds() {
422 | if (bounds == null) {
423 | throw new IllegalStateException("Requesting bounds that are not set! Make sure your target is ready");
424 | }
425 | return bounds;
426 | }
427 |
428 | @Nullable
429 | Integer outerCircleColorInt(Context context) {
430 | return colorResOrInt(context, outerCircleColor, outerCircleColorRes);
431 | }
432 |
433 | @Nullable
434 | Integer targetCircleColorInt(Context context) {
435 | return colorResOrInt(context, targetCircleColor, targetCircleColorRes);
436 | }
437 |
438 | @Nullable
439 | Integer dimColorInt(Context context) {
440 | return colorResOrInt(context, dimColor, dimColorRes);
441 | }
442 |
443 | @Nullable
444 | Integer titleTextColorInt(Context context) {
445 | return colorResOrInt(context, titleTextColor, titleTextColorRes);
446 | }
447 |
448 | @Nullable
449 | Integer descriptionTextColorInt(Context context) {
450 | return colorResOrInt(context, descriptionTextColor, descriptionTextColorRes);
451 | }
452 |
453 | int titleTextSizePx(Context context) {
454 | return dimenOrSize(context, titleTextSize, titleTextDimen);
455 | }
456 |
457 | int descriptionTextSizePx(Context context) {
458 | return dimenOrSize(context, descriptionTextSize, descriptionTextDimen);
459 | }
460 |
461 | @Nullable
462 | private Integer colorResOrInt(Context context, @Nullable Integer value, @ColorRes int resource) {
463 | if (resource != -1) {
464 | return ContextCompat.getColor(context, resource);
465 | }
466 |
467 | return value;
468 | }
469 |
470 | private int dimenOrSize(Context context, int size, @DimenRes int dimen) {
471 | if (dimen != -1) {
472 | return context.getResources().getDimensionPixelSize(dimen);
473 | }
474 |
475 | return UiUtil.sp(context, size);
476 | }
477 | }
478 |
--------------------------------------------------------------------------------
/taptargetview/src/main/java/com/getkeepsafe/taptargetview/TapTargetView.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016 Keepsafe Software, Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.getkeepsafe.taptargetview;
17 |
18 | import android.animation.ValueAnimator;
19 | import android.annotation.SuppressLint;
20 | import android.annotation.TargetApi;
21 | import android.app.Activity;
22 | import android.app.Dialog;
23 | import android.content.Context;
24 | import android.content.res.Resources;
25 | import android.graphics.Bitmap;
26 | import android.graphics.Canvas;
27 | import android.graphics.Color;
28 | import android.graphics.Outline;
29 | import android.graphics.Paint;
30 | import android.graphics.Path;
31 | import android.graphics.PixelFormat;
32 | import android.graphics.PorterDuff;
33 | import android.graphics.PorterDuffColorFilter;
34 | import android.graphics.PorterDuffXfermode;
35 | import android.graphics.Rect;
36 | import android.graphics.Region;
37 | import android.graphics.Typeface;
38 | import android.graphics.drawable.Drawable;
39 | import android.os.Build;
40 | import android.support.annotation.Nullable;
41 | import android.text.DynamicLayout;
42 | import android.text.Layout;
43 | import android.text.SpannableStringBuilder;
44 | import android.text.StaticLayout;
45 | import android.text.TextPaint;
46 | import android.util.DisplayMetrics;
47 | import android.view.Gravity;
48 | import android.view.KeyEvent;
49 | import android.view.MotionEvent;
50 | import android.view.View;
51 | import android.view.ViewGroup;
52 | import android.view.ViewManager;
53 | import android.view.ViewOutlineProvider;
54 | import android.view.ViewTreeObserver;
55 | import android.view.WindowManager;
56 | import android.view.animation.AccelerateDecelerateInterpolator;
57 |
58 | /**
59 | * TapTargetView implements a feature discovery paradigm following Google's Material Design
60 | * guidelines.
61 | *
62 | * This class should not be instantiated directly. Instead, please use the
63 | * {@link #showFor(Activity, TapTarget, Listener)} static factory method instead.
64 | *
65 | * More information can be found here:
66 | * https://material.google.com/growth-communications/feature-discovery.html#feature-discovery-design
67 | */
68 | @SuppressLint("ViewConstructor")
69 | public class TapTargetView extends View {
70 | private boolean isDismissed = false;
71 | private boolean isInteractable = true;
72 |
73 | final int TARGET_PADDING;
74 | final int TARGET_RADIUS;
75 | final int TARGET_PULSE_RADIUS;
76 | final int TEXT_PADDING;
77 | final int TEXT_SPACING;
78 | final int TEXT_MAX_WIDTH;
79 | final int TEXT_POSITIONING_BIAS;
80 | final int CIRCLE_PADDING;
81 | final int GUTTER_DIM;
82 | final int SHADOW_DIM;
83 | final int SHADOW_JITTER_DIM;
84 |
85 | @Nullable
86 | final ViewGroup boundingParent;
87 | final ViewManager parent;
88 | final TapTarget target;
89 | final Rect targetBounds;
90 |
91 | final TextPaint titlePaint;
92 | final TextPaint descriptionPaint;
93 | final Paint outerCirclePaint;
94 | final Paint outerCircleShadowPaint;
95 | final Paint targetCirclePaint;
96 | final Paint targetCirclePulsePaint;
97 |
98 | CharSequence title;
99 | @Nullable
100 | StaticLayout titleLayout;
101 | @Nullable
102 | CharSequence description;
103 | @Nullable
104 | StaticLayout descriptionLayout;
105 | boolean isDark;
106 | boolean debug;
107 | boolean shouldTintTarget;
108 | boolean shouldDrawShadow;
109 | boolean cancelable;
110 | boolean visible;
111 |
112 | // Debug related variables
113 | @Nullable
114 | SpannableStringBuilder debugStringBuilder;
115 | @Nullable
116 | DynamicLayout debugLayout;
117 | @Nullable
118 | TextPaint debugTextPaint;
119 | @Nullable
120 | Paint debugPaint;
121 |
122 | // Drawing properties
123 | Rect drawingBounds;
124 | Rect textBounds;
125 |
126 | Path outerCirclePath;
127 | float outerCircleRadius;
128 | int calculatedOuterCircleRadius;
129 | int[] outerCircleCenter;
130 | int outerCircleAlpha;
131 |
132 | float targetCirclePulseRadius;
133 | int targetCirclePulseAlpha;
134 |
135 | float targetCircleRadius;
136 | int targetCircleAlpha;
137 |
138 | int textAlpha;
139 | int dimColor;
140 |
141 | float lastTouchX;
142 | float lastTouchY;
143 |
144 | int topBoundary;
145 | int bottomBoundary;
146 |
147 | Bitmap tintedTarget;
148 |
149 | Listener listener;
150 |
151 | @Nullable
152 | ViewOutlineProvider outlineProvider;
153 |
154 | public static TapTargetView showFor(Activity activity, TapTarget target) {
155 | return showFor(activity, target, null);
156 | }
157 |
158 | public static TapTargetView showFor(Activity activity, TapTarget target, Listener listener) {
159 | if (activity == null) throw new IllegalArgumentException("Activity is null");
160 |
161 | final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
162 | final ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
163 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
164 | final ViewGroup content = (ViewGroup) decor.findViewById(android.R.id.content);
165 | final TapTargetView tapTargetView = new TapTargetView(activity, decor, content, target, listener);
166 | decor.addView(tapTargetView, layoutParams);
167 |
168 | return tapTargetView;
169 | }
170 |
171 | public static TapTargetView showFor(Dialog dialog, TapTarget target) {
172 | return showFor(dialog, target, null);
173 | }
174 |
175 | public static TapTargetView showFor(Dialog dialog, TapTarget target, Listener listener) {
176 | if (dialog == null) throw new IllegalArgumentException("Dialog is null");
177 |
178 | final Context context = dialog.getContext();
179 | final WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
180 | final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
181 | params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
182 | params.format = PixelFormat.RGBA_8888;
183 | params.flags = 0;
184 | params.gravity = Gravity.START | Gravity.TOP;
185 | params.x = 0;
186 | params.y = 0;
187 | params.width = WindowManager.LayoutParams.MATCH_PARENT;
188 | params.height = WindowManager.LayoutParams.MATCH_PARENT;
189 |
190 | final TapTargetView tapTargetView = new TapTargetView(context, windowManager, null, target, listener);
191 | windowManager.addView(tapTargetView, params);
192 |
193 | return tapTargetView;
194 | }
195 |
196 | public static class Listener {
197 | /** Signals that the user has clicked inside of the target **/
198 | public void onTargetClick(TapTargetView view) {
199 | view.dismiss(true);
200 | }
201 |
202 | /** Signals that the user has long clicked inside of the target **/
203 | public void onTargetLongClick(TapTargetView view) {
204 | onTargetClick(view);
205 | }
206 |
207 | /** If cancelable, signals that the user has clicked outside of the outer circle **/
208 | public void onTargetCancel(TapTargetView view) {
209 | view.dismiss(false);
210 | }
211 |
212 | /** Signals that the user clicked on the outer circle portion of the tap target **/
213 | public void onOuterCircleClick(TapTargetView view) {
214 | // no-op as default
215 | }
216 |
217 | /**
218 | * Signals that the tap target has been dismissed
219 | * @param userInitiated Whether the user caused this action
220 | */
221 | public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
222 | }
223 | }
224 |
225 | final FloatValueAnimatorBuilder.UpdateListener expandContractUpdateListener = new FloatValueAnimatorBuilder.UpdateListener() {
226 | @Override
227 | public void onUpdate(float lerpTime) {
228 | final float newOuterCircleRadius = calculatedOuterCircleRadius * lerpTime;
229 | final boolean expanding = newOuterCircleRadius > outerCircleRadius;
230 | if (!expanding) {
231 | // When contracting we need to invalidate the old drawing bounds. Otherwise
232 | // you will see artifacts as the circle gets smaller
233 | calculateDrawingBounds();
234 | }
235 |
236 | final float targetAlpha = target.outerCircleAlpha * 255;
237 | outerCircleRadius = newOuterCircleRadius;
238 | outerCircleAlpha = (int) Math.min(targetAlpha, (lerpTime * 1.5f * targetAlpha));
239 | outerCirclePath.reset();
240 | outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
241 |
242 | targetCircleAlpha = (int) Math.min(255.0f, (lerpTime * 1.5f * 255.0f));
243 |
244 | if (expanding) {
245 | targetCircleRadius = TARGET_RADIUS * Math.min(1.0f, lerpTime * 1.5f);
246 | } else {
247 | targetCircleRadius = TARGET_RADIUS * lerpTime;
248 | targetCirclePulseRadius *= lerpTime;
249 | }
250 |
251 | textAlpha = (int) (delayedLerp(lerpTime, 0.7f) * 255);
252 |
253 | if (expanding) {
254 | calculateDrawingBounds();
255 | }
256 |
257 | invalidateViewAndOutline(drawingBounds);
258 | }
259 | };
260 |
261 | final ValueAnimator expandAnimation = new FloatValueAnimatorBuilder()
262 | .duration(250)
263 | .delayBy(250)
264 | .interpolator(new AccelerateDecelerateInterpolator())
265 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
266 | @Override
267 | public void onUpdate(float lerpTime) {
268 | expandContractUpdateListener.onUpdate(lerpTime);
269 | }
270 | })
271 | .onEnd(new FloatValueAnimatorBuilder.EndListener() {
272 | @Override
273 | public void onEnd() {
274 | pulseAnimation.start();
275 | }
276 | })
277 | .build();
278 |
279 | final ValueAnimator pulseAnimation = new FloatValueAnimatorBuilder()
280 | .duration(1000)
281 | .repeat(ValueAnimator.INFINITE)
282 | .interpolator(new AccelerateDecelerateInterpolator())
283 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
284 | @Override
285 | public void onUpdate(float lerpTime) {
286 | final float pulseLerp = delayedLerp(lerpTime, 0.5f);
287 | targetCirclePulseRadius = (1.0f + pulseLerp) * TARGET_RADIUS;
288 | targetCirclePulseAlpha = (int) ((1.0f - pulseLerp) * 255);
289 | targetCircleRadius = TARGET_RADIUS + halfwayLerp(lerpTime) * TARGET_PULSE_RADIUS;
290 |
291 | if (outerCircleRadius != calculatedOuterCircleRadius) {
292 | outerCircleRadius = calculatedOuterCircleRadius;
293 | }
294 |
295 | calculateDrawingBounds();
296 | invalidateViewAndOutline(drawingBounds);
297 | }
298 | })
299 | .build();
300 |
301 | final ValueAnimator dismissAnimation = new FloatValueAnimatorBuilder(true)
302 | .duration(250)
303 | .interpolator(new AccelerateDecelerateInterpolator())
304 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
305 | @Override
306 | public void onUpdate(float lerpTime) {
307 | expandContractUpdateListener.onUpdate(lerpTime);
308 | }
309 | })
310 | .onEnd(new FloatValueAnimatorBuilder.EndListener() {
311 | @Override
312 | public void onEnd() {
313 | ViewUtil.removeView(parent, TapTargetView.this);
314 | onDismiss();
315 | }
316 | })
317 | .build();
318 |
319 | private final ValueAnimator dismissConfirmAnimation = new FloatValueAnimatorBuilder()
320 | .duration(250)
321 | .interpolator(new AccelerateDecelerateInterpolator())
322 | .onUpdate(new FloatValueAnimatorBuilder.UpdateListener() {
323 | @Override
324 | public void onUpdate(float lerpTime) {
325 | final float spedUpLerp = Math.min(1.0f, lerpTime * 2.0f);
326 | outerCircleRadius = calculatedOuterCircleRadius * (1.0f + (spedUpLerp * 0.2f));
327 | outerCircleAlpha = (int) ((1.0f - spedUpLerp) * target.outerCircleAlpha * 255.0f);
328 | outerCirclePath.reset();
329 | outerCirclePath.addCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, Path.Direction.CW);
330 | targetCircleRadius = (1.0f - lerpTime) * TARGET_RADIUS;
331 | targetCircleAlpha = (int) ((1.0f - lerpTime) * 255.0f);
332 | targetCirclePulseRadius = (1.0f + lerpTime) * TARGET_RADIUS;
333 | targetCirclePulseAlpha = (int) ((1.0f - lerpTime) * targetCirclePulseAlpha);
334 | textAlpha = (int) ((1.0f - spedUpLerp) * 255.0f);
335 | calculateDrawingBounds();
336 | invalidateViewAndOutline(drawingBounds);
337 | }
338 | })
339 | .onEnd(new FloatValueAnimatorBuilder.EndListener() {
340 | @Override
341 | public void onEnd() {
342 | ViewUtil.removeView(parent, TapTargetView.this);
343 | onDismiss();
344 | }
345 | })
346 | .build();
347 |
348 | private ValueAnimator[] animators = new ValueAnimator[]
349 | {expandAnimation, pulseAnimation, dismissConfirmAnimation, dismissAnimation};
350 |
351 | private final ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
352 |
353 | /**
354 | * This constructor should only be used directly for very specific use cases not covered by
355 | * the static factory methods.
356 | *
357 | * @param context The host context
358 | * @param parent The parent that this TapTargetView will become a child of. This parent should
359 | * allow the largest possible area for this view to utilize
360 | * @param boundingParent Optional. Will be used to calculate boundaries if needed. For example,
361 | * if your view is added to the decor view of your Window, then you want
362 | * to adjust for system ui like the navigation bar or status bar, and so
363 | * you would pass in the content view (which doesn't include system ui)
364 | * here.
365 | * @param target The {@link TapTarget} to target
366 | * @param userListener Optional. The {@link Listener} instance for this view
367 | */
368 | public TapTargetView(final Context context,
369 | final ViewManager parent,
370 | @Nullable final ViewGroup boundingParent,
371 | final TapTarget target,
372 | @Nullable final Listener userListener) {
373 | super(context);
374 | if (target == null) throw new IllegalArgumentException("Target cannot be null");
375 |
376 | this.target = target;
377 | this.parent = parent;
378 | this.boundingParent = boundingParent;
379 | this.listener = userListener != null ? userListener : new Listener();
380 | this.title = target.title;
381 | this.description = target.description;
382 |
383 | TARGET_PADDING = UiUtil.dp(context, 20);
384 | CIRCLE_PADDING = UiUtil.dp(context, 40);
385 | TARGET_RADIUS = UiUtil.dp(context, target.targetRadius);
386 | TEXT_PADDING = UiUtil.dp(context, 40);
387 | TEXT_SPACING = UiUtil.dp(context, 8);
388 | TEXT_MAX_WIDTH = UiUtil.dp(context, 360);
389 | TEXT_POSITIONING_BIAS = UiUtil.dp(context, 20);
390 | GUTTER_DIM = UiUtil.dp(context, 88);
391 | SHADOW_DIM = UiUtil.dp(context, 8);
392 | SHADOW_JITTER_DIM = UiUtil.dp(context, 1);
393 | TARGET_PULSE_RADIUS = (int) (0.1f * TARGET_RADIUS);
394 |
395 | outerCirclePath = new Path();
396 | targetBounds = new Rect();
397 | drawingBounds = new Rect();
398 |
399 | titlePaint = new TextPaint();
400 | titlePaint.setTextSize(target.titleTextSizePx(context));
401 | titlePaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
402 | titlePaint.setAntiAlias(true);
403 |
404 | descriptionPaint = new TextPaint();
405 | descriptionPaint.setTextSize(target.descriptionTextSizePx(context));
406 | descriptionPaint.setTypeface(Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL));
407 | descriptionPaint.setAntiAlias(true);
408 | descriptionPaint.setAlpha((int) (0.54f * 255.0f));
409 |
410 | outerCirclePaint = new Paint();
411 | outerCirclePaint.setAntiAlias(true);
412 | outerCirclePaint.setAlpha((int) (target.outerCircleAlpha * 255.0f));
413 |
414 | outerCircleShadowPaint = new Paint();
415 | outerCircleShadowPaint.setAntiAlias(true);
416 | outerCircleShadowPaint.setAlpha(50);
417 | outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
418 | outerCircleShadowPaint.setStrokeWidth(SHADOW_JITTER_DIM);
419 | outerCircleShadowPaint.setColor(Color.BLACK);
420 |
421 | targetCirclePaint = new Paint();
422 | targetCirclePaint.setAntiAlias(true);
423 |
424 | targetCirclePulsePaint = new Paint();
425 | targetCirclePulsePaint.setAntiAlias(true);
426 |
427 | applyTargetOptions(context);
428 |
429 | globalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
430 | @Override
431 | public void onGlobalLayout() {
432 | updateTextLayouts();
433 | target.onReady(new Runnable() {
434 | @Override
435 | public void run() {
436 | final int[] offset = new int[2];
437 |
438 | targetBounds.set(target.bounds());
439 |
440 | getLocationOnScreen(offset);
441 | targetBounds.offset(-offset[0], -offset[1]);
442 |
443 | if (boundingParent != null) {
444 | final WindowManager windowManager
445 | = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
446 | final DisplayMetrics displayMetrics = new DisplayMetrics();
447 | windowManager.getDefaultDisplay().getMetrics(displayMetrics);
448 |
449 | final Rect rect = new Rect();
450 | boundingParent.getWindowVisibleDisplayFrame(rect);
451 |
452 | // We bound the boundaries to be within the screen's coordinates to
453 | // handle the case where the layout bounds do not match
454 | // (like when FLAG_LAYOUT_NO_LIMITS is specified)
455 | topBoundary = Math.max(0, rect.top);
456 | bottomBoundary = Math.min(rect.bottom, displayMetrics.heightPixels);
457 | }
458 |
459 | drawTintedTarget();
460 | requestFocus();
461 | calculateDimensions();
462 | if (!visible) {
463 | expandAnimation.start();
464 | visible = true;
465 | }
466 | }
467 | });
468 | }
469 | };
470 |
471 | getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
472 |
473 | setFocusableInTouchMode(true);
474 | setClickable(true);
475 | setOnClickListener(new OnClickListener() {
476 | @Override
477 | public void onClick(View v) {
478 | if (listener == null || outerCircleCenter == null || !isInteractable) return;
479 |
480 | final boolean clickedInTarget =
481 | distance(targetBounds.centerX(), targetBounds.centerY(), (int) lastTouchX, (int) lastTouchY) <= targetCircleRadius;
482 | final double distanceToOuterCircleCenter = distance(outerCircleCenter[0], outerCircleCenter[1],
483 | (int) lastTouchX, (int) lastTouchY);
484 | final boolean clickedInsideOfOuterCircle = distanceToOuterCircleCenter <= outerCircleRadius;
485 |
486 | if (clickedInTarget) {
487 | isInteractable = false;
488 | listener.onTargetClick(TapTargetView.this);
489 | } else if (clickedInsideOfOuterCircle) {
490 | listener.onOuterCircleClick(TapTargetView.this);
491 | } else if (cancelable) {
492 | isInteractable = false;
493 | listener.onTargetCancel(TapTargetView.this);
494 | }
495 | }
496 | });
497 |
498 | setOnLongClickListener(new OnLongClickListener() {
499 | @Override
500 | public boolean onLongClick(View v) {
501 | if (listener == null) return false;
502 |
503 | if (targetBounds.contains((int) lastTouchX, (int) lastTouchY)) {
504 | listener.onTargetLongClick(TapTargetView.this);
505 | return true;
506 | }
507 |
508 | return false;
509 | }
510 | });
511 | }
512 |
513 | protected void applyTargetOptions(Context context) {
514 | shouldTintTarget = target.tintTarget;
515 | shouldDrawShadow = target.drawShadow;
516 | cancelable = target.cancelable;
517 |
518 | // We can't clip out portions of a view outline, so if the user specified a transparent
519 | // target, we need to fallback to drawing a jittered shadow approximation
520 | if (shouldDrawShadow && Build.VERSION.SDK_INT >= 21 && !target.transparentTarget) {
521 | outlineProvider = new ViewOutlineProvider() {
522 | @TargetApi(Build.VERSION_CODES.LOLLIPOP)
523 | @Override
524 | public void getOutline(View view, Outline outline) {
525 | if (outerCircleCenter == null) return;
526 | outline.setOval(
527 | (int) (outerCircleCenter[0] - outerCircleRadius), (int) (outerCircleCenter[1] - outerCircleRadius),
528 | (int) (outerCircleCenter[0] + outerCircleRadius), (int) (outerCircleCenter[1] + outerCircleRadius));
529 | outline.setAlpha(outerCircleAlpha / 255.0f);
530 | if (Build.VERSION.SDK_INT >= 22) {
531 | outline.offset(0, SHADOW_DIM);
532 | }
533 | }
534 | };
535 |
536 | setOutlineProvider(outlineProvider);
537 | setElevation(SHADOW_DIM);
538 | }
539 |
540 | if (shouldDrawShadow && outlineProvider == null && Build.VERSION.SDK_INT < 18) {
541 | setLayerType(LAYER_TYPE_SOFTWARE, null);
542 | } else {
543 | setLayerType(LAYER_TYPE_HARDWARE, null);
544 | }
545 |
546 | final Resources.Theme theme = context.getTheme();
547 | isDark = UiUtil.themeIntAttr(context, "isLightTheme") == 0;
548 |
549 | final Integer outerCircleColor = target.outerCircleColorInt(context);
550 | if (outerCircleColor != null) {
551 | outerCirclePaint.setColor(outerCircleColor);
552 | } else if (theme != null) {
553 | outerCirclePaint.setColor(UiUtil.themeIntAttr(context, "colorPrimary"));
554 | } else {
555 | outerCirclePaint.setColor(Color.WHITE);
556 | }
557 |
558 | final Integer targetCircleColor = target.targetCircleColorInt(context);
559 | if (targetCircleColor != null) {
560 | targetCirclePaint.setColor(targetCircleColor);
561 | } else {
562 | targetCirclePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
563 | }
564 |
565 | if (target.transparentTarget) {
566 | targetCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
567 | }
568 |
569 | targetCirclePulsePaint.setColor(targetCirclePaint.getColor());
570 |
571 | final Integer targetDimColor = target.dimColorInt(context);
572 | if (targetDimColor != null) {
573 | dimColor = UiUtil.setAlpha(targetDimColor, 0.3f);
574 | } else {
575 | dimColor = -1;
576 | }
577 |
578 | final Integer titleTextColor = target.titleTextColorInt(context);
579 | if (titleTextColor != null) {
580 | titlePaint.setColor(titleTextColor);
581 | } else {
582 | titlePaint.setColor(isDark ? Color.BLACK : Color.WHITE);
583 | }
584 |
585 | final Integer descriptionTextColor = target.descriptionTextColorInt(context);
586 | if (descriptionTextColor != null) {
587 | descriptionPaint.setColor(descriptionTextColor);
588 | } else {
589 | descriptionPaint.setColor(titlePaint.getColor());
590 | }
591 |
592 | if (target.typeface != null) {
593 | titlePaint.setTypeface(target.typeface);
594 | descriptionPaint.setTypeface(target.typeface);
595 | }
596 | }
597 |
598 | @Override
599 | protected void onDetachedFromWindow() {
600 | super.onDetachedFromWindow();
601 | onDismiss(false);
602 | }
603 |
604 | void onDismiss() {
605 | onDismiss(true);
606 | }
607 |
608 | void onDismiss(boolean userInitiated) {
609 | if (isDismissed) return;
610 |
611 | isDismissed = true;
612 |
613 | for (final ValueAnimator animator : animators) {
614 | animator.cancel();
615 | animator.removeAllUpdateListeners();
616 | }
617 |
618 | ViewUtil.removeOnGlobalLayoutListener(getViewTreeObserver(), globalLayoutListener);
619 | visible = false;
620 |
621 | if (listener != null) {
622 | listener.onTargetDismissed(this, userInitiated);
623 | }
624 | }
625 |
626 | @Override
627 | protected void onDraw(Canvas c) {
628 | if (isDismissed || outerCircleCenter == null) return;
629 |
630 | if (topBoundary > 0 && bottomBoundary > 0) {
631 | c.clipRect(0, topBoundary, getWidth(), bottomBoundary);
632 | }
633 |
634 | if (dimColor != -1) {
635 | c.drawColor(dimColor);
636 | }
637 |
638 | int saveCount;
639 | outerCirclePaint.setAlpha(outerCircleAlpha);
640 | if (shouldDrawShadow && outlineProvider == null) {
641 | saveCount = c.save();
642 | {
643 | c.clipPath(outerCirclePath, Region.Op.DIFFERENCE);
644 | drawJitteredShadow(c);
645 | }
646 | c.restoreToCount(saveCount);
647 | }
648 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], outerCircleRadius, outerCirclePaint);
649 |
650 | targetCirclePaint.setAlpha(targetCircleAlpha);
651 | if (targetCirclePulseAlpha > 0) {
652 | targetCirclePulsePaint.setAlpha(targetCirclePulseAlpha);
653 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
654 | targetCirclePulseRadius, targetCirclePulsePaint);
655 | }
656 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(),
657 | targetCircleRadius, targetCirclePaint);
658 |
659 | saveCount = c.save();
660 | {
661 | c.clipPath(outerCirclePath);
662 | c.translate(textBounds.left, textBounds.top);
663 | titlePaint.setAlpha(textAlpha);
664 | if (titleLayout != null) {
665 | titleLayout.draw(c);
666 | }
667 |
668 | if (descriptionLayout != null && titleLayout != null) {
669 | c.translate(0, titleLayout.getHeight() + TEXT_SPACING);
670 | descriptionPaint.setAlpha((int) (0.54f * textAlpha));
671 | descriptionLayout.draw(c);
672 | }
673 | }
674 | c.restoreToCount(saveCount);
675 |
676 | saveCount = c.save();
677 | {
678 | if (tintedTarget != null) {
679 | c.translate(targetBounds.centerX() - tintedTarget.getWidth() / 2,
680 | targetBounds.centerY() - tintedTarget.getHeight() / 2);
681 | c.drawBitmap(tintedTarget, 0, 0, targetCirclePaint);
682 | } else if (target.icon != null) {
683 | c.translate(targetBounds.centerX() - target.icon.getBounds().width() / 2,
684 | targetBounds.centerY() - target.icon.getBounds().height() / 2);
685 | target.icon.setAlpha(targetCirclePaint.getAlpha());
686 | target.icon.draw(c);
687 | }
688 | }
689 | c.restoreToCount(saveCount);
690 |
691 | if (debug) {
692 | drawDebugInformation(c);
693 | }
694 | }
695 |
696 | @Override
697 | public boolean onTouchEvent(MotionEvent e) {
698 | lastTouchX = e.getX();
699 | lastTouchY = e.getY();
700 | return super.onTouchEvent(e);
701 | }
702 |
703 | @Override
704 | public boolean onKeyDown(int keyCode, KeyEvent event) {
705 | if (isVisible() && cancelable && keyCode == KeyEvent.KEYCODE_BACK) {
706 | event.startTracking();
707 | return true;
708 | }
709 |
710 | return false;
711 | }
712 |
713 | @Override
714 | public boolean onKeyUp(int keyCode, KeyEvent event) {
715 | if (isVisible() && isInteractable && cancelable
716 | && keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
717 | isInteractable = false;
718 |
719 | if (listener != null) {
720 | listener.onTargetCancel(this);
721 | } else {
722 | new Listener().onTargetCancel(this);
723 | }
724 |
725 | return true;
726 | }
727 |
728 | return false;
729 | }
730 |
731 | /**
732 | * Dismiss this view
733 | * @param tappedTarget If the user tapped the target or not
734 | * (results in different dismiss animations)
735 | */
736 | public void dismiss(boolean tappedTarget) {
737 | pulseAnimation.cancel();
738 | expandAnimation.cancel();
739 | if (tappedTarget) {
740 | dismissConfirmAnimation.start();
741 | } else {
742 | dismissAnimation.start();
743 | }
744 | }
745 |
746 | /** Specify whether to draw a wireframe around the view, useful for debugging **/
747 | public void setDrawDebug(boolean status) {
748 | if (debug != status) {
749 | debug = status;
750 | postInvalidate();
751 | }
752 | }
753 |
754 | /** Returns whether this view is visible or not **/
755 | public boolean isVisible() {
756 | return !isDismissed && visible;
757 | }
758 |
759 | void drawJitteredShadow(Canvas c) {
760 | final float baseAlpha = 0.20f * outerCircleAlpha;
761 | outerCircleShadowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
762 | outerCircleShadowPaint.setAlpha((int) baseAlpha);
763 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM, outerCircleRadius, outerCircleShadowPaint);
764 | outerCircleShadowPaint.setStyle(Paint.Style.STROKE);
765 | final int numJitters = 7;
766 | for (int i = numJitters - 1; i > 0; --i) {
767 | outerCircleShadowPaint.setAlpha((int) ((i / (float) numJitters) * baseAlpha));
768 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1] + SHADOW_DIM ,
769 | outerCircleRadius + (numJitters - i) * SHADOW_JITTER_DIM , outerCircleShadowPaint);
770 | }
771 | }
772 |
773 | void drawDebugInformation(Canvas c) {
774 | if (debugPaint == null) {
775 | debugPaint = new Paint();
776 | debugPaint.setARGB(255, 255, 0, 0);
777 | debugPaint.setStyle(Paint.Style.STROKE);
778 | debugPaint.setStrokeWidth(UiUtil.dp(getContext(), 1));
779 | }
780 |
781 | if (debugTextPaint == null) {
782 | debugTextPaint = new TextPaint();
783 | debugTextPaint.setColor(0xFFFF0000);
784 | debugTextPaint.setTextSize(UiUtil.sp(getContext(), 16));
785 | }
786 |
787 | // Draw wireframe
788 | debugPaint.setStyle(Paint.Style.STROKE);
789 | c.drawRect(textBounds, debugPaint);
790 | c.drawRect(targetBounds, debugPaint);
791 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], 10, debugPaint);
792 | c.drawCircle(outerCircleCenter[0], outerCircleCenter[1], calculatedOuterCircleRadius - CIRCLE_PADDING, debugPaint);
793 | c.drawCircle(targetBounds.centerX(), targetBounds.centerY(), TARGET_RADIUS + TARGET_PADDING, debugPaint);
794 |
795 | // Draw positions and dimensions
796 | debugPaint.setStyle(Paint.Style.FILL);
797 | final String debugText =
798 | "Text bounds: " + textBounds.toShortString() + "\n" +
799 | "Target bounds: " + targetBounds.toShortString() + "\n" +
800 | "Center: " + outerCircleCenter[0] + " " + outerCircleCenter[1] + "\n" +
801 | "View size: " + getWidth() + " " + getHeight() + "\n" +
802 | "Target bounds: " + targetBounds.toShortString();
803 |
804 | if (debugStringBuilder == null) {
805 | debugStringBuilder = new SpannableStringBuilder(debugText);
806 | } else {
807 | debugStringBuilder.clear();
808 | debugStringBuilder.append(debugText);
809 | }
810 |
811 | if (debugLayout == null) {
812 | debugLayout = new DynamicLayout(debugText, debugTextPaint, getWidth(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
813 | }
814 |
815 | final int saveCount = c.save();
816 | {
817 | debugPaint.setARGB(220, 0, 0, 0);
818 | c.translate(0.0f, topBoundary);
819 | c.drawRect(0.0f, 0.0f, debugLayout.getWidth(), debugLayout.getHeight(), debugPaint);
820 | debugPaint.setARGB(255, 255, 0, 0);
821 | debugLayout.draw(c);
822 | }
823 | c.restoreToCount(saveCount);
824 | }
825 |
826 | void drawTintedTarget() {
827 | final Drawable icon = target.icon;
828 | if (!shouldTintTarget || icon == null) {
829 | tintedTarget = null;
830 | return;
831 | }
832 |
833 | if (tintedTarget != null) return;
834 |
835 | tintedTarget = Bitmap.createBitmap(icon.getIntrinsicWidth(), icon.getIntrinsicHeight(),
836 | Bitmap.Config.ARGB_8888);
837 | final Canvas canvas = new Canvas(tintedTarget);
838 | icon.setColorFilter(new PorterDuffColorFilter(
839 | outerCirclePaint.getColor(), PorterDuff.Mode.SRC_ATOP));
840 | icon.draw(canvas);
841 | icon.setColorFilter(null);
842 | }
843 |
844 | void updateTextLayouts() {
845 | final int textWidth = Math.min(getWidth(), TEXT_MAX_WIDTH) - TEXT_PADDING * 2;
846 | if (textWidth <= 0) {
847 | return;
848 | }
849 |
850 | titleLayout = new StaticLayout(title, titlePaint, textWidth,
851 | Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
852 |
853 | if (description != null) {
854 | descriptionLayout = new StaticLayout(description, descriptionPaint, textWidth,
855 | Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
856 | } else {
857 | descriptionLayout = null;
858 | }
859 | }
860 |
861 | float halfwayLerp(float lerp) {
862 | if (lerp < 0.5f) {
863 | return lerp / 0.5f;
864 | }
865 |
866 | return (1.0f - lerp) / 0.5f;
867 | }
868 |
869 | float delayedLerp(float lerp, float threshold) {
870 | if (lerp < threshold) {
871 | return 0.0f;
872 | }
873 |
874 | return (lerp - threshold) / (1.0f - threshold);
875 | }
876 |
877 | void calculateDimensions() {
878 | textBounds = getTextBounds();
879 | outerCircleCenter = getOuterCircleCenterPoint();
880 | calculatedOuterCircleRadius = getOuterCircleRadius(outerCircleCenter[0], outerCircleCenter[1], textBounds, targetBounds);
881 | }
882 |
883 | void calculateDrawingBounds() {
884 | drawingBounds.left = (int) Math.max(0, outerCircleCenter[0] - outerCircleRadius);
885 | drawingBounds.top = (int) Math.min(0, outerCircleCenter[1] - outerCircleRadius);
886 | drawingBounds.right = (int) Math.min(getWidth(),
887 | outerCircleCenter[0] + outerCircleRadius + CIRCLE_PADDING);
888 | drawingBounds.bottom = (int) Math.min(getHeight(),
889 | outerCircleCenter[1] + outerCircleRadius + CIRCLE_PADDING);
890 | }
891 |
892 | int getOuterCircleRadius(int centerX, int centerY, Rect textBounds, Rect targetBounds) {
893 | final int targetCenterX = targetBounds.centerX();
894 | final int targetCenterY = targetBounds.centerY();
895 | final int expandedRadius = (int) (1.1f * TARGET_RADIUS);
896 | final Rect expandedBounds = new Rect(targetCenterX, targetCenterY, targetCenterX, targetCenterY);
897 | expandedBounds.inset(-expandedRadius, -expandedRadius);
898 |
899 | final int textRadius = maxDistanceToPoints(centerX, centerY, textBounds);
900 | final int targetRadius = maxDistanceToPoints(centerX, centerY, expandedBounds);
901 | return Math.max(textRadius, targetRadius) + CIRCLE_PADDING;
902 | }
903 |
904 | Rect getTextBounds() {
905 | final int totalTextHeight = getTotalTextHeight();
906 | final int totalTextWidth = getTotalTextWidth();
907 |
908 | final int possibleTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight;
909 | final int top;
910 | if (possibleTop > topBoundary) {
911 | top = possibleTop;
912 | } else {
913 | top = targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING;
914 | }
915 |
916 | final int relativeCenterDistance = (getWidth() / 2) - targetBounds.centerX();
917 | final int bias = relativeCenterDistance < 0 ? -TEXT_POSITIONING_BIAS : TEXT_POSITIONING_BIAS;
918 | final int left = Math.max(TEXT_PADDING, targetBounds.centerX() - bias - totalTextWidth);
919 | final int right = Math.min(getWidth() - TEXT_PADDING, left + totalTextWidth);
920 | return new Rect(left, top, right, top + totalTextHeight);
921 | }
922 |
923 | int[] getOuterCircleCenterPoint() {
924 | if (inGutter(targetBounds.centerY())) {
925 | return new int[]{targetBounds.centerX(), targetBounds.centerY()};
926 | }
927 |
928 | final int targetRadius = Math.max(targetBounds.width(), targetBounds.height()) / 2 + TARGET_PADDING;
929 | final int totalTextHeight = getTotalTextHeight();
930 |
931 | final boolean onTop = targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight > 0;
932 |
933 | final int left = Math.min(textBounds.left, targetBounds.left - targetRadius);
934 | final int right = Math.max(textBounds.right, targetBounds.right + targetRadius);
935 | final int titleHeight = titleLayout == null ? 0 : titleLayout.getHeight();
936 | final int centerY = onTop ?
937 | targetBounds.centerY() - TARGET_RADIUS - TARGET_PADDING - totalTextHeight + titleHeight
938 | :
939 | targetBounds.centerY() + TARGET_RADIUS + TARGET_PADDING + titleHeight;
940 |
941 | return new int[] { (left + right) / 2, centerY };
942 | }
943 |
944 | int getTotalTextHeight() {
945 | if (titleLayout == null) {
946 | return 0;
947 | }
948 |
949 | if (descriptionLayout == null) {
950 | return titleLayout.getHeight() + TEXT_SPACING;
951 | }
952 |
953 | return titleLayout.getHeight() + descriptionLayout.getHeight() + TEXT_SPACING;
954 | }
955 |
956 | int getTotalTextWidth() {
957 | if (titleLayout == null) {
958 | return 0;
959 | }
960 |
961 | if (descriptionLayout == null) {
962 | return titleLayout.getWidth();
963 | }
964 |
965 | return Math.max(titleLayout.getWidth(), descriptionLayout.getWidth());
966 | }
967 |
968 | boolean inGutter(int y) {
969 | if (bottomBoundary > 0) {
970 | return y < GUTTER_DIM || y > bottomBoundary - GUTTER_DIM;
971 | } else {
972 | return y < GUTTER_DIM || y > getHeight() - GUTTER_DIM;
973 | }
974 | }
975 |
976 | int maxDistanceToPoints(int x1, int y1, Rect bounds) {
977 | final double tl = distance(x1, y1, bounds.left, bounds.top);
978 | final double tr = distance(x1, y1, bounds.right, bounds.top);
979 | final double bl = distance(x1, y1, bounds.left, bounds.bottom);
980 | final double br = distance(x1, y1, bounds.right, bounds.bottom);
981 | return (int) Math.max(tl, Math.max(tr, Math.max(bl, br)));
982 | }
983 |
984 | double distance(int x1, int y1, int x2, int y2) {
985 | return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
986 | }
987 |
988 | void invalidateViewAndOutline(Rect bounds) {
989 | invalidate(bounds);
990 | if (outlineProvider != null && Build.VERSION.SDK_INT >= 21) {
991 | invalidateOutline();
992 | }
993 | }
994 | }
995 |
--------------------------------------------------------------------------------
2 |
8 |
9 | [ ](https://bintray.com/keepsafesoftware/Android/TapTargetView/_latestVersion)
10 |
11 |
12 | An implementation of tap targets from [Google's Material Design guidelines on feature discovery](https://material.google.com/growth-communications/feature-discovery.html#feature-discovery-design).
13 |
14 | **Min SDK:** 14
15 |
16 | ## Installation
17 |
18 | TapTargetView is distributed using [jcenter](https://bintray.com/keepsafesoftware/Android/TapTargetView/view).
19 |
20 | ```groovy
21 | repositories {
22 | jcenter()
23 | }
24 |
25 | dependencies {
26 | compile 'com.getkeepsafe.taptargetview:taptargetview:1.9.1'
27 | }
28 | ```
29 |
30 | If you wish to use a snapshot, please follow the instructions [here](https://jitpack.io/#KeepSafe/TapTargetView/-SNAPSHOT)
31 |
32 | ## Usage
33 |
34 | ### Simple usage
35 |
36 | ```java
37 | TapTargetView.showFor(this, // `this` is an Activity
38 | TapTarget.forView(findViewById(R.id.target), "This is a target", "We have the best targets, believe me")
39 | // All options below are optional
40 | .outerCircleColor(R.color.red) // Specify a color for the outer circle
41 | .outerCircleAlpha(0.96f) // Specify the alpha amount for the outer circle
42 | .targetCircleColor(R.color.white) // Specify a color for the target circle
43 | .titleTextSize(20) // Specify the size (in sp) of the title text
44 | .titleTextColor(R.color.white) // Specify the color of the title text
45 | .descriptionTextSize(10) // Specify the size (in sp) of the description text
46 | .descriptionTextColor(R.color.red) // Specify the color of the description text
47 | .textColor(R.color.blue) // Specify a color for both the title and description text
48 | .textTypeface(Typeface.SANS_SERIF) // Specify a typeface for the text
49 | .dimColor(R.color.black) // If set, will dim behind the view with 30% opacity of the given color
50 | .drawShadow(true) // Whether to draw a drop shadow or not
51 | .cancelable(false) // Whether tapping outside the outer circle dismisses the view
52 | .tintTarget(true) // Whether to tint the target view's color
53 | .transparentTarget(false) // Specify whether the target is transparent (displays the content underneath)
54 | .icon(Drawable) // Specify a custom drawable to draw as the target
55 | .targetRadius(60), // Specify the target radius (in dp)
56 | new TapTargetView.Listener() { // The listener can listen for regular clicks, long clicks or cancels
57 | @Override
58 | public void onTargetClick(TapTargetView view) {
59 | super.onTargetClick(view); // This call is optional
60 | doSomething();
61 | }
62 | });
63 | ```
64 |
65 | You may also choose to target your own custom `Rect` with `TapTarget.forBounds(Rect, ...)`
66 |
67 | Additionally, each color can be specified via a `@ColorRes` or a `@ColorInt`. Functions that have the suffix `Int` take a `@ColorInt`.
68 |
69 | ### Sequences
70 |
71 | You can easily create a sequence of tap targets with `TapTargetSequence`:
72 |
73 | ```java
74 | new TapTargetSequence(this)
75 | .targets(
76 | TapTarget.forView(findViewById(R.id.never), "Gonna"),
77 | TapTarget.forView(findViewById(R.id.give), "You", "Up")
78 | .dimColor(android.R.color.never)
79 | .outerCircleColor(R.color.gonna)
80 | .targetCircleColor(R.color.let)
81 | .textColor(android.R.color.you),
82 | TapTarget.forBounds(rickTarget, "Down", ":^)")
83 | .cancelable(false)
84 | .icon(rick))
85 | .listener(new TapTargetSequence.Listener() {
86 | // This listener will tell us when interesting(tm) events happen in regards
87 | // to the sequence
88 | @Override
89 | public void onSequenceFinish() {
90 | // Yay
91 | }
92 |
93 | @Override
94 | public void onSequenceStep(TapTarget lastTarget) {
95 | // Perfom action for the current target
96 | }
97 |
98 | @Override
99 | public void onSequenceCanceled(TapTarget lastTarget) {
100 | // Boo
101 | }
102 | });
103 | ```
104 |
105 | A sequence is started via a call to `start()` on the `TapTargetSequence` instance
106 |
107 | For more examples of usage, please look at the included sample app.
108 |
109 | ## License
110 |
111 | Copyright 2016 Keepsafe Software Inc.
112 |
113 | Licensed under the Apache License, Version 2.0 (the "License");
114 | you may not use this file except in compliance with the License.
115 | You may obtain a copy of the License at
116 |
117 | http://www.apache.org/licenses/LICENSE-2.0
118 |
119 | Unless required by applicable law or agreed to in writing, software
120 | distributed under the License is distributed on an "AS IS" BASIS,
121 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
122 | See the License for the specific language governing permissions and
123 | limitations under the License.
124 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn ( ) {
37 | echo "$*"
38 | }
39 |
40 | die ( ) {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save ( ) {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/app/src/main/java/com/getkeepsafe/taptargetviewsample/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.getkeepsafe.taptargetviewsample;
2 |
3 | import android.content.DialogInterface;
4 | import android.graphics.Rect;
5 | import android.graphics.Typeface;
6 | import android.graphics.drawable.Drawable;
7 | import android.os.Bundle;
8 | import android.support.v4.content.ContextCompat;
9 | import android.support.v7.app.AlertDialog;
10 | import android.support.v7.app.AppCompatActivity;
11 | import android.support.v7.widget.Toolbar;
12 | import android.text.SpannableString;
13 | import android.text.style.StyleSpan;
14 | import android.text.style.UnderlineSpan;
15 | import android.util.Log;
16 | import android.view.Display;
17 | import android.widget.TextView;
18 | import android.widget.Toast;
19 |
20 | import com.getkeepsafe.taptargetview.TapTarget;
21 | import com.getkeepsafe.taptargetview.TapTargetSequence;
22 | import com.getkeepsafe.taptargetview.TapTargetView;
23 |
24 | public class MainActivity extends AppCompatActivity {
25 | @Override
26 | protected void onCreate(Bundle savedInstanceState) {
27 | super.onCreate(savedInstanceState);
28 | setContentView(R.layout.activity_main);
29 |
30 | final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
31 | toolbar.inflateMenu(R.menu.menu_main);
32 | toolbar.setNavigationIcon(ContextCompat.getDrawable(this, R.drawable.ic_arrow_back_white_24dp));
33 |
34 | // We load a drawable and create a location to show a tap target here
35 | // We need the display to get the width and height at this point in time
36 | final Display display = getWindowManager().getDefaultDisplay();
37 | // Load our little droid guy
38 | final Drawable droid = ContextCompat.getDrawable(this, R.drawable.ic_android_black_24dp);
39 | // Tell our droid buddy where we want him to appear
40 | final Rect droidTarget = new Rect(0, 0, droid.getIntrinsicWidth() * 2, droid.getIntrinsicHeight() * 2);
41 | // Using deprecated methods makes you look way cool
42 | droidTarget.offset(display.getWidth() / 2, display.getHeight() / 2);
43 |
44 | final SpannableString sassyDesc = new SpannableString("It allows you to go back, sometimes");
45 | sassyDesc.setSpan(new StyleSpan(Typeface.ITALIC), sassyDesc.length() - "somtimes".length(), sassyDesc.length(), 0);
46 |
47 | // We have a sequence of targets, so lets build it!
48 | final TapTargetSequence sequence = new TapTargetSequence(this)
49 | .targets(
50 | // This tap target will target the back button, we just need to pass its containing toolbar
51 | TapTarget.forToolbarNavigationIcon(toolbar, "This is the back button", sassyDesc).id(1),
52 | // Likewise, this tap target will target the search button
53 | TapTarget.forToolbarMenuItem(toolbar, R.id.search, "This is a search icon", "As you can see, it has gotten pretty dark around here...")
54 | .dimColor(android.R.color.black)
55 | .outerCircleColor(R.color.colorAccent)
56 | .targetCircleColor(android.R.color.black)
57 | .transparentTarget(true)
58 | .textColor(android.R.color.black)
59 | .id(2),
60 | // You can also target the overflow button in your toolbar
61 | TapTarget.forToolbarOverflow(toolbar, "This will show more options", "But they're not useful :(").id(3),
62 | // This tap target will target our droid buddy at the given target rect
63 | TapTarget.forBounds(droidTarget, "Oh look!", "You can point to any part of the screen. You also can't cancel this one!")
64 | .cancelable(false)
65 | .icon(droid)
66 | .id(4)
67 | )
68 | .listener(new TapTargetSequence.Listener() {
69 | // This listener will tell us when interesting(tm) events happen in regards
70 | // to the sequence
71 | @Override
72 | public void onSequenceFinish() {
73 | ((TextView) findViewById(R.id.educated)).setText("Congratulations! You're educated now!");
74 | }
75 |
76 | @Override
77 | public void onSequenceStep(TapTarget lastTarget, boolean targetClicked) {
78 | Log.d("TapTargetView", "Clicked on " + lastTarget.id());
79 | }
80 |
81 | @Override
82 | public void onSequenceCanceled(TapTarget lastTarget) {
83 | final AlertDialog dialog = new AlertDialog.Builder(MainActivity.this)
84 | .setTitle("Uh oh")
85 | .setMessage("You canceled the sequence")
86 | .setPositiveButton("Oops", null).show();
87 | TapTargetView.showFor(dialog,
88 | TapTarget.forView(dialog.getButton(DialogInterface.BUTTON_POSITIVE), "Uh oh!", "You canceled the sequence at step " + lastTarget.id())
89 | .cancelable(false)
90 | .tintTarget(false), new TapTargetView.Listener() {
91 | @Override
92 | public void onTargetClick(TapTargetView view) {
93 | super.onTargetClick(view);
94 | dialog.dismiss();
95 | }
96 | });
97 | }
98 | });
99 |
100 | // You don't always need a sequence, and for that there's a single time tap target
101 | final SpannableString spannedDesc = new SpannableString("This is the sample app for TapTargetView");
102 | spannedDesc.setSpan(new UnderlineSpan(), spannedDesc.length() - "TapTargetView".length(), spannedDesc.length(), 0);
103 | TapTargetView.showFor(this, TapTarget.forView(findViewById(R.id.fab), "Hello, world!", spannedDesc)
104 | .cancelable(false)
105 | .drawShadow(true)
106 | .titleTextDimen(R.dimen.title_text_size)
107 | .tintTarget(false), new TapTargetView.Listener() {
108 | @Override
109 | public void onTargetClick(TapTargetView view) {
110 | super.onTargetClick(view);
111 | // .. which evidently starts the sequence we defined earlier
112 | sequence.start();
113 | }
114 |
115 | @Override
116 | public void onOuterCircleClick(TapTargetView view) {
117 | super.onOuterCircleClick(view);
118 | Toast.makeText(view.getContext(), "You clicked the outer circle!", Toast.LENGTH_SHORT).show();
119 | }
120 |
121 | @Override
122 | public void onTargetDismissed(TapTargetView view, boolean userInitiated) {
123 | Log.d("TapTargetViewSample", "You dismissed me :(");
124 | }
125 | });
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/taptargetview/src/main/java/com/getkeepsafe/taptargetview/ToolbarTapTarget.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2016 Keepsafe Software, Inc.
3 | *
3 |
4 | 
5 |
6 | TapTargetView
7 |
376 | * (0, 0, intrinsic-width, intrinsic-height)
377 | */
378 | public TapTarget icon(Drawable icon, boolean hasSetBounds) {
379 | if (icon == null) throw new IllegalArgumentException("Cannot use null drawable");
380 | this.icon = icon;
381 |
382 | if (!hasSetBounds) {
383 | this.icon.setBounds(new Rect(0, 0, this.icon.getIntrinsicWidth(), this.icon.getIntrinsicHeight()));
384 | }
385 |
386 | return this;
387 | }
388 |
389 | /** Specify a unique identifier for this target. **/
390 | public TapTarget id(int id) {
391 | this.id = id;
392 | return this;
393 | }
394 |
395 | /** Specify the target radius in dp. **/
396 | public TapTarget targetRadius(int targetRadius) {
397 | this.targetRadius = targetRadius;
398 | return this;
399 | }
400 |
401 |
402 | /** Return the id associated with this tap target **/
403 | public int id() {
404 | return id;
405 | }
406 |
407 | /**
408 | * In case your target needs time to be ready (laid out in your view, not created, etc), the
409 | * runnable passed here will be invoked when the target is ready.
410 | */
411 | public void onReady(Runnable runnable) {
412 | runnable.run();
413 | }
414 |
415 | /**
416 | * Returns the target bounds. Throws an exception if they are not set
417 | * (target may not be ready)
418 | *