Last active
August 29, 2015 14:18
ShowcaseView Memory leak Fix
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2014 Alex Curran | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package com.github.amlcurran.showcaseview; | |
import android.app.Activity; | |
import android.content.Context; | |
import android.content.res.TypedArray; | |
import android.graphics.Bitmap; | |
import android.graphics.Canvas; | |
import android.graphics.Color; | |
import android.graphics.Point; | |
import android.graphics.PorterDuff; | |
import android.os.Build; | |
import android.text.TextUtils; | |
import android.util.AttributeSet; | |
import android.view.LayoutInflater; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.view.ViewTreeObserver; | |
import android.widget.Button; | |
import android.widget.RelativeLayout; | |
import com.github.amlcurran.showcaseview.targets.Target; | |
import static com.github.amlcurran.showcaseview.AnimationFactory.AnimationEndListener; | |
import static com.github.amlcurran.showcaseview.AnimationFactory.AnimationStartListener; | |
/** | |
* A view which allows you to showcase areas of your app with an explanation. | |
*/ | |
public class ShowcaseView extends RelativeLayout | |
implements View.OnTouchListener, ShowcaseViewApi { | |
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; | |
private CalculateTextOnPreDraw draw=null; | |
private UpdateOnGlobalLayout globalLayout=null; | |
// 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; | |
private Activity mActivity=null; | |
// Animation items | |
private long fadeInMillis; | |
private long fadeOutMillis; | |
private boolean isShowing; | |
public ShowcaseView(Context context, boolean newStyle) { | |
this(context, null, R.styleable.CustomTheme_showcaseViewStyle, newStyle); | |
if(context instanceof Activity){ | |
mActivity=(Activity)context; | |
} | |
} | |
public ShowcaseView(Context context, AttributeSet attrs, int defStyle, boolean newStyle) { | |
super(context, attrs, defStyle); | |
if(context instanceof Activity){ | |
mActivity=(Activity)context; | |
} | |
ApiUtils apiUtils = new ApiUtils(); | |
animationFactory = new AnimatorAnimationFactory(); | |
showcaseAreaCalculator = new ShowcaseAreaCalculator(); | |
shotStateStore = new ShotStateStore(context); | |
apiUtils.setFitsSystemWindowsCompat(this); | |
draw=new CalculateTextOnPreDraw(); | |
getViewTreeObserver().addOnPreDrawListener(draw); | |
globalLayout=new UpdateOnGlobalLayout(); | |
getViewTreeObserver().addOnGlobalLayoutListener(globalLayout); | |
// 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.button_margin); | |
RelativeLayout.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(android.R.string.ok); | |
if (!hasCustomClickListener) { | |
mEndButton.setOnClickListener(hideOnClickListener); | |
} | |
addView(mEndButton); | |
} | |
} | |
public 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()) | |
{ | |
if(bitmapBuffer != null) { | |
bitmapBuffer.recycle(); | |
bitmapBuffer=null; | |
} | |
bitmapBuffer = Bitmap.createBitmap(getMeasuredWidth() > 0 ? getMeasuredWidth() : 16, | |
getMeasuredHeight() > 0 ? getMeasuredHeight() : 16, | |
Bitmap.Config.ARGB_8888); | |
// 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()) { | |
hide(); | |
return; | |
} | |
if (mEndButton != null) { | |
if (listener != null) { | |
mEndButton.setOnClickListener(listener); | |
} else { | |
mEndButton.setOnClickListener(hideOnClickListener); | |
} | |
} | |
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); | |
} | |
} | |
private void recalculateText() { | |
boolean recalculatedCling = showcaseAreaCalculator.calculateShowcaseRect(showcaseX, showcaseY, showcaseDrawer); | |
boolean recalculateText = recalculatedCling || hasAlteredText; | |
if (recalculateText) { | |
textDrawer.calculateTextPosition(getMeasuredWidth(), getMeasuredHeight(), this, shouldCentreText); | |
} | |
hasAlteredText = false; | |
} | |
@SuppressWarnings("NullableProblems") | |
@Override | |
protected void dispatchDraw(Canvas canvas) { | |
if (showcaseX < 0 || showcaseY < 0 || shotStateStore.hasShot() || bitmapBuffer == null) { | |
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 hide() { | |
clearBitmap(); | |
// If the type is set to one-shot, store that it has shot | |
shotStateStore.storeShot(); | |
mEventListener.onShowcaseViewHide(this); | |
fadeOutShowcase(); | |
getViewTreeObserver().removeOnPreDrawListener(draw); | |
if(Build.VERSION.SDK_INT>15){ | |
getViewTreeObserver().removeOnGlobalLayoutListener(globalLayout); | |
} | |
else { | |
getViewTreeObserver().removeGlobalOnLayoutListener(globalLayout); | |
} | |
// removeView(ShowcaseView.this); | |
((ViewGroup)mActivity.getWindow().getDecorView()).removeView(ShowcaseView.this); | |
} | |
private void clearBitmap() { | |
if (bitmapBuffer != null && !bitmapBuffer.isRecycled()) { | |
bitmapBuffer.recycle(); | |
bitmapBuffer = null; | |
} | |
} | |
private void fadeOutShowcase() { | |
animationFactory.fadeOutView(this, fadeOutMillis, new AnimationEndListener() { | |
@Override | |
public void onAnimationEnd() { | |
setVisibility(View.GONE); | |
isShowing = false; | |
mEventListener.onShowcaseViewDidHide(ShowcaseView.this); | |
} | |
}); | |
} | |
@Override | |
public void show() { | |
isShowing = true; | |
mEventListener.onShowcaseViewShow(this); | |
fadeInShowcase(); | |
} | |
private void fadeInShowcase() { | |
animationFactory.fadeInView(this, fadeInMillis, | |
new 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.hide(); | |
} | |
} | |
public void hideImmediate() { | |
isShowing = false; | |
setVisibility(GONE); | |
// removeView(ShowcaseView.this); | |
((ViewGroup)mActivity.getWindow().getDecorView()).removeView(ShowcaseView.this); | |
} | |
@Override | |
public void setContentTitle(CharSequence title) { | |
textDrawer.setContentTitle(title); | |
} | |
@Override | |
public void setContentText(CharSequence text) { | |
textDrawer.setContentText(text); | |
} | |
private void setScaleMultiplier(float scaleMultiplier) { | |
this.scaleMultiplier = scaleMultiplier; | |
} | |
public void hideButton() { | |
mEndButton.setVisibility(GONE); | |
} | |
public void showButton() { | |
mEndButton.setVisibility(VISIBLE); | |
} | |
/** | |
* Builder class which allows easier creation of {@link 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.github.amlcurran.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.github.amlcurran.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.github.amlcurran.showcaseview.ShowcaseView.Builder#setSingleShot(long) | |
*/ | |
public 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 | |
*/ | |
@Override | |
public void setButtonPosition(RelativeLayout.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.github.amlcurran.showcaseview.ShowcaseView.Builder#hideOnTouchOutside() | |
*/ | |
@Override | |
public void setHideOnTouchOutside(boolean hideOnTouch) { | |
this.hideOnTouch = hideOnTouch; | |
} | |
/** | |
* @see com.github.amlcurran.showcaseview.ShowcaseView.Builder#doNotBlockTouches() | |
*/ | |
@Override | |
public void setBlocksTouches(boolean blockTouches) { | |
this.blockTouches = blockTouches; | |
} | |
/** | |
* @see com.github.amlcurran.showcaseview.ShowcaseView.Builder#setStyle(int) | |
*/ | |
@Override | |
public void setStyle(int theme) { | |
TypedArray array = getContext().obtainStyledAttributes(theme, R.styleable.ShowcaseView); | |
updateStyle(array, true); | |
} | |
@Override | |
public boolean isShowing() { | |
return isShowing; | |
} | |
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 = getResources().getString(android.R.string.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); | |
} | |
} | |
private class UpdateOnGlobalLayout implements ViewTreeObserver.OnGlobalLayoutListener { | |
@Override | |
public void onGlobalLayout() { | |
if (!shotStateStore.hasShot()) { | |
updateBitmap(); | |
} | |
} | |
} | |
private class CalculateTextOnPreDraw implements ViewTreeObserver.OnPreDrawListener { | |
@Override | |
public boolean onPreDraw() { | |
recalculateText(); | |
return true; | |
} | |
} | |
private OnClickListener hideOnClickListener = new OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
hide(); | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why don't you send a pull request to the repo? I mean the one here: https://github.com/Seishin/showcaseview-android