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);
}
}
}