ECLaboratorio / BubbleShowCase-Android

BubbleShowCase is a framework that let you to use informative bubbles to help your users pointing out different App features.
MIT License
603 stars 90 forks source link

double tapping on backgrounds is showing 2 bubbles #27

Open fatimasiddique opened 5 years ago

fatimasiddique commented 5 years ago

Hi,

I want to show next sequence bubble when background is clicked. Issue is when the user taps on background twice then 2 bubbles starts appearing.

image

FitApps7 commented 4 years ago

I resolved this issue by updating this class

package com.elconfidencial.bubbleshowcase


class BubbleShowCase(builder: BubbleShowCaseBuilder) {
    private val SHARED_PREFS_NAME = "BubbleShowCasePrefs"
    private val TAG = "BubbleShowCase
```"

    private val DOUBLE_CLICK_DURATION = 1000
    private val FOREGROUND_LAYOUT_ID = 731

    private val DURATION_SHOW_CASE_ANIMATION = 200 //ms
    private val DURATION_BACKGROUND_ANIMATION = 400 //ms
    private val DURATION_BEATING_ANIMATION = 800 //ms

    private val MAX_WIDTH_MESSAGE_VIEW_TABLET = 420 //dp

    /**
     * Enum class which corresponds to each valid position for the BubbleMessageView arrow
     */
    enum class ArrowPosition {
        TOP, BOTTOM, LEFT, RIGHT
    }

    /**
     * Highlight mode. It represents the way that the target view will be highlighted
     * - VIEW_LAYOUT: Default value. All the view box is highlighted (the rectangle where the view is contained). Example: For a TextView, all the element is highlighted (characters and background)
     * - VIEW_SURFACE: Only the view surface is highlighted, but not the background. Example: For a TextView, only the characters will be highlighted
     */
    enum class HighlightMode {
        VIEW_LAYOUT, VIEW_SURFACE
    }

    private val mActivity: WeakReference<Activity> = builder.mActivity!!

    //BubbleMessageView params
    private val mImage: Drawable? = builder.mImage
    private val mTitle: String? = builder.mTitle
    private val mSubtitle: String? = builder.mSubtitle
    private val mTextNextBtn: String? = builder.mTextNextBtn
    private val mCloseAction: Drawable? = builder.mCloseAction
    private val mBackgroundColor: Int? = builder.mBackgroundColor
    private val mTextColor: Int? = builder.mTextColor
    private val mTitleTextSize: Int? = builder.mTitleTextSize
    private val mSubtitleTextSize: Int? = builder.mSubtitleTextSize
    private val mShowOnce: String? = builder.mShowOnce
    private val mDisableTargetClick: Boolean = builder.mDisableTargetClick
    private val mDisableCloseAction: Boolean = builder.mDisableCloseAction
    private val mHighlightMode: BubbleShowCase.HighlightMode? = builder.mHighlightMode
    private val mArrowPositionList: MutableList<ArrowPosition> = builder.mArrowPositionList
    private val mTargetView: WeakReference<View>? = builder.mTargetView
    private val mBubbleShowCaseListener: BubbleShowCaseListener? = builder.mBubbleShowCaseListener

    //Sequence params
    private val mSequenceListener: SequenceShowCaseListener? = builder.mSequenceShowCaseListener
    private val isFirstOfSequence: Boolean = builder.mIsFirstOfSequence!!
    private val isLastOfSequence: Boolean = builder.mIsLastOfSequence!!

    //References
    private var backgroundDimLayout: RelativeLayout? = null
    private var bubbleMessageViewBuilder: BubbleMessageView.Builder? = null

    fun show() {
        if (mShowOnce != null) {
            if (isBubbleShowCaseHasBeenShowedPreviously(mShowOnce)) {
                notifyDismissToSequenceListener()
                return
            } else {
                registerBubbleShowCaseInPreferences(mShowOnce)
            }
        }

        val rootView = getViewRoot(mActivity.get()!!)
        backgroundDimLayout = getBackgroundDimLayout()
        setBackgroundDimListener(backgroundDimLayout)
        bubbleMessageViewBuilder = getBubbleMessageViewBuilder()

        if (mTargetView != null && mArrowPositionList.size <= 1) {
            //Wait until the end of the layout animation, to avoid problems with pending scrolls or view movements
            val handler = Handler()
            handler.postDelayed({
                val target = mTargetView.get()!!
                //If the arrow list is empty, the arrow position is set by default depending on the targetView position on the screen
                if (mArrowPositionList.isEmpty()) {
                    if (ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(
                            mActivity.get()!!,
                            target
                        )
                    ) mArrowPositionList.add(ArrowPosition.TOP) else mArrowPositionList.add(
                        ArrowPosition.BOTTOM
                    )
                    bubbleMessageViewBuilder = getBubbleMessageViewBuilder()
                }

                if (isVisibleOnScreen(target)) {
                    addTargetViewAtBackgroundDimLayout(target, backgroundDimLayout)
                    addBubbleMessageViewDependingOnTargetView(
                        target,
                        bubbleMessageViewBuilder!!,
                        backgroundDimLayout
                    )
                } else {
                    dismiss()
                }
            }, DURATION_BACKGROUND_ANIMATION.toLong())
        } else {
            addBubbleMessageViewOnScreenCenter(bubbleMessageViewBuilder!!, backgroundDimLayout)
        }
        if (isFirstOfSequence) {
            //Add the background dim layout above the root view
            val animation = AnimationUtils.getFadeInAnimation(0, DURATION_BACKGROUND_ANIMATION)
            backgroundDimLayout?.let {
                rootView.addView(
                    AnimationUtils.setAnimationToView(
                        backgroundDimLayout!!,
                        animation
                    )
                )
            }
        }
    }

    fun dismiss() {
        if (backgroundDimLayout != null && isLastOfSequence) {
            //Remove background dim layout if the BubbleShowCase is the last of the sequence
            finishSequence()
        } else {
            //Remove all the views created over the background dim layout waiting for the next BubbleShowCsse in the sequence
            backgroundDimLayout?.removeAllViews()
        }
        notifyDismissToSequenceListener()
    }

    private fun finishSequence() {
        val rootView = getViewRoot(mActivity.get()!!)
        rootView.removeView(backgroundDimLayout)
        backgroundDimLayout = null
    }

    private fun notifyDismissToSequenceListener() {
        mSequenceListener?.let {
            mSequenceListener.onDismiss()
        }
    }

    private fun getViewRoot(activity: Activity): ViewGroup {
        val androidContent = activity.findViewById<ViewGroup>(android.R.id.content)
        return androidContent.parent.parent as ViewGroup
    }

    private fun getBackgroundDimLayout(): RelativeLayout {
        if (mActivity.get()!!.findViewById<RelativeLayout>(FOREGROUND_LAYOUT_ID) != null)
            return mActivity.get()!!.findViewById(FOREGROUND_LAYOUT_ID)
        val backgroundLayout = RelativeLayout(mActivity.get()!!)
        backgroundLayout.id = FOREGROUND_LAYOUT_ID
        backgroundLayout.layoutParams = RelativeLayout.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT,
            ViewGroup.LayoutParams.MATCH_PARENT
        )
        backgroundLayout.setBackgroundColor(
            ContextCompat.getColor(
                mActivity.get()!!,
                R.color.transparent_grey
            )
        )
        backgroundLayout.isClickable = true
        return backgroundLayout
    }

    private fun setBackgroundDimListener(backgroundDimLayout: RelativeLayout?) {
        backgroundDimLayout?.setOnClickListener {
            try {
                if (SystemClock.elapsedRealtime() - mLastClickTime < DOUBLE_CLICK_DURATION) {//this code disable double tap
                    return@setOnClickListener
                }
                mLastClickTime = SystemClock.elapsedRealtime()

                mBubbleShowCaseListener?.onBackgroundDimClick(this)
                dismiss()
                mBubbleShowCaseListener?.onCloseActionImageClick(this)
            } catch (e: Exception) {
                Log.e(TAG, e.toString())
            }
        }
    }

    private fun getBubbleMessageViewBuilder(): BubbleMessageView.Builder {
        return BubbleMessageView.Builder()
            .from(mActivity.get()!!)
            .arrowPosition(mArrowPositionList)
            .backgroundColor(mBackgroundColor)
            .textColor(mTextColor)
            .titleTextSize(mTitleTextSize)
            .subtitleTextSize(mSubtitleTextSize)
            .textNextBtn(mTextNextBtn)
            .title(mTitle)
            .subtitle(mSubtitle)
            .image(mImage)
            .closeActionImage(mCloseAction)
            .disableCloseAction(mDisableCloseAction)
            .listener(object : OnBubbleMessageViewListener {
                override fun onBubbleClick() {
                    mBubbleShowCaseListener?.onBubbleClick(this@BubbleShowCase)
                }

                override fun onCloseActionImageClick() {
                    try {
                        if (SystemClock.elapsedRealtime() - mLastClickTime < DOUBLE_CLICK_DURATION) {//this code disable double tap
                            return
                        }
                        mLastClickTime = SystemClock.elapsedRealtime()

                        dismiss()
                        mBubbleShowCaseListener?.onCloseActionImageClick(this@BubbleShowCase)

                    } catch (e: Exception) {
                        Log.e(TAG, e.toString())
                    }
                }
            })
    }

    private fun isBubbleShowCaseHasBeenShowedPreviously(id: String): Boolean {
        val mPrefs = mActivity.get()!!.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE)
        return getString(mPrefs, id) != null
    }

    private fun registerBubbleShowCaseInPreferences(id: String) {
        val mPrefs = mActivity.get()!!.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE)
        setString(mPrefs, id, id)
    }

    private fun getString(mPrefs: SharedPreferences, key: String): String? {
        return mPrefs.getString(key, null)
    }

    private fun setString(mPrefs: SharedPreferences, key: String, value: String) {
        val editor = mPrefs.edit()
        editor.putString(key, value)
        editor.apply()
    }

    /**
     * This function takes a screenshot of the targetView, creating an ImageView from it. This new ImageView is also set on the layout passed by param
     */
    private fun addTargetViewAtBackgroundDimLayout(
        targetView: View?,
        backgroundDimLayout: RelativeLayout?
    ) {
        if (targetView == null) return

        val targetScreenshot = takeScreenshot(targetView, mHighlightMode)
        val targetScreenshotView = ImageView(mActivity.get()!!)
        targetScreenshotView.setImageBitmap(targetScreenshot)
        targetScreenshotView.setOnClickListener {
            try {
                if (SystemClock.elapsedRealtime() - mLastClickTime < DOUBLE_CLICK_DURATION) {//this code disable double tap
                    return@setOnClickListener
                }
                mLastClickTime = SystemClock.elapsedRealtime()

                if (!mDisableTargetClick)
                    dismiss()
                mBubbleShowCaseListener?.onTargetClick(this)

            } catch (e: Exception) {
                Log.e(TAG, e.toString())
            }
        }

        val targetViewParams = RelativeLayout.LayoutParams(
            RelativeLayout.LayoutParams.WRAP_CONTENT,
            RelativeLayout.LayoutParams.WRAP_CONTENT
        )
        targetViewParams.setMargins(
            getXposition(targetView),
            getYposition(targetView),
            getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width),
            0
        )
        backgroundDimLayout?.addView(
            AnimationUtils.setBouncingAnimation(
                targetScreenshotView,
                0,
                DURATION_BEATING_ANIMATION
            ), targetViewParams
        )
    }

    /**
     * This function creates the BubbleMessageView depending the position of the target and the desired arrow position. This new view is also set on the layout passed by param
     */
    private fun addBubbleMessageViewDependingOnTargetView(
        targetView: View?,
        bubbleMessageViewBuilder: BubbleMessageView.Builder,
        backgroundDimLayout: RelativeLayout?
    ) {
        if (targetView == null) return
        val showCaseParams = RelativeLayout.LayoutParams(
            RelativeLayout.LayoutParams.MATCH_PARENT,
            RelativeLayout.LayoutParams.WRAP_CONTENT
        )
        if (bubbleMessageViewBuilder.mArrowPosition.size > 0)
            when (bubbleMessageViewBuilder.mArrowPosition[0]) {
                ArrowPosition.LEFT -> {
                    showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT)
                    if (ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(
                            mActivity.get()!!,
                            targetView
                        )
                    ) {
                        showCaseParams.setMargins(
                            getXposition(targetView) + targetView.width,
                            getYposition(targetView),
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - (getXposition(
                                targetView
                            ) + targetView.width) - getMessageViewWidthOnTablet(
                                getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width)
                            ) else 0,
                            0
                        )
                        showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                    } else {
                        showCaseParams.setMargins(
                            getXposition(targetView) + targetView.width,
                            0,
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - (getXposition(
                                targetView
                            ) + targetView.width) - getMessageViewWidthOnTablet(
                                getScreenWidth(mActivity.get()!!) - (getXposition(targetView) + targetView.width)
                            ) else 0,
                            getScreenHeight(mActivity.get()!!) - getYposition(targetView) - targetView.height
                        )
                        showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                    }
                }
                ArrowPosition.RIGHT -> {
                    showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                    if (ScreenUtils.isViewLocatedAtHalfTopOfTheScreen(
                            mActivity.get()!!,
                            targetView
                        )
                    ) {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) - getMessageViewWidthOnTablet(
                                getXposition(targetView)
                            ) else 0,
                            getYposition(targetView),
                            getScreenWidth(mActivity.get()!!) - getXposition(targetView),
                            0
                        )
                        showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                    } else {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) - getMessageViewWidthOnTablet(
                                getXposition(targetView)
                            ) else 0,
                            0,
                            getScreenWidth(mActivity.get()!!) - getXposition(targetView),
                            getScreenHeight(mActivity.get()!!) - getYposition(targetView) - targetView.height
                        )
                        showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                    }
                }
                ArrowPosition.TOP -> {
                    showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_TOP)
                    if (ScreenUtils.isViewLocatedAtHalfLeftOfTheScreen(
                            mActivity.get()!!,
                            targetView
                        )
                    ) {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) else 0,
                            getYposition(targetView) + targetView.height,
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(
                                targetView
                            ) - getMessageViewWidthOnTablet(
                                getScreenWidth(mActivity.get()!!) - getXposition(targetView)
                            ) else 0,
                            0
                        )
                    } else {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) + targetView.width - getMessageViewWidthOnTablet(
                                getXposition(targetView)
                            ) else 0,
                            getYposition(targetView) + targetView.height,
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(
                                targetView
                            ) - targetView.width else 0,
                            0
                        )
                    }
                }
                ArrowPosition.BOTTOM -> {
                    showCaseParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                    if (ScreenUtils.isViewLocatedAtHalfLeftOfTheScreen(
                            mActivity.get()!!,
                            targetView
                        )
                    ) {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) else 0,
                            0,
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(
                                targetView
                            ) - getMessageViewWidthOnTablet(
                                getScreenWidth(mActivity.get()!!) - getXposition(targetView)
                            ) else 0,
                            (getScreenHeight(mActivity.get()!!) - getYposition(targetView)) + 50//this value add to set margin on bottom side of arrow
                        )
                    } else {
                        showCaseParams.setMargins(
                            if (isTablet()) getXposition(targetView) + targetView.width - getMessageViewWidthOnTablet(
                                getXposition(targetView)
                            ) else 0,
                            0,
                            if (isTablet()) getScreenWidth(mActivity.get()!!) - getXposition(
                                targetView
                            ) - targetView.width else 0,
                            (getScreenHeight(mActivity.get()!!) - getYposition(targetView)) + 50//this value add to set margin on bottom side of arrow
                        )
                    }
                }
            }

        val bubbleMessageView = bubbleMessageViewBuilder.targetViewScreenLocation(
            RectF(
                getXposition(targetView).toFloat(),
                getYposition(targetView).toFloat(),
                getXposition(targetView).toFloat() + targetView.width,
                getYposition(targetView).toFloat() + targetView.height
            )
        )
            .build()

        bubbleMessageView.id = createViewId()
        val animation = AnimationUtils.getScaleAnimation(0, DURATION_SHOW_CASE_ANIMATION)
        backgroundDimLayout?.addView(
            AnimationUtils.setAnimationToView(
                bubbleMessageView,
                animation
            ), showCaseParams
        )
    }

    /**
     * This function creates a BubbleMessageView and it is set on the center of the layout passed by param
     */
    private fun addBubbleMessageViewOnScreenCenter(
        bubbleMessageViewBuilder: BubbleMessageView.Builder,
        backgroundDimLayout: RelativeLayout?
    ) {
        val showCaseParams = RelativeLayout.LayoutParams(
            RelativeLayout.LayoutParams.MATCH_PARENT,
            RelativeLayout.LayoutParams.WRAP_CONTENT
        )
        showCaseParams.addRule(RelativeLayout.CENTER_VERTICAL)
        val bubbleMessageView: BubbleMessageView = bubbleMessageViewBuilder.build()
        bubbleMessageView.id = createViewId()
        if (isTablet()) showCaseParams.setMargins(
            if (isTablet()) getScreenWidth(mActivity.get()!!) / 2 - ScreenUtils.dpToPx(
                MAX_WIDTH_MESSAGE_VIEW_TABLET
            ) / 2 else 0,
            0,
            if (isTablet()) getScreenWidth(mActivity.get()!!) / 2 - ScreenUtils.dpToPx(
                MAX_WIDTH_MESSAGE_VIEW_TABLET
            ) / 2 else 0,
            0
        )
        val animation = AnimationUtils.getScaleAnimation(0, DURATION_SHOW_CASE_ANIMATION)
        backgroundDimLayout?.addView(
            AnimationUtils.setAnimationToView(
                bubbleMessageView,
                animation
            ), showCaseParams
        )
    }

    private fun createViewId(): Int {
        return View.generateViewId()
    }

    private fun takeScreenshot(targetView: View, highlightMode: HighlightMode?): Bitmap? {
        if (highlightMode == null || highlightMode == HighlightMode.VIEW_LAYOUT)
            return takeScreenshotOfLayoutView(targetView)
        return takeScreenshotOfSurfaceView(targetView)
    }

    private fun takeScreenshotOfLayoutView(targetView: View): Bitmap? {
        if (targetView.width == 0 || targetView.height == 0) {
            return null
        }

        val rootView = getViewRoot(mActivity.get()!!)
        val currentScreenView = rootView.getChildAt(0)
        /*currentScreenView.buildDrawingCache()
        val bitmap: Bitmap
        bitmap = Bitmap.createBitmap(currentScreenView.drawingCache, getXposition(targetView), getYposition(targetView), targetView.width, targetView.height)
        currentScreenView.isDrawingCacheEnabled = false
        currentScreenView.destroyDrawingCache()*/

        val bitmapp = Bitmap.createBitmap(
            currentScreenView.width,
            currentScreenView.height,
            Bitmap.Config.ARGB_8888
        )
        val canvas = Canvas(bitmapp)
        val bgDrawable = currentScreenView.background
        if (bgDrawable != null) {
            bgDrawable.draw(canvas)
        } else {
            canvas.drawColor(Color.WHITE)
        }
        targetView.draw(canvas)

        val bitmappp = Bitmap.createBitmap(
            bitmapp,
            getXposition(targetView),
            getYposition(targetView),
            targetView.width,
            targetView.height
        )
        val canvass = Canvas(bitmappp)
        val bgDrawablee = targetView.background
        if (bgDrawablee != null) {
            bgDrawablee.draw(canvass)
        } else {
            canvass.drawColor(Color.WHITE)
        }
        targetView.draw(canvass)

        return bitmappp
    }

    private fun takeScreenshotOfSurfaceView(targetView: View): Bitmap? {
        if (targetView.width == 0 || targetView.height == 0) {
            return null
        }

        targetView.isDrawingCacheEnabled = true
        val bitmap: Bitmap = Bitmap.createBitmap(targetView.drawingCache)
        targetView.isDrawingCacheEnabled = false
        return bitmap
    }

    private fun isVisibleOnScreen(targetView: View?): Boolean {
        if (targetView != null) {
            if (getXposition(targetView) >= 0 && getYposition(targetView) >= 0) {
                return getXposition(targetView) != 0 || getYposition(targetView) != 0
            }
        }
        return false
    }

    private fun getXposition(targetView: View): Int {
        return ScreenUtils.getAxisXpositionOfViewOnScreen(targetView) - getScreenHorizontalOffset()
    }

    private fun getYposition(targetView: View): Int {
        return ScreenUtils.getAxisYpositionOfViewOnScreen(targetView) - getScreenVerticalOffset()
    }

    private fun getScreenHeight(context: Context): Int {
        return ScreenUtils.getScreenHeight(context) - getScreenVerticalOffset()
    }

    private fun getScreenWidth(context: Context): Int {
        return ScreenUtils.getScreenWidth(context) - getScreenHorizontalOffset()
    }

    private fun getScreenVerticalOffset(): Int {
        return if (backgroundDimLayout != null) ScreenUtils.getAxisYpositionOfViewOnScreen(
            backgroundDimLayout!!
        ) else 0
    }

    private fun getScreenHorizontalOffset(): Int {
        return if (backgroundDimLayout != null) ScreenUtils.getAxisXpositionOfViewOnScreen(
            backgroundDimLayout!!
        ) else 0
    }

    private fun getMessageViewWidthOnTablet(availableSpace: Int): Int {
        return if (availableSpace > ScreenUtils.dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET)) ScreenUtils.dpToPx(
            MAX_WIDTH_MESSAGE_VIEW_TABLET
        ) else availableSpace
    }

    private fun isTablet(): Boolean = mActivity.get()!!.resources.getBoolean(R.bool.isTablet)

}`