├── BubbleIntroHelper
├── .classpath
├── .project
├── AndroidManifest.xml
├── ic_launcher-web.png
├── libs
│ ├── android-support-v13.jar
│ └── android-support-v7-appcompat.jar
├── lint.xml
├── proguard-project.txt
├── project.properties
├── res
│ ├── anim
│ │ ├── scale_off.xml
│ │ └── scale_on.xml
│ ├── drawable-hdpi
│ │ ├── ic_launcher.png
│ │ └── tile.png
│ ├── drawable-mdpi
│ │ └── ic_launcher.png
│ ├── drawable-xhdpi
│ │ └── ic_launcher.png
│ ├── drawable-xxhdpi
│ │ └── ic_launcher.png
│ ├── drawable
│ │ └── tile_drawable.xml
│ ├── layout
│ │ ├── activity_main.xml
│ │ └── help_popup_window.xml
│ ├── menu
│ │ └── main.xml
│ ├── values-sw600dp
│ │ └── dimens.xml
│ ├── values-sw720dp-land
│ │ └── dimens.xml
│ └── values
│ │ ├── attrs.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── styles.xml
└── src
│ └── com
│ └── example
│ ├── bubbleEngine
│ ├── AlertDialogFragment.java
│ ├── BubbleRelativeLayout.java
│ ├── HelpPopupManager.java
│ └── HelpPopupWindow.java
│ └── bubbleintrohelper
│ ├── MainActivity.java
│ └── MyScrollView.java
└── README.md
/BubbleIntroHelper/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | BubbleIntroHelper
4 |
5 |
6 |
7 |
8 |
9 | com.android.ide.eclipse.adt.ResourceManagerBuilder
10 |
11 |
12 |
13 |
14 | com.android.ide.eclipse.adt.PreCompilerBuilder
15 |
16 |
17 |
18 |
19 | org.eclipse.jdt.core.javabuilder
20 |
21 |
22 |
23 |
24 | com.android.ide.eclipse.adt.ApkBuilder
25 |
26 |
27 |
28 |
29 |
30 | com.android.ide.eclipse.adt.AndroidNature
31 | org.eclipse.jdt.core.javanature
32 |
33 |
34 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/ic_launcher-web.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/libs/android-support-v13.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/libs/android-support-v13.jar
--------------------------------------------------------------------------------
/BubbleIntroHelper/libs/android-support-v7-appcompat.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/libs/android-support-v7-appcompat.jar
--------------------------------------------------------------------------------
/BubbleIntroHelper/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-19
15 | android.library.reference.1=../../../Android/SDKs/sdk/extras/android/support/v7/appcompat
16 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/anim/scale_off.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/anim/scale_on.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
13 |
14 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable-hdpi/tile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/res/drawable-hdpi/tile.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/theredsunrise/BubbleIntroHelper/e623c027209151e3e2ec88a71dda53e361b364ef/BubbleIntroHelper/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/drawable/tile_drawable.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
18 |
19 |
27 |
28 |
36 |
37 |
47 |
48 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/layout/help_popup_window.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
27 |
28 |
35 |
36 |
50 |
51 |
61 |
62 |
76 |
77 |
78 |
79 |
96 |
97 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values-sw600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values-sw720dp-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | 128dp
8 |
9 |
10 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 16dp
5 | 16dp
6 |
7 |
8 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | BubbleIntroHelper
5 | Settings
6 | Yes
7 | Cancel
8 | Ok
9 | Cancel Help
10 | Info:
11 | Are you sure to cancel a help wizard?
12 |
13 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
24 |
25 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleEngine/AlertDialogFragment.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleEngine;
2 |
3 | import android.app.AlertDialog;
4 | import android.app.Dialog;
5 | import android.content.DialogInterface;
6 | import android.os.Bundle;
7 | import android.support.v4.app.DialogFragment;
8 | import android.support.v4.app.Fragment;
9 | import android.support.v4.app.FragmentManager;
10 | import android.support.v4.app.FragmentTransaction;
11 | import android.text.util.Linkify;
12 | import android.view.Gravity;
13 | import android.widget.TextView;
14 |
15 | import com.example.bubbleintrohelper.R;
16 |
17 | public class AlertDialogFragment extends DialogFragment {
18 |
19 | public static interface AlertDialogFragmentListener {
20 | public void onPositiveClick();
21 |
22 | public void onNegativeClick();
23 | }
24 |
25 | private String mMessage = null;
26 | private int mTitleResId = -1;
27 | private int mMessageResId = -1;
28 |
29 | private AlertDialogFragmentListener mAlertDialogFragmentListener;
30 |
31 | public AlertDialogFragment(final int titleResID, final int messageResId, final AlertDialogFragmentListener alertDialogFragmentListener) {
32 | super();
33 | setRetainInstance(true);
34 |
35 | mTitleResId = titleResID;
36 | mMessageResId = messageResId;
37 | mAlertDialogFragmentListener = alertDialogFragmentListener;
38 |
39 | }
40 |
41 | public AlertDialogFragment(final int titleResID, final String message) {
42 | super();
43 | setRetainInstance(true);
44 |
45 | mTitleResId = titleResID;
46 | mMessage = message;
47 |
48 | }
49 |
50 | public AlertDialogFragment(final int titleResID, final int messageResId) {
51 | super();
52 | setRetainInstance(true);
53 |
54 | mTitleResId = titleResID;
55 | mMessageResId = messageResId;
56 |
57 | }
58 |
59 | public AlertDialogFragment() {
60 | super();
61 | setRetainInstance(true);
62 | }
63 |
64 | public void showDialog(final FragmentManager fragmentManager) {
65 |
66 | Fragment prev = fragmentManager.findFragmentByTag("alertDialog");
67 | FragmentTransaction ft = fragmentManager.beginTransaction();
68 |
69 | if (prev != null) {
70 | ft.remove(prev);
71 | }
72 |
73 | ft.addToBackStack(null);
74 | show(ft, "alertDialog");
75 | }
76 |
77 | @Override
78 | public Dialog onCreateDialog(Bundle savedInstanceState) {
79 |
80 | final Dialog dialog;
81 |
82 | TextView txtContent = new TextView(getActivity());
83 | txtContent.setGravity(Gravity.CENTER);
84 | txtContent.setTextAppearance(getActivity(), R.style.TextAppearance_AppCompat_Base_CompactMenu_Dialog);
85 | txtContent.setText(mMessage != null ? mMessageResId : mMessageResId);
86 | Linkify.addLinks(txtContent, Linkify.ALL);
87 |
88 | if (mAlertDialogFragmentListener == null) {
89 | dialog = new AlertDialog.Builder(getActivity()).setIcon(R.drawable.ic_launcher).setTitle(mTitleResId).setView(txtContent).setPositiveButton(android.R.string.ok, null)
90 | .create();
91 |
92 | } else {
93 | dialog = new AlertDialog.Builder(getActivity()).setIcon(R.drawable.ic_launcher).setTitle(mTitleResId).setView(txtContent)
94 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
95 | public void onClick(DialogInterface dialog, int whichButton) {
96 | mAlertDialogFragmentListener.onPositiveClick();
97 | }
98 | }).setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
99 | public void onClick(DialogInterface dialog, int whichButton) {
100 | mAlertDialogFragmentListener.onNegativeClick();
101 | }
102 | }).create();
103 | }
104 |
105 | return dialog;
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleEngine/BubbleRelativeLayout.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleEngine;
2 |
3 | import android.content.Context;
4 | import android.content.res.Configuration;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Canvas;
7 | import android.graphics.Color;
8 | import android.graphics.CornerPathEffect;
9 | import android.graphics.LinearGradient;
10 | import android.graphics.Matrix;
11 | import android.graphics.Paint;
12 | import android.graphics.Paint.Cap;
13 | import android.graphics.Paint.Style;
14 | import android.graphics.Path;
15 | import android.graphics.Path.Direction;
16 | import android.graphics.RectF;
17 | import android.graphics.Shader.TileMode;
18 | import android.os.Build;
19 | import android.util.AttributeSet;
20 | import android.util.DisplayMetrics;
21 | import android.widget.RelativeLayout;
22 |
23 | import com.example.bubbleintrohelper.R;
24 |
25 | public class BubbleRelativeLayout extends RelativeLayout {
26 |
27 | public static interface BubbleLayoutChangeListener {
28 | void onConfigurationChanged(final Configuration newConfig);
29 | }
30 |
31 | public static enum BubbleLegOrientation {
32 | TOP, LEFT, RIGHT, BOTTOM, NONE
33 | };
34 |
35 | public static float PADDING = 30;
36 | public static float LEG_HALF_BASE = 30;
37 | public static float STROKE_WIDTH = 2f;
38 | public static float CORNER_RADIUS = 8f;
39 | public static int SHADOW_COLOR = Color.argb(100, 0, 0, 0);
40 | public static float MIN_LEG_DISTANCE = PADDING + LEG_HALF_BASE;
41 |
42 | private Paint mFillPaint = null;
43 | private final Path mPath = new Path();
44 | private final Path mBubbleLegPrototype = new Path();
45 | private final Paint mPaint = new Paint(Paint.DITHER_FLAG);
46 | private BubbleLayoutChangeListener mBubbleLayoutChangeListener = null;
47 |
48 | private float mBubbleLegOffset = 0.75f;
49 | private BubbleLegOrientation mBubbleOrientation = BubbleLegOrientation.LEFT;
50 |
51 | public BubbleRelativeLayout(Context context) {
52 | this(context, null);
53 | }
54 |
55 | public BubbleRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
56 | super(context, attrs, defStyle);
57 | init(context, attrs);
58 | }
59 |
60 | public BubbleRelativeLayout(Context context, AttributeSet attrs) {
61 | this(context, attrs, 0);
62 | }
63 |
64 | public void setBubbleLayoutChangeListener(final BubbleLayoutChangeListener bubbleLayoutChangeListener) {
65 | mBubbleLayoutChangeListener = bubbleLayoutChangeListener;
66 | }
67 |
68 | private int dpToPx(float dp) {
69 | DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
70 | return (int) ((dp * displayMetrics.density) + 0.5);
71 | }
72 |
73 | private void init(final Context context, final AttributeSet attrs) {
74 |
75 | if (attrs != null) {
76 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.bubble_intro_helper);
77 |
78 | try {
79 |
80 | PADDING = typedArray.getDimensionPixelSize(R.styleable.bubble_intro_helper_padding, dpToPx(18));
81 | SHADOW_COLOR = typedArray.getInt(R.styleable.bubble_intro_helper_shadowColor, Color.argb(100, 0, 0, 0));
82 | LEG_HALF_BASE = typedArray.getDimensionPixelSize(R.styleable.bubble_intro_helper_halfBaseOfLeg, dpToPx(18));
83 | MIN_LEG_DISTANCE = PADDING + LEG_HALF_BASE;
84 | STROKE_WIDTH = typedArray.getFloat(R.styleable.bubble_intro_helper_strokeWidth, 2f);
85 | CORNER_RADIUS = typedArray.getFloat(R.styleable.bubble_intro_helper_cornerRadius, 8f);
86 |
87 | } finally {
88 |
89 | if (typedArray != null) {
90 | typedArray.recycle();
91 | }
92 | }
93 | }
94 |
95 | mPaint.setColor(SHADOW_COLOR);
96 | mPaint.setStyle(Style.FILL);
97 | mPaint.setStrokeCap(Cap.BUTT);
98 | mPaint.setAntiAlias(true);
99 | mPaint.setStrokeWidth(STROKE_WIDTH);
100 | mPaint.setStrokeJoin(Paint.Join.MITER);
101 | mPaint.setPathEffect(new CornerPathEffect(CORNER_RADIUS));
102 |
103 | if (Build.VERSION.SDK_INT >= 11) {
104 | setLayerType(LAYER_TYPE_SOFTWARE, mPaint);
105 | }
106 |
107 | mFillPaint = new Paint(mPaint);
108 | mFillPaint.setColor(Color.WHITE);
109 | mFillPaint.setShader(new LinearGradient(100f, 0f, 100f, 200f, Color.WHITE, Color.WHITE, TileMode.CLAMP));
110 |
111 | if (Build.VERSION.SDK_INT >= 11) {
112 | setLayerType(LAYER_TYPE_SOFTWARE, mFillPaint);
113 | }
114 | mPaint.setShadowLayer(2f, 2F, 5F, SHADOW_COLOR);
115 |
116 | renderBubbleLegPrototype();
117 |
118 | setPadding((int) PADDING, (int) PADDING, (int) PADDING, (int) PADDING);
119 |
120 | }
121 |
122 | @Override
123 | protected void onConfigurationChanged(Configuration newConfig) {
124 | super.onConfigurationChanged(newConfig);
125 |
126 | if (mBubbleLayoutChangeListener != null) {
127 | mBubbleLayoutChangeListener.onConfigurationChanged(newConfig);
128 | }
129 |
130 | }
131 |
132 | private void renderBubbleLegPrototype() {
133 |
134 | mBubbleLegPrototype.moveTo(0, 0);
135 | mBubbleLegPrototype.lineTo(PADDING * 3, -PADDING);
136 | mBubbleLegPrototype.lineTo(PADDING * 3, PADDING);
137 | mBubbleLegPrototype.close();
138 |
139 | }
140 |
141 | public void setBubbleParams(final BubbleLegOrientation bubbleOrientation, final float bubbleOffset) {
142 |
143 | mBubbleLegOffset = bubbleOffset;
144 | mBubbleOrientation = bubbleOrientation;
145 |
146 | }
147 |
148 | private Matrix renderBubbleLegMatrix(final float width, final float height) {
149 |
150 | final float offset = Math.max(mBubbleLegOffset, MIN_LEG_DISTANCE);
151 |
152 | float dstX = 0;
153 | float dstY = Math.min(offset, height - MIN_LEG_DISTANCE);
154 | ;
155 | final Matrix matrix = new Matrix();
156 |
157 | switch (mBubbleOrientation) {
158 |
159 | case TOP:
160 | dstX = Math.min(offset, width - MIN_LEG_DISTANCE);
161 | dstY = 0;
162 | matrix.postRotate(90);
163 | break;
164 |
165 | case RIGHT:
166 | dstX = width;
167 | dstY = Math.min(offset, height - MIN_LEG_DISTANCE);
168 | matrix.postRotate(180);
169 | break;
170 |
171 | case BOTTOM:
172 | dstX = Math.min(offset, width - MIN_LEG_DISTANCE);
173 | ;
174 | dstY = height;
175 | matrix.postRotate(270);
176 | break;
177 |
178 | }
179 |
180 | matrix.postTranslate(dstX, dstY);
181 | return matrix;
182 | }
183 |
184 | @Override
185 | protected void onDraw(Canvas canvas) {
186 |
187 | final float width = canvas.getWidth();
188 | final float height = canvas.getHeight();
189 |
190 | mPath.rewind();
191 | mPath.addRoundRect(new RectF(PADDING, PADDING, width - PADDING, height - PADDING), CORNER_RADIUS, CORNER_RADIUS, Direction.CW);
192 | mPath.addPath(mBubbleLegPrototype, renderBubbleLegMatrix(width, height));
193 |
194 | canvas.drawPath(mPath, mPaint);
195 | canvas.scale((width - STROKE_WIDTH) / width, (height - STROKE_WIDTH) / height, width / 2f, height / 2f);
196 |
197 | canvas.drawPath(mPath, mFillPaint);
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleEngine/HelpPopupManager.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleEngine;
2 |
3 | import java.util.Collections;
4 | import java.util.Iterator;
5 | import java.util.Map.Entry;
6 | import java.util.SortedMap;
7 | import java.util.TreeMap;
8 |
9 | import android.graphics.Rect;
10 | import android.os.Build;
11 | import android.os.Handler;
12 | import android.view.View;
13 | import android.view.ViewTreeObserver.OnGlobalLayoutListener;
14 | import android.widget.PopupWindow;
15 |
16 | public final class HelpPopupManager {
17 |
18 | public static int INVALID_ID_VALUE = 0;
19 | public static int INVALID_VERSION_CODE_VALUE = -1;
20 |
21 | private static boolean mIsEnabled = false;
22 | private static volatile HelpPopupWindow mCurrentHelpPopup = null;
23 |
24 | private static SortedMap mHelpPopups = Collections.synchronizedSortedMap(new TreeMap());
25 |
26 | private static OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
27 |
28 | @Override
29 | public void onGlobalLayout() {
30 |
31 | new Handler().postDelayed(new Runnable() {
32 |
33 | @Override
34 | public void run() {
35 | startShowing();
36 | }
37 | }, 500);
38 |
39 | }
40 | };
41 |
42 | private static PopupWindow.OnDismissListener mOnDismissListener = new PopupWindow.OnDismissListener() {
43 |
44 | @Override
45 | public void onDismiss() {
46 |
47 | removeCurrentPopup();
48 | startShowing();
49 | }
50 | };
51 |
52 | private static View.OnClickListener mOnNextClickListener = new View.OnClickListener() {
53 |
54 | @Override
55 | public void onClick(View v) {
56 |
57 | removeCurrentPopup();
58 | startShowing();
59 | }
60 | };
61 |
62 | private static View.OnClickListener mOnCancelClickListener = new View.OnClickListener() {
63 |
64 | @Override
65 | public void onClick(View v) {
66 |
67 | }
68 | };
69 |
70 | private static void removeCurrentPopup() {
71 |
72 | if (mCurrentHelpPopup != null) {
73 |
74 | mHelpPopups.remove(mCurrentHelpPopup.getID());
75 | mCurrentHelpPopup = null;
76 | }
77 |
78 | }
79 |
80 | /**
81 | * Assign popup window to the specific parent view. Once the window is shown it will not be shown in the future anymore.
82 | * To provide popup's showing again one must clear app's storage data.
83 | *
84 | *
85 | * @param forVersionCode App version code for which popup will be shown
86 | * @param ID An ID of a popup window
87 | * @param parent A parent view to which this popup window will belong
88 | * @param content Context
89 | * @return
90 | */
91 | public static synchronized HelpPopupWindow addHelpPopupWindow(final int forVersionCode, final int ID,
92 | final View parent, final String content) {
93 |
94 | if (ID == HelpPopupManager.INVALID_ID_VALUE) {
95 | return null;
96 |
97 | }
98 |
99 | HelpPopupWindow popupWindow = mHelpPopups.get(ID);
100 |
101 | if (popupWindow != null) {
102 |
103 | // reinitialize parent
104 | if (!popupWindow.getParent().equals(parent)) {
105 |
106 | parent.getViewTreeObserver().addOnGlobalLayoutListener(
107 | mOnGlobalLayoutListener);
108 | popupWindow.setParent(parent);
109 |
110 | }
111 |
112 | return popupWindow.isShownAlready(false) ? null : popupWindow;
113 |
114 | } else {
115 |
116 | parent.getViewTreeObserver().addOnGlobalLayoutListener(
117 | mOnGlobalLayoutListener);
118 | popupWindow = new HelpPopupWindow(forVersionCode, ID, parent, content);
119 |
120 | if (popupWindow.isShownAlready(false)) {
121 | return null;
122 | }
123 | }
124 |
125 | popupWindow.setOnNotifyCancelClick(mOnCancelClickListener);
126 | popupWindow.setOnNotifyNextClick(mOnNextClickListener);
127 | popupWindow.setOnNotifyDismissListener(mOnDismissListener);
128 |
129 | mHelpPopups.put(ID, popupWindow);
130 | return popupWindow;
131 | }
132 |
133 | public static void setEnabled(final boolean enabled) {
134 |
135 | mIsEnabled = enabled;
136 |
137 | }
138 |
139 | public static synchronized void startShowing() {
140 |
141 | if (!mIsEnabled || mCurrentHelpPopup != null) {
142 | return;
143 | }
144 |
145 | final Iterator> iterator = mHelpPopups
146 | .entrySet().iterator();
147 |
148 | while (iterator.hasNext()) {
149 |
150 | final HelpPopupWindow tempPopupWindow = (HelpPopupWindow) iterator
151 | .next().getValue();
152 |
153 | final Rect rootHitRect = new Rect();
154 | tempPopupWindow.getParent().getRootView().getHitRect(rootHitRect);
155 |
156 | if (tempPopupWindow.getParent().isShown()
157 | && tempPopupWindow.getParent().getLocalVisibleRect(
158 | rootHitRect)) {
159 |
160 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
161 |
162 | tempPopupWindow
163 | .getParent()
164 | .getViewTreeObserver()
165 | .removeOnGlobalLayoutListener(
166 | mOnGlobalLayoutListener);
167 |
168 | } else {
169 |
170 | tempPopupWindow
171 | .getParent()
172 | .getViewTreeObserver()
173 | .removeGlobalOnLayoutListener(
174 | mOnGlobalLayoutListener);
175 |
176 |
177 | }
178 |
179 | tempPopupWindow.show();
180 | mCurrentHelpPopup = tempPopupWindow;
181 |
182 | break;
183 | }
184 | }
185 | }
186 |
187 | }
188 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleEngine/HelpPopupWindow.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleEngine;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.content.SharedPreferences.Editor;
6 | import android.content.res.Configuration;
7 | import android.graphics.Rect;
8 | import android.graphics.drawable.ColorDrawable;
9 | import android.os.Handler;
10 | import android.support.v7.app.ActionBarActivity;
11 | import android.util.DisplayMetrics;
12 | import android.view.KeyEvent;
13 | import android.view.LayoutInflater;
14 | import android.view.MotionEvent;
15 | import android.view.View;
16 | import android.view.View.MeasureSpec;
17 | import android.view.WindowManager;
18 | import android.widget.Button;
19 | import android.widget.FrameLayout;
20 | import android.widget.PopupWindow;
21 | import android.widget.RelativeLayout;
22 | import android.widget.TextView;
23 |
24 | import com.example.bubbleEngine.AlertDialogFragment.AlertDialogFragmentListener;
25 | import com.example.bubbleEngine.BubbleRelativeLayout.BubbleLayoutChangeListener;
26 | import com.example.bubbleEngine.BubbleRelativeLayout.BubbleLegOrientation;
27 | import com.example.bubbleintrohelper.R;
28 |
29 | public class HelpPopupWindow extends PopupWindow implements BubbleLayoutChangeListener {
30 |
31 | private static final String PRIVATE_PREF = "WhereAmI";
32 | private static final String VERSION_KEY = "version_number";
33 |
34 | private float mTopPos = 0;
35 | private float mLeftPos = 0;
36 | private float mRightPos = 0;
37 | private float mBottomPos = 0;
38 | private float mXoffsetPercentage = 50f;
39 | private float mYoffsetPercentage = 50f;
40 |
41 | private View mParent = null;
42 | private Integer mID = HelpPopupManager.INVALID_ID_VALUE;
43 | private Integer mForVersionCode = HelpPopupManager.INVALID_ID_VALUE;
44 | private BubbleRelativeLayout mBubbleRelativeLayout = null;
45 |
46 | private View.OnClickListener mOnNotifyNextClick = null;
47 | private View.OnClickListener mOnNotifyCancelClick = null;
48 | private PopupWindow.OnDismissListener mOnNotifyDismissListener = null;
49 |
50 | private PopupWindow.OnDismissListener mOnDismissListener = new OnDismissListener() {
51 |
52 | @Override
53 | public void onDismiss() {
54 |
55 | if (mOnNotifyDismissListener != null) {
56 | mOnNotifyDismissListener.onDismiss();
57 | }
58 | }
59 | };
60 |
61 | @Override
62 | public int hashCode() {
63 | final int prime = 31;
64 | int result = 1;
65 | result = prime * result + ((mID == null) ? 0 : mID.hashCode());
66 | return result;
67 | }
68 |
69 | @Override
70 | public boolean equals(Object obj) {
71 | if (this == obj)
72 | return true;
73 | if (obj == null)
74 | return false;
75 | if (getClass() != obj.getClass())
76 | return false;
77 | HelpPopupWindow other = (HelpPopupWindow) obj;
78 | if (mID == null) {
79 | if (other.mID != null)
80 | return false;
81 | } else if (!mID.equals(other.mID))
82 | return false;
83 | return true;
84 | }
85 |
86 | private View.OnClickListener mOnNextClickListener = new View.OnClickListener() {
87 |
88 | @Override
89 | public void onClick(View v) {
90 |
91 | mOnNotifyDismissListener = null;
92 |
93 | dismiss();
94 |
95 | if (mOnNotifyNextClick != null) {
96 | mOnNotifyNextClick.onClick(v);
97 | }
98 | }
99 | };
100 |
101 | private View.OnClickListener mOnCancelClickListener = new View.OnClickListener() {
102 |
103 | @Override
104 | public void onClick(final View v) {
105 |
106 | ActionBarActivity activity = (ActionBarActivity) getContentView().getContext();
107 |
108 | new AlertDialogFragment(R.string.alert_title_info, R.string.alert_cancel_help, new AlertDialogFragmentListener() {
109 |
110 | @Override
111 | public void onPositiveClick() {
112 | mOnNotifyDismissListener = null;
113 |
114 | dismiss();
115 |
116 | if (mOnNotifyCancelClick != null) {
117 | mOnNotifyCancelClick.onClick(v);
118 | }
119 | }
120 |
121 | @Override
122 | public void onNegativeClick() {
123 | // TODO Auto-generated method stub
124 |
125 | }
126 | }).showDialog(activity.getSupportFragmentManager());
127 | }
128 | };
129 |
130 | public void setOnNotifyCancelClick(View.OnClickListener mOnNotifyCancelClick) {
131 | this.mOnNotifyCancelClick = mOnNotifyCancelClick;
132 | }
133 |
134 | public void setOnNotifyDismissListener(PopupWindow.OnDismissListener mOnNotifyDismissListener) {
135 | this.mOnNotifyDismissListener = mOnNotifyDismissListener;
136 | }
137 |
138 | public void setOnNotifyNextClick(View.OnClickListener mOnNotifyNextClick) {
139 | this.mOnNotifyNextClick = mOnNotifyNextClick;
140 | }
141 |
142 | public HelpPopupWindow(final int forVersionCode, final int ID, final View parent, final String content) {
143 |
144 | this(forVersionCode, ID, parent, 50f, 50f, content);
145 |
146 | }
147 |
148 | public HelpPopupWindow(final int forVersionCode, final int ID, final View parent, final float xOffsetPercentage, final float yOffsetPercentage, final String content) {
149 | super(parent.getContext());
150 |
151 | mID = ID;
152 | mParent = parent;
153 | mForVersionCode = forVersionCode;
154 | mXoffsetPercentage = xOffsetPercentage;
155 | mYoffsetPercentage = yOffsetPercentage;
156 |
157 | final LayoutInflater inflater = (LayoutInflater) parent.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
158 |
159 | final View contentView = inflater.inflate(R.layout.help_popup_window, null);
160 | setContentView(contentView);
161 |
162 | setFocusable(true);
163 | setOutsideTouchable(false);
164 | setClippingEnabled(false);
165 |
166 | setBackgroundDrawable(new ColorDrawable(0));
167 | setAnimationStyle(R.style.AnimationPopup);
168 |
169 | final Button buttonNext = (Button) contentView.findViewById(R.id.buttonNext);
170 | buttonNext.setOnClickListener(mOnNextClickListener);
171 |
172 | final Button buttonCancel = (Button) contentView.findViewById(R.id.buttonCancel);
173 | buttonCancel.setOnClickListener(mOnCancelClickListener);
174 |
175 | setTouchInterceptor(new View.OnTouchListener() {
176 |
177 | @Override
178 | public boolean onTouch(View view, MotionEvent motionEvent) {
179 |
180 | if (motionEvent.getAction() == KeyEvent.ACTION_UP) {
181 | return false;
182 | }
183 |
184 | final View touchAllowedControls[] = new View[] { buttonNext, buttonCancel };
185 |
186 | for (final View touchView : touchAllowedControls) {
187 | Rect rect = new Rect();
188 | int location[] = new int[2];
189 |
190 | touchView.getHitRect(rect);
191 | touchView.getLocationOnScreen(location);
192 | rect.offsetTo(location[0], location[1]);
193 |
194 | if (rect.contains((int) motionEvent.getRawX(), (int) motionEvent.getRawY())) {
195 | return false;
196 | }
197 | }
198 |
199 | return true;
200 | }
201 | });
202 |
203 | setOnDismissListener(mOnDismissListener);
204 |
205 | final TextView textViewContent = (TextView) contentView.findViewById(R.id.txtContent);
206 | textViewContent.setText(content);
207 |
208 | mBubbleRelativeLayout = (BubbleRelativeLayout) contentView;
209 | mBubbleRelativeLayout.setBubbleLayoutChangeListener(this);
210 |
211 | }
212 |
213 | public void setParent(final View parent) {
214 | mParent = parent;
215 | }
216 |
217 | public View getParent() {
218 | return mParent;
219 | }
220 |
221 | public Integer getID() {
222 | return mID;
223 | }
224 |
225 | public Integer getForVersionCode() {
226 | return mForVersionCode;
227 | }
228 |
229 | public boolean isShownAlready(final boolean writeToFile) {
230 |
231 | final SharedPreferences sharedPref = mParent.getContext().getSharedPreferences(PRIVATE_PREF, Context.MODE_PRIVATE);
232 |
233 | int storedVersionCode;
234 |
235 | try {
236 | storedVersionCode = sharedPref.getInt(mID.toString(), HelpPopupManager.INVALID_VERSION_CODE_VALUE);
237 |
238 | } catch (Exception ex) {
239 |
240 | final Editor editor = sharedPref.edit();
241 | editor.remove(mID.toString());
242 | editor.commit();
243 |
244 | storedVersionCode = HelpPopupManager.INVALID_VERSION_CODE_VALUE;
245 | }
246 |
247 | if (storedVersionCode != HelpPopupManager.INVALID_VERSION_CODE_VALUE && storedVersionCode >= mForVersionCode) {
248 | return true;
249 | }
250 |
251 | if (writeToFile) {
252 | final Editor editor = sharedPref.edit();
253 | editor.putInt(mID.toString(), mForVersionCode);
254 | editor.commit();
255 | }
256 | return false;
257 |
258 | }
259 |
260 | public void show() {
261 |
262 | if (isShownAlready(true)) {
263 | return;
264 | }
265 | showOrUpdatePopup(false);
266 | }
267 |
268 | private void showOrUpdatePopup(final boolean update) {
269 |
270 | final DisplayMetrics dispMetrics = new DisplayMetrics();
271 |
272 | final WindowManager wm = (WindowManager) mParent.getContext().getSystemService(Context.WINDOW_SERVICE);
273 | wm.getDefaultDisplay().getMetrics(dispMetrics);
274 |
275 | final RelativeLayout contentView = (RelativeLayout) getContentView();
276 | contentView.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT));
277 |
278 | final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(dispMetrics.widthPixels, MeasureSpec.AT_MOST);
279 | final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(dispMetrics.heightPixels, MeasureSpec.AT_MOST);
280 |
281 | contentView.measure(widthMeasureSpec, heightMeasureSpec);
282 |
283 | final float bubbleWidth = contentView.getMeasuredWidth();
284 | final float bubbleHeight = contentView.getMeasuredHeight();
285 |
286 | setWindowLayoutMode(1, 1);
287 |
288 | setWidth((int) bubbleWidth);
289 | setHeight((int) bubbleHeight + 10);
290 |
291 | final int[] absolutePosition = new int[2];
292 | mParent.getLocationOnScreen(absolutePosition);
293 |
294 | final float xOff = mParent.getWidth() * mXoffsetPercentage / 100f;
295 | final float yOff = mParent.getHeight() * mYoffsetPercentage / 100f;
296 |
297 | mLeftPos = (float) (absolutePosition[0] + xOff);
298 | mRightPos = (float) (dispMetrics.widthPixels - mLeftPos);
299 | mTopPos = (float) (absolutePosition[1] + mParent.getHeight() - yOff);
300 | mBottomPos = (float) (dispMetrics.heightPixels - mTopPos);
301 |
302 | final BubbleLegOrientation bubbleOrientation = resolveBubbleLegOrientation(bubbleWidth - BubbleRelativeLayout.PADDING, bubbleHeight - BubbleRelativeLayout.PADDING,
303 | dispMetrics);
304 |
305 | final float bubbleLegOffset = computeBubbleLegOffset(dispMetrics, bubbleOrientation, bubbleWidth, bubbleHeight);
306 |
307 | computePinPointAndShow(update, bubbleOrientation, bubbleLegOffset, bubbleWidth, bubbleHeight);
308 | }
309 |
310 | private BubbleLegOrientation resolveBubbleLegOrientation(final float bubbleWidth, final float bubbleHeight, final DisplayMetrics dispMetrics) {
311 |
312 | final boolean isMinTopBottomLegDistance = Math.min(mTopPos, mBottomPos) > BubbleRelativeLayout.MIN_LEG_DISTANCE;
313 | final boolean isMinLeftRightLegDistance = Math.min(mLeftPos, mRightPos) > BubbleRelativeLayout.MIN_LEG_DISTANCE;
314 |
315 | BubbleLegOrientation bubbleOrientation = (dispMetrics.widthPixels > (bubbleWidth + mLeftPos)) && isMinTopBottomLegDistance ? BubbleLegOrientation.LEFT
316 | : BubbleLegOrientation.RIGHT;
317 |
318 | if (bubbleOrientation == BubbleLegOrientation.RIGHT) {
319 |
320 | bubbleOrientation = (bubbleWidth < mLeftPos) && isMinTopBottomLegDistance ? bubbleOrientation : BubbleLegOrientation.BOTTOM;
321 |
322 | if (bubbleOrientation == BubbleLegOrientation.BOTTOM) {
323 |
324 | bubbleOrientation = (bubbleHeight < mTopPos) && isMinLeftRightLegDistance ? bubbleOrientation : BubbleLegOrientation.TOP;
325 |
326 | if (bubbleOrientation == BubbleLegOrientation.TOP) {
327 |
328 | bubbleOrientation = (dispMetrics.heightPixels > (bubbleHeight + mTopPos)) && isMinLeftRightLegDistance ? bubbleOrientation : BubbleLegOrientation.NONE;
329 | }
330 | }
331 | }
332 |
333 | return bubbleOrientation;
334 | }
335 |
336 | private float computeBubbleLegOffset(final DisplayMetrics dispMetrics, final BubbleLegOrientation bubbleLegOrientation, final float bubbleWidth, final float bubbleHeight) {
337 |
338 | if (bubbleLegOrientation == BubbleLegOrientation.BOTTOM || bubbleLegOrientation == bubbleLegOrientation.TOP) {
339 |
340 | if (mLeftPos < mRightPos) {
341 |
342 | return Math.min(mLeftPos, bubbleWidth / 2f);
343 |
344 | } else {
345 |
346 | return Math.max(bubbleWidth - mRightPos, bubbleWidth / 2f);
347 | }
348 |
349 | }
350 |
351 | if (mTopPos < mBottomPos) {
352 |
353 | return Math.min(mTopPos, bubbleHeight / 2f);
354 |
355 | } else {
356 |
357 | return Math.max(bubbleHeight - mBottomPos, bubbleHeight / 2f);
358 | }
359 |
360 | }
361 |
362 | private void computePinPointAndShow(final boolean update, final BubbleLegOrientation bubbleLegOrientation, final float legOffset, final float bubbleWidth,
363 | final float bubbleHeight) {
364 |
365 | mBubbleRelativeLayout.setBubbleParams(bubbleLegOrientation, legOffset);
366 |
367 | float xOff = mParent.getWidth() * mXoffsetPercentage / 100f;
368 | float yOff = -mParent.getHeight() * mYoffsetPercentage / 100f;
369 |
370 | if (bubbleLegOrientation == BubbleLegOrientation.TOP) {
371 |
372 | xOff -= legOffset;
373 | yOff += 0;
374 |
375 | } else if (bubbleLegOrientation == BubbleLegOrientation.BOTTOM) {
376 |
377 | xOff -= legOffset;
378 | yOff -= bubbleHeight;
379 |
380 | } else if (bubbleLegOrientation == BubbleLegOrientation.LEFT) {
381 |
382 | xOff += 0;
383 | yOff -= legOffset;
384 |
385 | } else if (bubbleLegOrientation == BubbleLegOrientation.RIGHT) {
386 |
387 | xOff -= bubbleWidth;
388 | yOff -= legOffset;
389 | }
390 |
391 | if (update) {
392 | getContentView().invalidate();
393 | update(mParent, (int) xOff, (int) yOff, (int) bubbleWidth, (int) bubbleHeight);
394 |
395 | } else {
396 | showAsDropDown(mParent, (int) xOff, (int) yOff);
397 |
398 | }
399 | }
400 |
401 | @Override
402 | public void onConfigurationChanged(Configuration newConfig) {
403 |
404 | new Handler().postDelayed(new Runnable() {
405 |
406 | @Override
407 | public void run() {
408 | showOrUpdatePopup(true);
409 | }
410 | }, 100);
411 | }
412 | }
413 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleintrohelper/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleintrohelper;
2 |
3 | import android.os.Bundle;
4 | import android.support.v7.app.ActionBarActivity;
5 | import android.view.Menu;
6 | import android.widget.Button;
7 | import android.widget.RadioButton;
8 | import android.widget.TextView;
9 |
10 | import com.example.bubbleEngine.HelpPopupManager;
11 |
12 | public class MainActivity extends ActionBarActivity {
13 |
14 | @Override
15 | protected void onCreate(Bundle savedInstanceState) {
16 | super.onCreate(savedInstanceState);
17 | setContentView(R.layout.activity_main);
18 |
19 | TextView tvSmallTest = (TextView)findViewById(R.id.textViewSmallTest);
20 | HelpPopupManager.addHelpPopupWindow(1, 1, tvSmallTest, "This is small text");
21 |
22 | TextView tvLargeTest = (TextView)findViewById(R.id.textViewLargeTest);
23 | HelpPopupManager.addHelpPopupWindow(1, 2, tvLargeTest, "This is large text");
24 |
25 | RadioButton radioButtonTest = (RadioButton)findViewById(R.id.radioButtonTest);
26 | HelpPopupManager.addHelpPopupWindow(1, 3, radioButtonTest, "This is radio button test");
27 |
28 | TextView buttonTest = (Button)findViewById(R.id.buttonTest);
29 | HelpPopupManager.addHelpPopupWindow(1, 4, buttonTest, "This is button text");
30 |
31 | }
32 |
33 | @Override
34 | public boolean onCreateOptionsMenu(Menu menu) {
35 | getMenuInflater().inflate(R.menu.main, menu);
36 |
37 | HelpPopupManager.setEnabled(true);
38 | HelpPopupManager.startShowing();
39 |
40 | return true;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/BubbleIntroHelper/src/com/example/bubbleintrohelper/MyScrollView.java:
--------------------------------------------------------------------------------
1 | package com.example.bubbleintrohelper;
2 |
3 | import com.example.bubbleEngine.HelpPopupManager;
4 |
5 | import android.content.Context;
6 | import android.util.AttributeSet;
7 | import android.widget.ScrollView;
8 |
9 | public class MyScrollView extends ScrollView {
10 |
11 | private static final int SCROLL_ENDED_UPDATION_INTERVAL = 250;
12 | private Runnable scrollEndedRunnable = new Runnable() {
13 |
14 | @Override
15 | public void run() {
16 | HelpPopupManager.startShowing();
17 | }
18 | };
19 |
20 | public MyScrollView(Context context, AttributeSet attrs, int defStyle) {
21 | super(context, attrs, defStyle);
22 | }
23 |
24 | public MyScrollView(Context context, AttributeSet attrs) {
25 | super(context, attrs);
26 | }
27 |
28 | public MyScrollView(Context context) {
29 | super(context);
30 | }
31 |
32 | @Override
33 | protected void onScrollChanged(int l, int t, int oldl, int oldt) {
34 | super.onScrollChanged(l, t, oldl, oldt);
35 |
36 | removeCallbacks(scrollEndedRunnable);
37 | postDelayed(scrollEndedRunnable, SCROLL_ENDED_UPDATION_INTERVAL);
38 |
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BubbleIntroHelper
2 | =================
3 |
4 | *BubbleIntroHelper provides an easy way how to add for an instance bubble guided tour for your android application or just bubbles on your map. Bubble algorithm automatically distinguish where to place bubble to be fitted well onto a screen.*
5 |
6 | **Showing a bubble for your view is provided by using the following method:**
7 |
8 | /**
9 | * Assign popup window to the specific parent view. Once the window is shown it will not be shown in the future anymore.
10 | * To provide popup's showing again one must clear app's storage data.
11 | *
12 | *
13 | * @param forVersionCode App version code for which popup will be shown
14 | * @param ID An ID of a popup window
15 | * @param parent A parent view to which this popup window will belong
16 | * @param content Context
17 | * @return
18 | */
19 | public static synchronized HelpPopupWindow addHelpPopupWindow(final int forVersionCode, final int ID,
20 | final View parent, final String content)
21 |
22 | 
23 |
--------------------------------------------------------------------------------