package com.marshalchen.common.uimodule.showcaseview; import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.*; import android.text.TextUtils; import android.util.AttributeSet; import android.view.*; import android.widget.Button; import android.widget.RelativeLayout; import com.marshalchen.common.uimodule.R; import com.marshalchen.common.uimodule.showcaseview.targets.Target; /** * A view which allows you to showcase areas of your app with an explanation. */ public class ShowcaseView extends RelativeLayout implements View.OnClickListener, View.OnTouchListener, ViewTreeObserver.OnPreDrawListener, ViewTreeObserver.OnGlobalLayoutListener { private static final int HOLO_BLUE = Color.parseColor("#33B5E5"); private final Button mEndButton; private final TextDrawer textDrawer; private final ShowcaseDrawer showcaseDrawer; private final ShowcaseAreaCalculator showcaseAreaCalculator; private final AnimationFactory animationFactory; private final ShotStateStore shotStateStore; // Showcase metrics private int showcaseX = -1; private int showcaseY = -1; private float scaleMultiplier = 1f; // Touch items private boolean hasCustomClickListener = false; private boolean blockTouches = true; private boolean hideOnTouch = false; private OnShowcaseEventListener mEventListener = OnShowcaseEventListener.NONE; private boolean hasAlteredText = false; private boolean hasNoTarget = false; private boolean shouldCentreText; private Bitmap bitmapBuffer; // Animation items private long fadeInMillis; private long fadeOutMillis; protected ShowcaseView(Context context, boolean newStyle) { this(context, null, R.styleable.CustomTheme_showcaseViewStyle, newStyle); } protected ShowcaseView(Context context, AttributeSet attrs, int defStyle, boolean newStyle) { super(context, attrs, defStyle); ApiUtils apiUtils = new ApiUtils(); animationFactory = new AnimatorAnimationFactory(); showcaseAreaCalculator = new ShowcaseAreaCalculator(); shotStateStore = new ShotStateStore(context); apiUtils.setFitsSystemWindowsCompat(this); getViewTreeObserver().addOnPreDrawListener(this); getViewTreeObserver().addOnGlobalLayoutListener(this); // Get the attributes for the ShowcaseView final TypedArray styled = context.getTheme() .obtainStyledAttributes(attrs, R.styleable.ShowcaseView, R.attr.showcaseViewStyle, R.style.ShowcaseView); // Set the default animation times fadeInMillis = getResources().getInteger(android.R.integer.config_mediumAnimTime); fadeOutMillis = getResources().getInteger(android.R.integer.config_mediumAnimTime); mEndButton = (Button) LayoutInflater.from(context).inflate(R.layout.showcase_button, null); if (newStyle) { showcaseDrawer = new NewShowcaseDrawer(getResources()); } else { showcaseDrawer = new StandardShowcaseDrawer(getResources()); } textDrawer = new TextDrawer(getResources(), showcaseAreaCalculator, getContext()); updateStyle(styled, false); init(); } private void init() { setOnTouchListener(this); if (mEndButton.getParent() == null) { int margin = (int) getResources().getDimension(R.dimen.showcase_button_margin); LayoutParams lps = (LayoutParams) generateDefaultLayoutParams(); lps.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); lps.addRule(RelativeLayout.ALIGN_PARENT_RIGHT); lps.setMargins(margin, margin, margin, margin); mEndButton.setLayoutParams(lps); mEndButton.setText(""); if (!hasCustomClickListener) { mEndButton.setOnClickListener(this); } addView(mEndButton); } } private boolean hasShot() { return shotStateStore.hasShot(); } void setShowcasePosition(Point point) { setShowcasePosition(point.x, point.y); } void setShowcasePosition(int x, int y) { if (shotStateStore.hasShot()) { return; } showcaseX = x; showcaseY = y; //init(); invalidate(); } public void setTarget(final Target target) { setShowcase(target, false); } public void setShowcase(final Target target, final boolean animate) { postDelayed(new Runnable() { @Override public void run() { if (!shotStateStore.hasShot()) { updateBitmap(); Point targetPoint = target.getPoint(); if (targetPoint != null) { hasNoTarget = false; if (animate) { animationFactory.animateTargetToPoint(ShowcaseView.this, targetPoint); } else { setShowcasePosition(targetPoint); } } else { hasNoTarget = true; invalidate(); } } } }, 100); } private void updateBitmap() { if (bitmapBuffer == null || haveBoundsChanged()) { bitmapBuffer = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); } } private boolean haveBoundsChanged() { return getMeasuredWidth() != bitmapBuffer.getWidth() || getMeasuredHeight() != bitmapBuffer.getHeight(); } public boolean hasShowcaseView() { return (showcaseX != 1000000 && showcaseY != 1000000) || !hasNoTarget; } public void setShowcaseX(int x) { setShowcasePosition(x, showcaseY); } public void setShowcaseY(int y) { setShowcasePosition(showcaseX, y); } public int getShowcaseX() { return showcaseX; } public int getShowcaseY() { return showcaseY; } /** * Override the standard button click event * * @param listener Listener to listen to on click events */ public void overrideButtonClick(OnClickListener listener) { if (shotStateStore.hasShot()) { return; } if (mEndButton != null) { mEndButton.setOnClickListener(listener != null ? listener : this); } hasCustomClickListener = true; } public void setOnShowcaseEventListener(OnShowcaseEventListener listener) { if (listener != null) { mEventListener = listener; } else { mEventListener = OnShowcaseEventListener.NONE; } } public void setButtonText(CharSequence text) { if (mEndButton != null) { mEndButton.setText(text); } } @Override public boolean onPreDraw() { boolean recalculatedCling = showcaseAreaCalculator.calculateShowcaseRect(showcaseX, showcaseY, showcaseDrawer); boolean recalculateText = recalculatedCling || hasAlteredText; if (recalculateText) { textDrawer.calculateTextPosition(getMeasuredWidth(), getMeasuredHeight(), this, shouldCentreText); } hasAlteredText = false; return true; } @Override protected void dispatchDraw(Canvas canvas) { if (showcaseX < 0 || showcaseY < 0 || shotStateStore.hasShot()) { super.dispatchDraw(canvas); return; } //Draw background color showcaseDrawer.erase(bitmapBuffer); // Draw the showcase drawable if (!hasNoTarget) { showcaseDrawer.drawShowcase(bitmapBuffer, showcaseX, showcaseY, scaleMultiplier); showcaseDrawer.drawToCanvas(canvas, bitmapBuffer); } // Draw the text on the screen, recalculating its position if necessary textDrawer.draw(canvas); super.dispatchDraw(canvas); } @Override public void onClick(View view) { hide(); } public void hide() { clearBitmap(); // If the type is set to one-shot, store that it has shot shotStateStore.storeShot(); mEventListener.onShowcaseViewHide(this); fadeOutShowcase(); } private void clearBitmap() { if (bitmapBuffer != null && !bitmapBuffer.isRecycled()) { bitmapBuffer.recycle(); bitmapBuffer = null; } } private void fadeOutShowcase() { animationFactory.fadeOutView(this, fadeOutMillis, new AnimationFactory.AnimationEndListener() { @Override public void onAnimationEnd() { setVisibility(View.GONE); mEventListener.onShowcaseViewDidHide(ShowcaseView.this); } }); } public void show() { mEventListener.onShowcaseViewShow(this); fadeInShowcase(); } private void fadeInShowcase() { animationFactory.fadeInView(this, fadeInMillis, new AnimationFactory.AnimationStartListener() { @Override public void onAnimationStart() { setVisibility(View.VISIBLE); } } ); } @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 (MotionEvent.ACTION_UP == motionEvent.getAction() && hideOnTouch && distanceFromFocus > showcaseDrawer.getBlockedRadius()) { this.hide(); return true; } return blockTouches && distanceFromFocus > showcaseDrawer.getBlockedRadius(); } private static void insertShowcaseView(ShowcaseView showcaseView, Activity activity) { ((ViewGroup) activity.getWindow().getDecorView()).addView(showcaseView); if (!showcaseView.hasShot()) { showcaseView.show(); } else { showcaseView.hideImmediate(); } } private void hideImmediate() { setVisibility(GONE); } public void setContentTitle(CharSequence title) { textDrawer.setContentTitle(title); } public void setContentText(CharSequence text) { textDrawer.setContentText(text); } private void setScaleMultiplier(float scaleMultiplier) { this.scaleMultiplier = scaleMultiplier; } @Override public void onGlobalLayout() { if (!shotStateStore.hasShot()) { updateBitmap(); } } public void hideButton() { mEndButton.setVisibility(GONE); } public void showButton() { mEndButton.setVisibility(VISIBLE); } /** * Builder class which allows easier creation of {@link com.marshalchen.common.uimodule.showcaseview.ShowcaseView}s. * It is recommended that you use this Builder class. */ public static class Builder { final ShowcaseView showcaseView; private final Activity activity; public Builder(Activity activity) { this(activity, false); } public Builder(Activity activity, boolean useNewStyle) { this.activity = activity; this.showcaseView = new ShowcaseView(activity, useNewStyle); this.showcaseView.setTarget(Target.NONE); } /** * Create the {@link com.marshalchen.common.uimodule.showcaseview.ShowcaseView} and show it. * * @return the created ShowcaseView */ public ShowcaseView build() { insertShowcaseView(showcaseView, activity); return showcaseView; } /** * Set the title text shown on the ShowcaseView. */ public Builder setContentTitle(int resId) { return setContentTitle(activity.getString(resId)); } /** * Set the title text shown on the ShowcaseView. */ public Builder setContentTitle(CharSequence title) { showcaseView.setContentTitle(title); return this; } /** * Set the descriptive text shown on the ShowcaseView. */ public Builder setContentText(int resId) { return setContentText(activity.getString(resId)); } /** * Set the descriptive text shown on the ShowcaseView. */ public Builder setContentText(CharSequence text) { showcaseView.setContentText(text); return this; } /** * Set the target of the showcase. * * @param target a {@link com.marshalchen.common.uimodule.showcaseview.targets.Target} representing * the item to showcase (e.g., a button, or action item). */ public Builder setTarget(Target target) { showcaseView.setTarget(target); return this; } /** * Set the style of the ShowcaseView. See the sample app for example styles. */ public Builder setStyle(int theme) { showcaseView.setStyle(theme); return this; } /** * Set a listener which will override the button clicks. * <p/> * Note that you will have to manually hide the ShowcaseView */ public Builder setOnClickListener(OnClickListener onClickListener) { showcaseView.overrideButtonClick(onClickListener); return this; } /** * Don't make the ShowcaseView block touches on itself. This doesn't * block touches in the showcased area. * <p/> * By default, the ShowcaseView does block touches */ public Builder doNotBlockTouches() { showcaseView.setBlocksTouches(false); return this; } /** * Make this ShowcaseView hide when the user touches outside the showcased area. * This enables {@link #doNotBlockTouches()} as well. * <p/> * By default, the ShowcaseView doesn't hide on touch. */ public Builder hideOnTouchOutside() { showcaseView.setBlocksTouches(true); showcaseView.setHideOnTouchOutside(true); return this; } /** * Set the ShowcaseView to only ever show once. * * @param shotId a unique identifier (<em>across the app</em>) to store * whether this ShowcaseView has been shown. */ public Builder singleShot(long shotId) { showcaseView.setSingleShot(shotId); return this; } public Builder setShowcaseEventListener(OnShowcaseEventListener showcaseEventListener) { showcaseView.setOnShowcaseEventListener(showcaseEventListener); return this; } } /** * Set whether the text should be centred in the screen, or left-aligned (which is the default). */ public void setShouldCentreText(boolean shouldCentreText) { this.shouldCentreText = shouldCentreText; hasAlteredText = true; invalidate(); } /** * @see com.marshalchen.common.uimodule.showcaseview.ShowcaseView.Builder#setSingleShot(long) */ private void setSingleShot(long shotId) { shotStateStore.setSingleShot(shotId); } /** * Change the position of the ShowcaseView's button from the default bottom-right position. * * @param layoutParams a {@link android.widget.RelativeLayout.LayoutParams} representing * the new position of the button */ public void setButtonPosition(LayoutParams layoutParams) { mEndButton.setLayoutParams(layoutParams); } /** * Set the duration of the fading in and fading out of the ShowcaseView */ private void setFadeDurations(long fadeInMillis, long fadeOutMillis) { this.fadeInMillis = fadeInMillis; this.fadeOutMillis = fadeOutMillis; } /** * @see com.marshalchen.common.uimodule.showcaseview.ShowcaseView.Builder#hideOnTouchOutside() */ public void setHideOnTouchOutside(boolean hideOnTouch) { this.hideOnTouch = hideOnTouch; } /** * @see com.marshalchen.common.uimodule.showcaseview.ShowcaseView.Builder#doNotBlockTouches() */ public void setBlocksTouches(boolean blockTouches) { this.blockTouches = blockTouches; } /** * @see com.marshalchen.common.uimodule.showcaseview.ShowcaseView.Builder#setStyle(int) */ public void setStyle(int theme) { TypedArray array = getContext().obtainStyledAttributes(theme, R.styleable.ShowcaseView); updateStyle(array, true); } private void updateStyle(TypedArray styled, boolean invalidate) { int backgroundColor = styled.getColor(R.styleable.ShowcaseView_sv_backgroundColor, Color.argb(128, 80, 80, 80)); int showcaseColor = styled.getColor(R.styleable.ShowcaseView_sv_showcaseColor, HOLO_BLUE); String buttonText = styled.getString(R.styleable.ShowcaseView_sv_buttonText); if (TextUtils.isEmpty(buttonText)) { buttonText ="ok"; } boolean tintButton = styled.getBoolean(R.styleable.ShowcaseView_sv_tintButtonColor, true); int titleTextAppearance = styled.getResourceId(R.styleable.ShowcaseView_sv_titleTextAppearance, R.style.TextAppearance_ShowcaseView_Title); int detailTextAppearance = styled.getResourceId(R.styleable.ShowcaseView_sv_detailTextAppearance, R.style.TextAppearance_ShowcaseView_Detail); styled.recycle(); showcaseDrawer.setShowcaseColour(showcaseColor); showcaseDrawer.setBackgroundColour(backgroundColor); tintButton(showcaseColor, tintButton); mEndButton.setText(buttonText); textDrawer.setTitleStyling(titleTextAppearance); textDrawer.setDetailStyling(detailTextAppearance); hasAlteredText = true; if (invalidate) { invalidate(); } } private void tintButton(int showcaseColor, boolean tintButton) { if (tintButton) { mEndButton.getBackground().setColorFilter(showcaseColor, PorterDuff.Mode.MULTIPLY); } else { mEndButton.getBackground().setColorFilter(HOLO_BLUE, PorterDuff.Mode.MULTIPLY); } } }