package com.github.espiandev.showcaseview; import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.content.res.TypedArray; import android.graphics.*; import android.graphics.drawable.Drawable; import android.os.Build; import android.text.DynamicLayout; import android.text.Layout; import android.text.TextPaint; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; import android.view.*; import android.widget.Button; import android.widget.RelativeLayout; import com.github.espiandev.showcaseview.anim.AnimationUtils; import java.lang.reflect.Field; import static com.github.espiandev.showcaseview.anim.AnimationUtils.AnimationEndListener; import static com.github.espiandev.showcaseview.anim.AnimationUtils.AnimationStartListener; /** * A view which allows you to showcase areas of your app with an explanation. */ public class ShowcaseView extends RelativeLayout implements View.OnClickListener, View.OnTouchListener { public static final int TYPE_NO_LIMIT = 0; public static final int TYPE_ONE_SHOT = 1; public static final int INSERT_TO_DECOR = 0; public static final int INSERT_TO_VIEW = 1; public static final int ITEM_ACTION_HOME = 0; public static final int ITEM_TITLE = 1; public static final int ITEM_SPINNER = 2; public static final int ITEM_ACTION_ITEM = 3; public static final int ITEM_ACTION_OVERFLOW = 6; private static final String PREFS_SHOWCASE_INTERNAL = "showcase_internal"; public static final int INNER_CIRCLE_RADIUS = 94; private float showcaseX = -1; private float showcaseY = -1; private float showcaseRadius = -1; private float metricScale = 1.0f; private float legacyShowcaseX = -1; private float legacyShowcaseY = -1; private boolean isRedundant = false; private boolean hasCustomClickListener = false; private ConfigOptions mOptions; private Paint mPaintTitle, mEraser; private TextPaint mPaintDetail; private int backColor; private Drawable showcase; private View mHandy; private final Button mEndButton; private OnShowcaseEventListener mEventListener; private Rect voidedArea; private String mTitleText, mSubText; private int detailTextColor = -1; private int titleTextColor = -1; private DynamicLayout mDynamicTitleLayout; private DynamicLayout mDynamicDetailLayout; private float[] mBestTextPosition; private boolean mAlteredText = false; private final String buttonText; private float scaleMultiplier = 1f; public ShowcaseView(Context context) { this(context, null, R.styleable.CustomTheme_showcaseViewStyle); } public ShowcaseView(Context context, AttributeSet attrs) { this(context, attrs, R.styleable.CustomTheme_showcaseViewStyle); } public ShowcaseView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // Get the attributes for the ShowcaseView final TypedArray styled = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ShowcaseView, R.attr.showcaseViewStyle, R.style.ShowcaseView); backColor = styled.getInt(R.styleable.ShowcaseView_sv_backgroundColor, Color.argb(128, 80, 80, 80)); detailTextColor = styled.getColor(R.styleable.ShowcaseView_sv_detailTextColor, Color.WHITE); titleTextColor = styled.getColor(R.styleable.ShowcaseView_sv_titleTextColor, Color.parseColor("#49C0EC")); buttonText = styled.getString(R.styleable.ShowcaseView_sv_buttonText); styled.recycle(); metricScale = getContext().getResources().getDisplayMetrics().density; mEndButton = (Button) LayoutInflater.from(context).inflate(R.layout.showcase_button, null); ConfigOptions options = new ConfigOptions(); options.showcaseId = getId(); setConfigOptions(options); } private void init() { boolean hasShot = getContext().getSharedPreferences(PREFS_SHOWCASE_INTERNAL, Context.MODE_PRIVATE) .getBoolean("hasShot" + getConfigOptions().showcaseId, false); if (hasShot && mOptions.shotType == TYPE_ONE_SHOT) { // The showcase has already been shot once, so we don't need to do anything setVisibility(View.GONE); isRedundant = true; return; } showcase = getContext().getResources().getDrawable(R.drawable.cling); showcaseRadius = metricScale * INNER_CIRCLE_RADIUS; PorterDuffXfermode mBlender = new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY); setOnTouchListener(this); mPaintTitle = new Paint(); mPaintTitle.setColor(titleTextColor); mPaintTitle.setShadowLayer(2.0f, 0f, 2.0f, Color.DKGRAY); mPaintTitle.setTextSize(24 * metricScale); mPaintTitle.setAntiAlias(true); mPaintDetail = new TextPaint(); mPaintDetail.setColor(detailTextColor); mPaintDetail.setShadowLayer(2.0f, 0f, 2.0f, Color.DKGRAY); mPaintDetail.setTextSize(16 * metricScale); mPaintDetail.setAntiAlias(true); mEraser = new Paint(); mEraser.setColor(0xFFFFFF); mEraser.setAlpha(0); mEraser.setXfermode(mBlender); mEraser.setAntiAlias(true); if (!mOptions.noButton && mEndButton.getParent() == null) { RelativeLayout.LayoutParams lps = (LayoutParams) generateDefaultLayoutParams(); lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); lps.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); int margin = ((Number) (metricScale * 12)).intValue(); lps.setMargins(margin, margin, margin, margin); lps.height = LayoutParams.WRAP_CONTENT; lps.width = LayoutParams.WRAP_CONTENT; mEndButton.setLayoutParams(lps); mEndButton.setText(buttonText != null ? buttonText : getResources().getString(R.string.ok)); if (!hasCustomClickListener) mEndButton.setOnClickListener(this); addView(mEndButton); } } /** * Set the view to showcase * * @param view The {@link View} to showcase. */ public void setShowcaseView(final View view) { if (isRedundant || view == null) { isRedundant = true; return; } isRedundant = false; view.post(new Runnable() { @Override public void run() { init(); if (mOptions.insert == INSERT_TO_VIEW) { showcaseX = (float) (view.getLeft() + view.getWidth() / 2); showcaseY = (float) (view.getTop() + view.getHeight() / 2); } else { int[] coordinates = new int[2]; view.getLocationInWindow(coordinates); showcaseX = (float) (coordinates[0] + view.getWidth() / 2); showcaseY = (float) (coordinates[1] + view.getHeight() / 2); } invalidate(); } }); } /** * Set a specific position to showcase * * @param x X co-ordinate * @param y Y co-ordinate */ public void setShowcasePosition(float x, float y) { if (isRedundant) { return; } showcaseX = x; showcaseY = y; init(); invalidate(); } public void setShowcaseItem(final int itemType, final int actionItemId, final Activity activity) { post(new Runnable() { @Override public void run() { View homeButton = activity.findViewById(android.R.id.home); if (homeButton == null) { // Thanks to @hameno for this int homeId = activity.getResources().getIdentifier("abs__home", "id", activity.getPackageName()); if (homeId != 0) { homeButton = activity.findViewById(homeId); } } if (homeButton == null) throw new RuntimeException("insertShowcaseViewWithType cannot be used when the theme " + "has no ActionBar"); ViewParent p = homeButton.getParent().getParent(); //ActionBarView if (!p.getClass().getName().contains("ActionBarView")) { String previousP = p.getClass().getName(); p = p.getParent(); String throwP = p.getClass().getName(); if (!p.getClass().getName().contains("ActionBarView")) throw new IllegalStateException("Cannot find ActionBarView for " + "Activity, instead found " + previousP + " and " + throwP); } Class abv = p.getClass(); //ActionBarView class Class absAbv = abv.getSuperclass(); //AbsActionBarView class switch (itemType) { case ITEM_ACTION_HOME: setShowcaseView(homeButton); break; case ITEM_SPINNER: showcaseSpinner(p, abv); break; case ITEM_TITLE: showcaseTitle(p, abv); break; case ITEM_ACTION_ITEM: case ITEM_ACTION_OVERFLOW: showcaseActionItem(p, absAbv, itemType, actionItemId); break; default: Log.e("TAG", "Unknown item type"); } } }); } private void showcaseActionItem(ViewParent p, Class absAbv, int itemType, int actionItemId) { try { Field mAmpField = absAbv.getDeclaredField("mActionMenuPresenter"); mAmpField.setAccessible(true); Object mAmp = mAmpField.get(p); if (itemType == ITEM_ACTION_OVERFLOW) { // Finds the overflow button associated with the ActionMenuPresenter Field mObField = mAmp.getClass().getDeclaredField("mOverflowButton"); mObField.setAccessible(true); View mOb = (View) mObField.get(mAmp); if (mOb != null) setShowcaseView(mOb); } else { // Want an ActionItem, so find it Field mAmvField = mAmp.getClass().getSuperclass().getDeclaredField("mMenuView"); mAmvField.setAccessible(true); Object mAmv = mAmvField.get(mAmp); Field mChField; if (mAmv.getClass().toString().contains("com.actionbarsherlock")) { // There are thousands of superclasses to traverse up // Have to get superclasses because mChildren is private mChField = mAmv.getClass().getSuperclass().getSuperclass() .getSuperclass().getSuperclass().getDeclaredField("mChildren"); } else mChField = mAmv.getClass().getSuperclass().getSuperclass().getDeclaredField("mChildren"); mChField.setAccessible(true); Object[] mChs = (Object[]) mChField.get(mAmv); for (Object mCh : mChs) { if (mCh != null) { View v = (View) mCh; if (v.getId() == actionItemId) setShowcaseView(v); } } } } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (NullPointerException npe) { throw new RuntimeException("insertShowcaseViewWithType() must be called " + "after or during onCreateOptionsMenu() of the host Activity"); } } private void showcaseSpinner(ViewParent p, Class abv) { try { Field mSpinnerField = abv.getDeclaredField("mSpinner"); mSpinnerField.setAccessible(true); View mSpinnerView = (View) mSpinnerField.get(p); if (mSpinnerView != null) { setShowcaseView(mSpinnerView); } } catch (NoSuchFieldException e) { Log.e("TAG", "Failed to find actionbar spinner", e); } catch (IllegalAccessException e) { Log.e("TAG", "Failed to access actionbar spinner", e); } } private void showcaseTitle(ViewParent p, Class abv) { try { Field mTitleViewField = abv.getDeclaredField("mTitleView"); mTitleViewField.setAccessible(true); View titleView = (View) mTitleViewField.get(p); if (titleView != null) { setShowcaseView(titleView); } } catch (NoSuchFieldException e) { Log.e("TAG", "Failed to find actionbar title", e); } catch (IllegalAccessException e) { Log.e("TAG", "Failed to access actionbar title", e); } } /** * Set the shot method of the showcase - only once or no limit * * @param shotType either TYPE_ONE_SHOT or TYPE_NO_LIMIT * @deprecated Use the option in {@link ConfigOptions} instead. */ @Deprecated public void setShotType(int shotType) { if (shotType == TYPE_NO_LIMIT || shotType == TYPE_ONE_SHOT) { mOptions.shotType = shotType; } } /** * Decide whether touches outside the showcased circle should be ignored or not * * @param block true to block touches, false otherwise. By default, this is true. * @deprecated Use the option in {@link ConfigOptions} instead. */ @Deprecated public void blockNonShowcasedTouches(boolean block) { mOptions.block = block; } /** * Override the standard button click event * * @param listener Listener to listen to on click events */ public void overrideButtonClick(OnClickListener listener) { if (isRedundant) { return; } if (mEndButton != null) { mEndButton.setOnClickListener(listener != null ? listener : this); } hasCustomClickListener = true; } public void setOnShowcaseEventListener(OnShowcaseEventListener listener) { mEventListener = listener; } @Override protected void dispatchDraw(Canvas canvas) { if (showcaseX < 0 || showcaseY < 0 || isRedundant) { super.dispatchDraw(canvas); return; } Bitmap b = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); //Draw the semi-transparent background c.drawColor(backColor); //Draw to the scale specified Matrix mm = new Matrix(); mm.postScale(scaleMultiplier, scaleMultiplier, showcaseX, showcaseY); c.setMatrix(mm); //Erase the area for the ring c.drawCircle(showcaseX, showcaseY, showcaseRadius, mEraser); boolean recalculateText = makeVoidedRect() || mAlteredText; mAlteredText = false; showcase.setBounds(voidedArea); showcase.draw(c); canvas.drawBitmap(b, 0, 0, null); // Clean up, as we no longer require these items. try { c.setBitmap(null); } catch (NullPointerException npe) { //TODO why does this NPE happen? npe.printStackTrace(); } b.recycle(); b = null; if (!TextUtils.isEmpty(mTitleText) || !TextUtils.isEmpty(mSubText)) { if (recalculateText) mBestTextPosition = getBestTextPosition(canvas.getWidth(), canvas.getHeight()); if (!TextUtils.isEmpty(mTitleText)) { //TODO: use a dynamic detail layout canvas.drawText(mTitleText, mBestTextPosition[0], mBestTextPosition[1], mPaintTitle); } if (!TextUtils.isEmpty(mSubText)) { canvas.save(); if (recalculateText) mDynamicDetailLayout = new DynamicLayout(mSubText, mPaintDetail, ((Number) mBestTextPosition[2]).intValue(), Layout.Alignment.ALIGN_NORMAL, 1.2f, 1.0f, true); canvas.translate(mBestTextPosition[0], mBestTextPosition[1] + 12 * metricScale); mDynamicDetailLayout.draw(canvas); canvas.restore(); } } super.dispatchDraw(canvas); } /** * Calculates the best place to position text * * @param canvasW width of the screen * @param canvasH height of the screen * @return */ private float[] getBestTextPosition(int canvasW, int canvasH) { //if the width isn't much bigger than the voided area, just consider top & bottom float spaceTop = voidedArea.top; float spaceBottom = canvasH - voidedArea.bottom - 64 * metricScale; //64dip considers the OK button //float spaceLeft = voidedArea.left; //float spaceRight = canvasW - voidedArea.right; //TODO: currently only considers above or below showcase, deal with left or right return new float[]{24 * metricScale, spaceTop > spaceBottom ? 128 * metricScale : 24 * metricScale + voidedArea.bottom, canvasW - 48 * metricScale}; } /** * Creates a {@link Rect} which represents the area the showcase covers. Used to calculate * where best to place the text * * @return true if voidedArea has changed, false otherwise. */ private boolean makeVoidedRect() { // This if statement saves resources by not recalculating voidedArea // if the X & Y coordinates haven't changed if (voidedArea == null || (showcaseX != legacyShowcaseX || showcaseY != legacyShowcaseY)) { int cx = (int) showcaseX, cy = (int) showcaseY; int dw = showcase.getIntrinsicWidth(); int dh = showcase.getIntrinsicHeight(); voidedArea = new Rect(cx - dw / 2, cy - dh / 2, cx + dw / 2, cy + dh / 2); legacyShowcaseX = showcaseX; legacyShowcaseY = showcaseY; return true; } return false; } public void animateGesture(float offsetStartX, float offsetStartY, float offsetEndX, float offsetEndY) { mHandy = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.handy, null); addView(mHandy); moveHand(offsetStartX, offsetStartY, offsetEndX, offsetEndY, new AnimationEndListener() { @Override public void onAnimationEnd() { removeView(mHandy); } }); } private void moveHand(float offsetStartX, float offsetStartY, float offsetEndX, float offsetEndY, AnimationEndListener listener) { AnimationUtils.createMovementAnimation(mHandy, showcaseX, showcaseY, offsetStartX, offsetStartY, offsetEndX, offsetEndY, listener).start(); } @Override public void onClick(View view) { // If the type is set to one-shot, store that it has shot if (mOptions.shotType == TYPE_ONE_SHOT) { SharedPreferences internal = getContext().getSharedPreferences(PREFS_SHOWCASE_INTERNAL, Context.MODE_PRIVATE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { internal.edit().putBoolean("hasShot" + getConfigOptions().showcaseId, true).apply(); } else { internal.edit().putBoolean("hasShot" + getConfigOptions().showcaseId, true).commit(); } } hide(); } public void hide() { if (mEventListener != null) { mEventListener.onShowcaseViewHide(this); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { fadeOutShowcase(); } else { setVisibility(View.GONE); } } private void fadeOutShowcase() { AnimationUtils.createFadeOutAnimation(this, new AnimationEndListener() { @Override public void onAnimationEnd() { setVisibility(View.GONE); } }).start(); } public void show() { if (mEventListener != null) { mEventListener.onShowcaseViewShow(this); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { fadeInShowcase(); } else { setVisibility(View.VISIBLE); } } private void fadeInShowcase() { AnimationUtils.createFadeInAnimation(this, new AnimationStartListener() { @Override public void onAnimationStart() { setVisibility(View.VISIBLE); } }).start(); } @Override public boolean onTouch(View view, MotionEvent motionEvent) { float xDelta = Math.abs(motionEvent.getRawX() - showcaseX); float yDelta = Math.abs(motionEvent.getRawY() - showcaseY); double distanceFromFocus = Math.sqrt(Math.pow(xDelta, 2) + Math.pow(yDelta, 2)); if (mOptions.hideOnClickOutside && distanceFromFocus > showcaseRadius) { this.hide(); return true; } return mOptions.block && distanceFromFocus > showcaseRadius; } public void setShowcaseIndicatorScale(float scaleMultiplier) { this.scaleMultiplier = scaleMultiplier; } public interface OnShowcaseEventListener { public void onShowcaseViewHide(ShowcaseView showcaseView); public void onShowcaseViewShow(ShowcaseView showcaseView); } public ShowcaseView setTextColors(int titleTextColor, int detailTextColor) { this.titleTextColor = titleTextColor; this.detailTextColor = detailTextColor; if (mPaintTitle != null) { mPaintTitle.setColor(titleTextColor); } if (mPaintDetail != null) { mPaintDetail.setColor(detailTextColor); } invalidate(); return this; } public void setText(int titleTextResId, int subTextResId) { String titleText = getContext().getResources().getString(titleTextResId); String subText = getContext().getResources().getString(subTextResId); setText(titleText, subText); } public void setText(String titleText, String subText) { mTitleText = titleText; mSubText = subText; mAlteredText = true; invalidate(); } /** * Get the ghostly gesture hand for custom gestures * * @return a View representing the ghostly hand */ public View getHand() { final View mHandy = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.handy, null); addView(mHandy); AnimationUtils.hide(mHandy); return mHandy; } /** * Point to a specific view * * @param view The {@link View} to Showcase */ public void pointTo(View view) { float x = AnimationUtils.getX(view) + view.getWidth() / 2; float y = AnimationUtils.getY(view) + view.getHeight() / 2; pointTo(x, y); } /** * Point to a specific point on the screen * * @param x X-coordinate to point to * @param y Y-coordinate to point to */ public void pointTo(float x, float y) { AnimationUtils.createMovementAnimation(mHandy, x, y).start(); } private void setConfigOptions(ConfigOptions options) { mOptions = options; } private ConfigOptions getConfigOptions() { // Make sure that this method never returns null if (mOptions == null) return mOptions = new ConfigOptions(); return mOptions; } /** * Quick method to insert a ShowcaseView into an Activity * * @param viewToShowcase View to showcase * @param activity Activity to insert into * @param title Text to show as a title. Can be null. * @param detailText More detailed text. Can be null. * @param options A set of options to customise the ShowcaseView * @return the created ShowcaseView instance */ public static ShowcaseView insertShowcaseView(View viewToShowcase, Activity activity, String title, String detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcaseView(viewToShowcase); sv.setText(title, detailText); return sv; } /** * Quick method to insert a ShowcaseView into an Activity * * @param viewToShowcase View to showcase * @param activity Activity to insert into * @param title Text to show as a title. Can be null. * @param detailText More detailed text. Can be null. * @param options A set of options to customise the ShowcaseView * @return the created ShowcaseView instance */ public static ShowcaseView insertShowcaseView(View viewToShowcase, Activity activity, int title, int detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcaseView(viewToShowcase); sv.setText(title, detailText); return sv; } public static ShowcaseView insertShowcaseView(int showcaseViewId, Activity activity, String title, String detailText, ConfigOptions options) { View v = activity.findViewById(showcaseViewId); if (v != null) { return insertShowcaseView(v, activity, title, detailText, options); } return null; } public static ShowcaseView insertShowcaseView(int showcaseViewId, Activity activity, int title, int detailText, ConfigOptions options) { View v = activity.findViewById(showcaseViewId); if (v != null) { return insertShowcaseView(v, activity, title, detailText, options); } return null; } public static ShowcaseView insertShowcaseView(float x, float y, Activity activity, String title, String detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcasePosition(x, y); sv.setText(title, detailText); return sv; } public static ShowcaseView insertShowcaseView(float x, float y, Activity activity, int title, int detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcasePosition(x, y); sv.setText(title, detailText); return sv; } public static ShowcaseView insertShowcaseView(View showcase, Activity activity) { return insertShowcaseView(showcase, activity, null, null, null); } /** * Quickly insert a ShowcaseView into an Activity, highlighting an item. * * @param type the type of item to showcase (can be ITEM_ACTION_HOME, ITEM_TITLE_OR_SPINNER, ITEM_ACTION_ITEM or ITEM_ACTION_OVERFLOW) * @param itemId the ID of an Action item to showcase (only required for ITEM_ACTION_ITEM * @param activity Activity to insert the ShowcaseView into * @param title Text to show as a title. Can be null. * @param detailText More detailed text. Can be null. * @param options A set of options to customise the ShowcaseView * @return the created ShowcaseView instance */ public static ShowcaseView insertShowcaseViewWithType(int type, int itemId, Activity activity, String title, String detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcaseItem(type, itemId, activity); sv.setText(title, detailText); return sv; } /** * Quickly insert a ShowcaseView into an Activity, highlighting an item. * * @param type the type of item to showcase (can be ITEM_ACTION_HOME, ITEM_TITLE_OR_SPINNER, ITEM_ACTION_ITEM or ITEM_ACTION_OVERFLOW) * @param itemId the ID of an Action item to showcase (only required for ITEM_ACTION_ITEM * @param activity Activity to insert the ShowcaseView into * @param title Text to show as a title. Can be null. * @param detailText More detailed text. Can be null. * @param options A set of options to customise the ShowcaseView * @return the created ShowcaseView instance */ public static ShowcaseView insertShowcaseViewWithType(int type, int itemId, Activity activity, int title, int detailText, ConfigOptions options) { ShowcaseView sv = new ShowcaseView(activity); if (options != null) sv.setConfigOptions(options); if (sv.getConfigOptions().insert == INSERT_TO_DECOR) { ((ViewGroup) activity.getWindow().getDecorView()).addView(sv); } else { ((ViewGroup) activity.findViewById(android.R.id.content)).addView(sv); } sv.setShowcaseItem(type, itemId, activity); sv.setText(title, detailText); return sv; } public static ShowcaseView insertShowcaseView(float x, float y, Activity activity) { return insertShowcaseView(x, y, activity, null, null, null); } public static class ConfigOptions { public boolean block = true, noButton = false; public int showcaseId = 0; public int shotType = TYPE_NO_LIMIT; public int insert = INSERT_TO_DECOR; public boolean hideOnClickOutside = false; } }