package com.joanfuentes.hintcase; import android.animation.Animator; import android.animation.AnimatorSet; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Point; import android.support.annotation.NonNull; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.RelativeLayout; import com.joanfuentes.hintcase.utils.DimenUtils; import java.util.ArrayList; import java.util.List; class HintCaseView extends RelativeLayout { private static final int DEFAULT_BACKGROUND_COLOR = 0xCC000000; private static final int DEFAULT_HINT_BLOCK_POSITION = HintCase.HINT_BLOCK_POSITION_BOTTOM; private static final Shape DEFAULT_SHAPE = new RectangularShape(); private static final ContentHolder NO_BLOCK_INFO = null; private static final View NO_BLOCK_INFO_VIEW = null; private static final View NO_TARGETVIEW = null; private View targetView; private Bitmap bitmap; private Shape shape = DEFAULT_SHAPE; private ContentHolder hintBlock; private Paint basePaint; private View hintBlockView; private ViewGroup parent; private int parentIndex; private int backgroundColor; private int offsetInPx; private int hintBlockPosition; private HintCase.OnClosedListener onClosedListener; private ShapeAnimator showShapeAnimator; private ShapeAnimator hideShapeAnimator; private ContentHolderAnimator showContentHolderAnimator; private ContentHolderAnimator hideContentHolderAnimator; private List<ContentHolder> extraBlocks; private List<View> extraBlockViews; private List<ContentHolderAnimator> showExtraContentHolderAnimators; private List<ContentHolderAnimator> hideExtraContentHolderAnimators; private boolean closeOnTouch; private HintCase hintCase; private boolean isTargetClickable; private Point navigationBarSizeIfExistAtTheBottom; private Point navigationBarSizeIfExistOnTheRight; private boolean wasPressedOnShape; public View getHintBlockView() { return hintBlockView; } public HintCaseView(Context context, HintCase hintCase) { super(context); init(hintCase); } private void init(HintCase hintCase) { setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); this.hintCase = hintCase; closeOnTouch = true; showExtraContentHolderAnimators = new ArrayList<>(); hideExtraContentHolderAnimators = new ArrayList<>(); hintBlock = NO_BLOCK_INFO; hintBlockView = NO_BLOCK_INFO_VIEW; extraBlocks = new ArrayList<>(); extraBlockViews = new ArrayList<>(); backgroundColor = DEFAULT_BACKGROUND_COLOR; offsetInPx = HintCase.NO_OFFSET_IN_PX; hintBlockPosition = DEFAULT_HINT_BLOCK_POSITION; basePaint = new Paint(Paint.ANTI_ALIAS_FLAG); navigationBarSizeIfExistAtTheBottom = DimenUtils.getNavigationBarSizeIfExistAtTheBottom(getContext()); navigationBarSizeIfExistOnTheRight = DimenUtils.getNavigationBarSizeIfExistOnTheRight(getContext()); } private void buildBaseBitmap() { if (bitmap != null) { bitmap.recycle(); } if (parent.getMeasuredWidth() > 0 && parent.getMeasuredHeight() > 0) { bitmap = Bitmap.createBitmap(parent.getMeasuredWidth(), parent.getMeasuredHeight(), Bitmap.Config.ARGB_8888); } } private void performShow() { parent.addView(this, parentIndex); if (showShapeAnimator != ShapeAnimator.NO_ANIMATOR) { ValueAnimator animator = showShapeAnimator.getAnimator(this, shape, new ShapeAnimator.OnFinishListener() { @Override public void onFinish() { performShowBlocks(); } }); animator.start(); } else { shape.setMinimumValue(); performShowBlocks(); } } private void performShowBlocks() { List<Animator> animators = new ArrayList<>(); if (showContentHolderAnimator != ContentHolderAnimator.NO_ANIMATOR) { animators.add(showContentHolderAnimator.getAnimator(hintBlockView)); } if (!showExtraContentHolderAnimators.isEmpty()) { for (int i = 0; i < extraBlocks.size(); i++) { ContentHolderAnimator animator = showExtraContentHolderAnimators.get(i); if (animator != ContentHolderAnimator.NO_ANIMATOR) { animators.add(animator.getAnimator(extraBlockViews.get(i))); } } } AnimatorSet animatorSet = new AnimatorSet(); if (animators.isEmpty()) { if (existHintBlock()) { getHintBlockView().setAlpha(1); } for (View view: extraBlockViews) { view.setAlpha(1); } } else { animatorSet.playTogether(animators); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { if (existHintBlock() && showContentHolderAnimator == ContentHolderAnimator.NO_ANIMATOR) { getHintBlockView().setAlpha(1); } for (int i = 0; i < showExtraContentHolderAnimators.size(); i++) { ContentHolderAnimator animator = showExtraContentHolderAnimators.get(i); if (animator == ContentHolderAnimator.NO_ANIMATOR) { extraBlockViews.get(i).setAlpha(1); } } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorSet.start(); } } void performHide() { List<Animator> animators = new ArrayList<>(); if (hideContentHolderAnimator != ContentHolderAnimator.NO_ANIMATOR) { animators.add(hideContentHolderAnimator.getAnimator(hintBlockView)); } else { if (existHintBlock()) { getHintBlockView().setAlpha(0); } } if (!hideExtraContentHolderAnimators.isEmpty()) { for (int i = 0; i < extraBlocks.size(); i++) { ContentHolderAnimator animator = hideExtraContentHolderAnimators.get(i); if (animator != ContentHolderAnimator.NO_ANIMATOR) { animators.add(animator.getAnimator(extraBlockViews.get(i))); } } } AnimatorSet animatorSet = new AnimatorSet(); if (animators.isEmpty()) { for (View view: extraBlockViews) { view.setAlpha(0); } performHideShape(); } else { animatorSet.playTogether(animators); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { performHideShape(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorSet.start(); } } private void performHideShape() { List<Animator> animators = new ArrayList<>(); if (hideShapeAnimator != ShapeAnimator.NO_ANIMATOR) { animators.add(hideShapeAnimator.getAnimator(this, shape)); } AnimatorSet animatorSet = new AnimatorSet(); if (animators.isEmpty()) { close(); } else { animatorSet.playSequentially(animators); animatorSet.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { close(); } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); animatorSet.start(); } } private void close() { removeView(); clearData(); if (onClosedListener != null) { onClosedListener.onClosed(); } } private void clearData() { if (bitmap != null) { bitmap.recycle(); } bitmap = null; parent = null; hintCase = null; } private void removeView() { if (parent != null) { parent.removeView(this); } } private void setViews() { if (existHintBlock()) { FrameLayout frameLayout = getHintBlockFrameLayout(); if (hintBlockView == NO_BLOCK_INFO_VIEW) { hintBlockView = hintBlock.getView(getContext(), hintCase, frameLayout); hintBlockView.setAlpha(0); } frameLayout.addView(hintBlockView); addView(frameLayout); } if (existExtraBlock()) { RelativeLayout relativeLayout = getExtraContentHolderRelativeLayout(); for (int i = 0; i < extraBlocks.size(); i++) { View view = extraBlocks.get(i).getView(getContext(), hintCase, relativeLayout); if (showExtraContentHolderAnimators.get(i) != ContentHolderAnimator.NO_ANIMATOR) { view.setAlpha(0); } extraBlockViews.add(view); relativeLayout.addView(view); } addView(relativeLayout); } } private boolean existHintBlock() { return hintBlock != NO_BLOCK_INFO; } private boolean existExtraBlock() { return !extraBlocks.isEmpty(); } @NonNull private FrameLayout getHintBlockFrameLayout() { int blockWidth = 0; int blockHeight = 0; int blockAlign = 0; switch (hintBlockPosition) { case HintCase.HINT_BLOCK_POSITION_TOP: blockWidth = parent.getWidth(); blockHeight = shape.getTop() - parent.getTop() - DimenUtils.getStatusBarHeight(getContext()); blockAlign = RelativeLayout.ALIGN_PARENT_TOP; break; case HintCase.HINT_BLOCK_POSITION_BOTTOM: blockWidth = parent.getWidth(); blockHeight = parent.getBottom() - navigationBarSizeIfExistAtTheBottom.y - shape.getBottom(); blockAlign = RelativeLayout.ALIGN_PARENT_BOTTOM; break; case HintCase.HINT_BLOCK_POSITION_LEFT: blockWidth = shape.getLeft() - parent.getLeft(); blockHeight = parent.getHeight() - DimenUtils.getStatusBarHeight(getContext()); blockAlign = RelativeLayout.ALIGN_PARENT_LEFT; break; case HintCase.HINT_BLOCK_POSITION_RIGHT: blockWidth = parent.getRight() - navigationBarSizeIfExistOnTheRight.x - shape.getRight(); blockHeight = parent.getHeight() - DimenUtils.getStatusBarHeight(getContext()); blockAlign = RelativeLayout.ALIGN_PARENT_RIGHT; break; case HintCase.HINT_BLOCK_POSITION_CENTER: blockWidth = parent.getWidth() - navigationBarSizeIfExistOnTheRight.x; blockHeight = parent.getHeight() - navigationBarSizeIfExistAtTheBottom.y - DimenUtils.getStatusBarHeight(getContext()); blockAlign = RelativeLayout.ALIGN_PARENT_BOTTOM; break; } LayoutParams relativeLayoutParams = new LayoutParams(blockWidth, blockHeight); relativeLayoutParams.addRule(blockAlign); relativeLayoutParams.topMargin = DimenUtils.getStatusBarHeight(getContext()); relativeLayoutParams.bottomMargin = navigationBarSizeIfExistAtTheBottom.y; relativeLayoutParams.rightMargin = navigationBarSizeIfExistOnTheRight.x; FrameLayout frameLayout = new FrameLayout(getContext()); frameLayout.setLayoutParams(relativeLayoutParams); return frameLayout; } @NonNull private RelativeLayout getExtraContentHolderRelativeLayout() { LayoutParams relativeLayoutParams = new LayoutParams(parent.getWidth(), parent.getHeight()); RelativeLayout relativeLayout = new RelativeLayout(getContext()); relativeLayoutParams.topMargin = DimenUtils.getStatusBarHeight(getContext()); relativeLayoutParams.bottomMargin = navigationBarSizeIfExistAtTheBottom.y; relativeLayoutParams.rightMargin = navigationBarSizeIfExistOnTheRight.x; relativeLayout.setLayoutParams(relativeLayoutParams); return relativeLayout; } private void calculateHintBlockPosition(ViewGroup parent, Shape shape) { if (targetView == NO_TARGETVIEW) { hintBlockPosition = HintCase.HINT_BLOCK_POSITION_CENTER; } else { int[] areas = new int[4]; areas[HintCase.HINT_BLOCK_POSITION_LEFT] = shape.getLeft() - parent.getLeft(); areas[HintCase.HINT_BLOCK_POSITION_TOP] = shape.getTop() - parent.getTop(); areas[HintCase.HINT_BLOCK_POSITION_RIGHT] = parent.getRight() - shape.getRight(); areas[HintCase.HINT_BLOCK_POSITION_BOTTOM] = parent.getBottom() - shape.getBottom(); hintBlockPosition = HintCase.HINT_BLOCK_POSITION_LEFT; for(int i = 1; i < areas.length; i++) { if(areas[i] >= areas[hintBlockPosition]) { hintBlockPosition = i; } } } } public void setOnClosedListener(HintCase.OnClosedListener onClickListener) { this.onClosedListener = onClickListener; } public void setTargetInfo(View view, Shape shape, int offsetInPx, boolean isTargetClickable) { this.targetView = view; this.shape = shape; this.offsetInPx = offsetInPx; this.isTargetClickable = isTargetClickable; } public void setOverDecorView(@NonNull View decorView) { parent = ((ViewGroup) decorView); parentIndex = -1; } public void setParentView(View parentView) { parent = (ViewGroup) parentView; parentIndex = parent.getChildCount(); } public void setCloseOnTouch(boolean closeOnTouch) { this.closeOnTouch = closeOnTouch; } public void show() { initializeView(); performShow(); } public void initializeView() { shape.setShapeInfo(targetView, parent, offsetInPx, getContext()); calculateHintBlockPosition(parent, shape); setViews(); buildBaseBitmap(); } public Shape getShape() { return shape; } public void setShape(ShapeAnimator showShapeAnimator, ShapeAnimator hideShapeAnimator) { this.showShapeAnimator = showShapeAnimator; this.hideShapeAnimator = hideShapeAnimator; } public void setHintBlock(ContentHolder contentHolder, ContentHolderAnimator showContentHolderAnimator, ContentHolderAnimator hideContentHolderAnimator) { this.hintBlock = contentHolder; this.showContentHolderAnimator = showContentHolderAnimator; this.hideContentHolderAnimator = hideContentHolderAnimator; } public int getHintBlockPosition() { return hintBlockPosition; } public void setExtraBlock(ContentHolder contentHolder, ContentHolderAnimator showContentHolderAnimator, ContentHolderAnimator hideContentHolderAnimator) { if (contentHolder != null) { this.extraBlocks.add(contentHolder); this.showExtraContentHolderAnimators.add(showContentHolderAnimator); this.hideExtraContentHolderAnimators.add(hideContentHolderAnimator); } } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (hintBlock != NO_BLOCK_INFO) { hintBlock.onLayout(); } for (ContentHolder extraBlock: extraBlocks) { extraBlock.onLayout(); } } @Override public boolean onTouchEvent(MotionEvent event) { boolean consumeTouchEvent = true; switch(event.getAction()){ case MotionEvent.ACTION_DOWN: wasPressedOnShape = shape.isTouchEventInsideTheHint(event); break; case MotionEvent.ACTION_MOVE: if(!shape.isTouchEventInsideTheHint(event)) wasPressedOnShape = false; break; case MotionEvent.ACTION_UP: if (closeOnTouch) { performHide(); } if(targetView != null && isTargetClickable && wasPressedOnShape && shape.isTouchEventInsideTheHint(event)){ targetView.performClick(); } break; } return consumeTouchEvent; } @Override protected void dispatchDraw(Canvas canvas) { if (bitmap == null) { super.dispatchDraw(canvas); } else { bitmap.eraseColor(backgroundColor); if (shape != null && (showShapeAnimator != ShapeAnimator.NO_ANIMATOR || hideShapeAnimator != ShapeAnimator.NO_ANIMATOR)) { Canvas canvasShape = new Canvas(bitmap); shape.draw(canvasShape); } canvas.drawBitmap(bitmap, 0, 0, basePaint); super.dispatchDraw(canvas); } } @Override public void setBackgroundColor(int color) { backgroundColor = color; } }