diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d09b0b2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+
+.gradle/
+.ides/
+.idea/
diff --git a/OnBoardingBubbles/.gitignore b/OnBoardingBubbles/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/OnBoardingBubbles/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/OnBoardingBubbles/build.gradle b/OnBoardingBubbles/build.gradle
new file mode 100644
index 0000000..155c040
--- /dev/null
+++ b/OnBoardingBubbles/build.gradle
@@ -0,0 +1,38 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 30
+
+ defaultConfig {
+ minSdk 21
+ targetSdk 30
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles "consumer-rules.pro"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+}
+
+dependencies {
+
+ implementation 'androidx.core:core-ktx:1.5.0'
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'com.google.android.material:material:1.1.0-alpha09'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/consumer-rules.pro b/OnBoardingBubbles/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/OnBoardingBubbles/proguard-rules.pro b/OnBoardingBubbles/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/OnBoardingBubbles/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/AndroidManifest.xml b/OnBoardingBubbles/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..8f9f11e
--- /dev/null
+++ b/OnBoardingBubbles/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/AnimationUtils.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/AnimationUtils.kt
new file mode 100644
index 0000000..33a8583
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/AnimationUtils.kt
@@ -0,0 +1,53 @@
+package com.mohsin.onboardingbubbles
+
+import android.animation.ObjectAnimator
+import android.animation.PropertyValuesHolder
+import android.view.View
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.ScaleAnimation
+
+object AnimationUtils {
+
+ fun getScaleAnimation(offset: Int, duration: Int): Animation {
+ val anim = ScaleAnimation(
+ 0f, 1f, // Start and end values for the X axis scaling
+ 0f, 1f, // Start and end values for the Y axis scaling
+ Animation.RELATIVE_TO_SELF, 0.5f, // Pivot point of X scaling
+ Animation.RELATIVE_TO_SELF, 0.5f
+ ) // Pivot point of Y scaling
+ anim.fillAfter = true
+ anim.startOffset = offset.toLong()
+ anim.duration = duration.toLong()
+ return anim
+ }
+
+ fun getFadeInAnimation(offset: Int, duration: Int): Animation {
+ val fadeIn = AlphaAnimation(0f, 1f)
+ fadeIn.startOffset = offset.toLong()
+ fadeIn.interpolator = DecelerateInterpolator()
+ fadeIn.duration = duration.toLong()
+ return fadeIn
+ }
+
+ fun setBouncingAnimation(view: View, offset: Int, duration: Int): View {
+
+ val objAnim = ObjectAnimator.ofPropertyValuesHolder(
+ view,
+ PropertyValuesHolder.ofFloat("scaleX", 1.05f),
+ PropertyValuesHolder.ofFloat("scaleY", 1.05f)
+ )
+ objAnim.duration = duration.toLong()
+ objAnim.startDelay = offset.toLong()
+ objAnim.repeatCount = ObjectAnimator.INFINITE
+ objAnim.repeatMode = ObjectAnimator.REVERSE
+ objAnim.start()
+ return view
+ }
+
+ fun setAnimationToView(view: View, animation: Animation): View {
+ view.startAnimation(animation)
+ return view
+ }
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleMessageView.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleMessageView.kt
new file mode 100644
index 0000000..6db0fb7
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleMessageView.kt
@@ -0,0 +1,367 @@
+package com.mohsin.onboardingbubbles
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Path
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import java.lang.ref.WeakReference
+import java.util.ArrayList
+import kotlin.math.roundToInt
+
+class BubbleMessageView : ConstraintLayout {
+
+ companion object {
+ private const val WIDTH_ARROW = 20
+ }
+
+ private var itemView: View? = null
+
+ private var imageViewIcon: ImageView? = null
+ private var textViewTitle: TextView? = null
+ private var textViewSubtitle: TextView? = null
+ private var imageViewClose: ImageView? = null
+ private var showCaseMessageViewLayout: ConstraintLayout? = null
+ private var nextButton: Button? = null
+
+ private var targetViewScreenLocation: RectF? = null
+ private var mBackgroundColor: Int = ContextCompat.getColor(context, R.color.blue_default)
+ private var arrowPositionList = ArrayList()
+
+ private var paint: Paint? = null
+
+ constructor(context: Context) : super(context) {
+ initView()
+ }
+
+ constructor(context: Context, builder: Builder) : super(context) {
+ initView()
+ setAttributes(builder)
+ setBubbleListener(builder)
+ }
+
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+ initView()
+ }
+
+ private fun initView() {
+ setWillNotDraw(false)
+
+ inflateXML()
+ bindViews()
+ }
+
+ private fun inflateXML() {
+ itemView = inflate(context, R.layout.view_bubble_message, this)
+ }
+
+ private fun bindViews() {
+ imageViewIcon = findViewById(R.id.imageViewShowCase)
+ imageViewClose = findViewById(R.id.imageViewShowCaseClose)
+ textViewTitle = findViewById(R.id.textViewShowCaseTitle)
+ textViewSubtitle = findViewById(R.id.textViewShowCaseText)
+ showCaseMessageViewLayout = findViewById(R.id.showCaseMessageViewLayout)
+ nextButton = findViewById(R.id.nextButton)
+ }
+
+ private fun setAttributes(builder: Builder) {
+ if (builder.mImage != null) {
+ imageViewIcon?.visibility = View.VISIBLE
+ imageViewIcon?.setImageDrawable(builder.mImage!!)
+ }
+ if (builder.mCloseAction != null) {
+ imageViewClose?.visibility = View.VISIBLE
+ imageViewClose?.setImageDrawable(builder.mCloseAction!!)
+ }
+
+ if (builder.mDisableCloseAction != null && builder.mDisableCloseAction!!) {
+ imageViewClose?.visibility = View.INVISIBLE
+ }
+
+ builder.mTitle?.let {
+ textViewTitle?.visibility = View.VISIBLE
+ textViewTitle?.text = builder.mTitle
+ }
+ builder.mSubtitle?.let {
+ textViewSubtitle?.visibility = View.VISIBLE
+ textViewSubtitle?.text = builder.mSubtitle
+ }
+ builder.mTextColor?.let {
+ textViewTitle?.setTextColor(builder.mTextColor!!)
+ textViewSubtitle?.setTextColor(builder.mTextColor!!)
+ }
+ builder.mTitleTextSize?.let {
+ textViewTitle?.setTextSize(
+ TypedValue.COMPLEX_UNIT_SP,
+ builder.mTitleTextSize!!.toFloat()
+ )
+ }
+ builder.mSubtitleTextSize?.let {
+ textViewSubtitle?.setTextSize(
+ TypedValue.COMPLEX_UNIT_SP,
+ builder.mSubtitleTextSize!!.toFloat()
+ )
+ }
+ builder.mBackgroundColor?.let { mBackgroundColor = builder.mBackgroundColor!! }
+ arrowPositionList = builder.mArrowPosition
+ targetViewScreenLocation = builder.mTargetViewScreenLocation
+
+ if (builder.mShowNextButton) {
+ nextButton?.visibility = View.VISIBLE
+ }
+ }
+
+ private fun setBubbleListener(builder: Builder) {
+ imageViewClose?.setOnClickListener { builder.mListener?.onCloseActionImageClick() }
+ itemView?.setOnClickListener { builder.mListener?.onBubbleClick() }
+ nextButton?.setOnClickListener { builder.mListener?.onNextButtonClick() }
+ }
+
+ //REGION AUX FUNCTIONS
+
+ private fun getViewWidth(): Int = width
+
+ private fun getMargin(): Int = ScreenUtils.dpToPx(20)
+
+ private fun getSecurityArrowMargin(): Int =
+ getMargin() + ScreenUtils.dpToPx(2 * WIDTH_ARROW / 3)
+
+ //REGION SHOW ITEM
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ prepareToDraw()
+ drawRectangle(canvas)
+
+ for (arrowPosition in arrowPositionList) {
+ drawArrow(canvas, arrowPosition, targetViewScreenLocation)
+ }
+ }
+
+ private fun prepareToDraw() {
+ paint = Paint(Paint.ANTI_ALIAS_FLAG)
+ paint!!.color = mBackgroundColor
+ paint!!.style = Paint.Style.FILL
+ paint!!.strokeWidth = 4.0f
+ }
+
+ private fun drawRectangle(canvas: Canvas) {
+ val rect = RectF(
+ getMargin().toFloat(),
+ getMargin().toFloat(),
+ getViewWidth() - getMargin().toFloat(),
+ height - getMargin().toFloat()
+ )
+ canvas.drawRoundRect(rect, 10f, 10f, paint!!)
+ }
+
+ private fun drawArrow(
+ canvas: Canvas,
+ arrowPosition: BubbleShowCase.ArrowPosition,
+ targetViewLocationOnScreen: RectF?
+ ) {
+ val xPosition: Int
+ val yPosition: Int
+
+ when (arrowPosition) {
+ BubbleShowCase.ArrowPosition.LEFT -> {
+ xPosition = getMargin()
+ yPosition =
+ if (targetViewLocationOnScreen != null) getArrowVerticalPositionDependingOnTarget(
+ targetViewLocationOnScreen
+ ) else height / 2
+ }
+ BubbleShowCase.ArrowPosition.RIGHT -> {
+ xPosition = getViewWidth() - getMargin()
+ yPosition =
+ if (targetViewLocationOnScreen != null) getArrowVerticalPositionDependingOnTarget(
+ targetViewLocationOnScreen
+ ) else height / 2
+ }
+ BubbleShowCase.ArrowPosition.TOP -> {
+ xPosition =
+ if (targetViewLocationOnScreen != null) getArrowHorizontalPositionDependingOnTarget(
+ targetViewLocationOnScreen
+ ) else width / 2
+ yPosition = getMargin()
+ }
+ BubbleShowCase.ArrowPosition.BOTTOM -> {
+ xPosition =
+ if (targetViewLocationOnScreen != null) getArrowHorizontalPositionDependingOnTarget(
+ targetViewLocationOnScreen
+ ) else width / 2
+ yPosition = height - getMargin()
+ }
+ }
+
+ drawRhombus(canvas, paint, xPosition, yPosition, ScreenUtils.dpToPx(WIDTH_ARROW))
+ }
+
+ private fun getArrowHorizontalPositionDependingOnTarget(targetViewLocationOnScreen: RectF?): Int {
+ val xPosition: Int = when {
+ isOutOfRightBound(targetViewLocationOnScreen) -> width - getSecurityArrowMargin()
+ isOutOfLeftBound(targetViewLocationOnScreen) -> getSecurityArrowMargin()
+ else -> (targetViewLocationOnScreen!!.centerX() - ScreenUtils.getAxisXPositionOfViewOnScreen(
+ this
+ )).roundToInt()
+ }
+ return xPosition
+ }
+
+ private fun getArrowVerticalPositionDependingOnTarget(targetViewLocationOnScreen: RectF?): Int {
+ val yPosition: Int = when {
+ isOutOfBottomBound(targetViewLocationOnScreen) -> height - getSecurityArrowMargin()
+ isOutOfTopBound(targetViewLocationOnScreen) -> getSecurityArrowMargin()
+ else -> (targetViewLocationOnScreen!!.centerY() + ScreenUtils.getStatusBarHeight(context) - ScreenUtils.getAxisYPositionOfViewOnScreen(
+ this
+ )).roundToInt()
+ }
+ return yPosition
+ }
+
+ private fun isOutOfRightBound(targetViewLocationOnScreen: RectF?): Boolean {
+ return targetViewLocationOnScreen!!.centerX() > ScreenUtils.getAxisXPositionOfViewOnScreen(
+ this
+ ) + width - getSecurityArrowMargin()
+ }
+
+ private fun isOutOfLeftBound(targetViewLocationOnScreen: RectF?): Boolean {
+ return targetViewLocationOnScreen!!.centerX() < ScreenUtils.getAxisXPositionOfViewOnScreen(
+ this
+ ) + getSecurityArrowMargin()
+ }
+
+ private fun isOutOfBottomBound(targetViewLocationOnScreen: RectF?): Boolean {
+ return targetViewLocationOnScreen!!.centerY() > ScreenUtils.getAxisYPositionOfViewOnScreen(
+ this
+ ) + height - getSecurityArrowMargin() - ScreenUtils.getStatusBarHeight(context)
+ }
+
+ private fun isOutOfTopBound(targetViewLocationOnScreen: RectF?): Boolean {
+ return targetViewLocationOnScreen!!.centerY() < ScreenUtils.getAxisYPositionOfViewOnScreen(
+ this
+ ) + getSecurityArrowMargin() - ScreenUtils.getStatusBarHeight(context)
+ }
+
+ private fun drawRhombus(canvas: Canvas, paint: Paint?, x: Int, y: Int, width: Int) {
+ val halfRhombusWidth = width / 2
+
+ val path = Path()
+ path.moveTo(x.toFloat(), (y + halfRhombusWidth).toFloat()) // Top
+ path.lineTo((x - halfRhombusWidth).toFloat(), y.toFloat()) // Left
+ path.lineTo(x.toFloat(), (y - halfRhombusWidth).toFloat()) // Bottom
+ path.lineTo((x + halfRhombusWidth).toFloat(), y.toFloat()) // Right
+ path.lineTo(x.toFloat(), (y + halfRhombusWidth).toFloat()) // Back to Top
+ path.close()
+
+ canvas.drawPath(path, paint!!)
+ }
+
+
+ //END REGION
+
+ /**
+ * Builder for BubbleMessageView class
+ */
+ class Builder {
+ private lateinit var mContext: WeakReference
+ var mTargetViewScreenLocation: RectF? = null
+ var mImage: Drawable? = null
+ var mDisableCloseAction: Boolean? = null
+ var mTitle: String? = null
+ var mSubtitle: String? = null
+ var mCloseAction: Drawable? = null
+ var mBackgroundColor: Int? = null
+ var mTextColor: Int? = null
+ var mTitleTextSize: Int? = null
+ var mSubtitleTextSize: Int? = null
+ var mArrowPosition = ArrayList()
+ var mListener: OnBubbleMessageViewListener? = null
+ var mShowNextButton: Boolean = false
+
+ fun from(context: Context): Builder {
+ mContext = WeakReference(context)
+ return this
+ }
+
+ fun title(title: String?): Builder {
+ mTitle = title
+ return this
+ }
+
+ fun subtitle(subtitle: String?): Builder {
+ mSubtitle = subtitle
+ return this
+ }
+
+ fun image(image: Drawable?): Builder {
+ mImage = image
+ return this
+ }
+
+ fun closeActionImage(image: Drawable?): Builder {
+ mCloseAction = image
+ return this
+ }
+
+ fun disableCloseAction(isDisabled: Boolean): Builder {
+ mDisableCloseAction = isDisabled
+ return this
+ }
+
+ fun targetViewScreenLocation(targetViewLocationOnScreen: RectF): Builder {
+ mTargetViewScreenLocation = targetViewLocationOnScreen
+ return this
+ }
+
+ fun backgroundColor(backgroundColor: Int?): Builder {
+ mBackgroundColor = backgroundColor
+ return this
+ }
+
+ fun textColor(textColor: Int?): Builder {
+ mTextColor = textColor
+ return this
+ }
+
+ fun titleTextSize(textSize: Int?): Builder {
+ mTitleTextSize = textSize
+ return this
+ }
+
+ fun subtitleTextSize(textSize: Int?): Builder {
+ mSubtitleTextSize = textSize
+ return this
+ }
+
+ fun arrowPosition(arrowPosition: List): Builder {
+ mArrowPosition.clear()
+ mArrowPosition.addAll(arrowPosition)
+ return this
+ }
+
+ fun showNextButton(showNextButton: Boolean): Builder {
+ mShowNextButton = showNextButton
+ return this
+ }
+
+ fun listener(listener: OnBubbleMessageViewListener?): Builder {
+ mListener = listener
+ return this
+ }
+
+ fun build(): BubbleMessageView {
+ return BubbleMessageView(mContext.get()!!, this)
+ }
+ }
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCase.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCase.kt
new file mode 100644
index 0000000..3828ecc
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCase.kt
@@ -0,0 +1,524 @@
+package com.mohsin.onboardingbubbles
+
+import android.app.Activity
+import android.content.Context
+import android.content.Context.MODE_PRIVATE
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.graphics.RectF
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.RelativeLayout
+import androidx.core.content.ContextCompat
+import com.mohsin.onboardingbubbles.ScreenUtils.dpToPx
+import java.lang.ref.WeakReference
+
+class BubbleShowCase(builder: BubbleShowCaseBuilder) {
+
+ companion object {
+ private const val SHARED_PREFS_NAME = "BubbleShowCasePrefs"
+ private const val FOREGROUND_LAYOUT_ID = 731
+ private const val DURATION_SHOW_CASE_ANIMATION = 200 //ms
+ private const val DURATION_BACKGROUND_ANIMATION = 700 //ms
+ private const val DURATION_BEATING_ANIMATION = 700 //ms
+ private const 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 = builder.mActivity!!
+
+ //BubbleMessageView params
+ private val mImage: Drawable? = builder.mImage
+ private val mTitle: String? = builder.mTitle
+ private val mSubtitle: String? = builder.mSubtitle
+ 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 mShowNextButton: Boolean = builder.mShowNextButton
+ private val mDisableTargetClick: Boolean = builder.mDisableTargetClick
+ private val mDisableCloseAction: Boolean = builder.mDisableCloseAction
+ private val mHighlightMode: HighlightMode? = builder.mHighlightMode
+ private val mArrowPositionList: MutableList = builder.mArrowPositionList
+ private val mTargetView: WeakReference? = 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(Looper.getMainLooper())
+ 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 {
+ val childView = AnimationUtils.setAnimationToView(backgroundDimLayout!!, animation)
+ if (childView.parent == null) { //if the parent of childView is null then will add in rootView.
+ rootView.addView(childView)
+ }
+ }
+ }
+ }
+
+ 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 BubbleShowCase 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(android.R.id.content)
+ return androidContent.parent.parent as ViewGroup
+ }
+
+ private fun getBackgroundDimLayout(): RelativeLayout {
+ if (mActivity.get()!!.findViewById(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
+ backgroundLayout.isFocusable = true
+ return backgroundLayout
+ }
+
+ private fun setBackgroundDimListener(backgroundDimLayout: RelativeLayout?) {
+ backgroundDimLayout?.setOnClickListener { mBubbleShowCaseListener?.onBackgroundDimClick(this) }
+ }
+
+ private fun getBubbleMessageViewBuilder(): BubbleMessageView.Builder {
+ return BubbleMessageView.Builder()
+ .from(mActivity.get()!!)
+ .arrowPosition(mArrowPositionList)
+ .backgroundColor(mBackgroundColor)
+ .textColor(mTextColor)
+ .titleTextSize(mTitleTextSize)
+ .subtitleTextSize(mSubtitleTextSize)
+ .title(mTitle)
+ .subtitle(mSubtitle)
+ .image(mImage)
+ .showNextButton(mShowNextButton)
+ .closeActionImage(mCloseAction)
+ .disableCloseAction(mDisableCloseAction)
+ .listener(object : OnBubbleMessageViewListener {
+ override fun onBubbleClick() {
+ mBubbleShowCaseListener?.onBubbleClick(this@BubbleShowCase)
+ }
+
+ override fun onCloseActionImageClick() {
+ dismiss()
+ mBubbleShowCaseListener?.onCloseActionImageClick(this@BubbleShowCase)
+ }
+
+ override fun onNextButtonClick() {
+ dismiss()
+ mBubbleShowCaseListener?.onNextButtonClick()
+ }
+
+ })
+ }
+
+ 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 {
+ if (!mDisableTargetClick)
+ dismiss()
+ mBubbleShowCaseListener?.onTargetClick(this)
+ }
+
+ 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
+ )
+
+ 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)
+ )
+ } 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)
+ )
+ }
+ }
+ }
+
+ 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 - dpToPx(
+ MAX_WIDTH_MESSAGE_VIEW_TABLET
+ ) / 2 else 0,
+ 0,
+ if (isTablet()) getScreenWidth(mActivity.get()!!) / 2 - 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.createBitmap(
+ currentScreenView.drawingCache,
+ getXPosition(targetView),
+ getYPosition(targetView),
+ targetView.width,
+ targetView.height
+ )
+ currentScreenView.isDrawingCacheEnabled = false
+ currentScreenView.destroyDrawingCache()
+ return bitmap
+ }
+
+ 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 > dpToPx(MAX_WIDTH_MESSAGE_VIEW_TABLET)) dpToPx(
+ MAX_WIDTH_MESSAGE_VIEW_TABLET
+ ) else availableSpace
+ }
+
+ private fun isTablet(): Boolean = mActivity.get()!!.resources.getBoolean(R.bool.isTablet)
+
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseBuilder.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseBuilder.kt
new file mode 100644
index 0000000..6064a23
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseBuilder.kt
@@ -0,0 +1,294 @@
+package com.mohsin.onboardingbubbles
+
+import android.app.Activity
+import android.graphics.drawable.Drawable
+import android.view.View
+import android.view.ViewTreeObserver
+import androidx.core.content.ContextCompat
+import java.lang.ref.WeakReference
+import java.util.ArrayList
+
+/**
+ * It needs an instance of the current activity to convert it to a weak reference in order to avoid memory leaks
+ */
+class BubbleShowCaseBuilder(activity: Activity) {
+
+ internal var mActivity: WeakReference? = null
+ internal var mImage: Drawable? = null
+ internal var mTitle: String? = null
+ internal var mSubtitle: String? = null
+ internal var mCloseAction: Drawable? = null
+ internal var mBackgroundColor: Int? = null
+ internal var mTextColor: Int? = null
+ internal var mTitleTextSize: Int? = null
+ internal var mSubtitleTextSize: Int? = null
+ internal var mHighlightMode: BubbleShowCase.HighlightMode? = null
+ internal var mDisableTargetClick: Boolean = false
+ internal var mDisableCloseAction: Boolean = false
+ internal var mShowOnce: String? = null
+ internal var mIsFirstOfSequence: Boolean? = null
+ internal var mIsLastOfSequence: Boolean? = null
+ internal val mArrowPositionList = ArrayList()
+ internal var mTargetView: WeakReference? = null
+ internal var mBubbleShowCaseListener: BubbleShowCaseListener? = null
+ internal var mSequenceShowCaseListener: SequenceShowCaseListener? = null
+ internal var mShowNextButton: Boolean = false
+
+ private var onGlobalLayoutListenerTargetView: ViewTreeObserver.OnGlobalLayoutListener? = null
+
+ init {
+ mActivity = WeakReference(activity)
+ }
+
+ /**
+ * Title of the BubbleShowCase. This text is bolded in the view.
+ */
+ fun title(title: String): BubbleShowCaseBuilder {
+ mTitle = title
+ return this
+ }
+
+ /**
+ * Additional description of the BubbleShowCase. This text has a regular format
+ */
+ fun description(subtitle: String): BubbleShowCaseBuilder {
+ mSubtitle = subtitle
+ return this
+ }
+
+ /**
+ * Image drawable to inserted as main image in the BubbleShowCase
+ * - If this param is not passed, the BubbleShowCase will not have main image
+ */
+ fun image(image: Drawable): BubbleShowCaseBuilder {
+ mImage = image
+ return this
+ }
+
+ /**
+ * Image resource id to insert the corresponding drawable as main image in the BubbleShowCase
+ * - If this param is not passed, the BubbleShowCase will not have main image
+ */
+ fun imageResourceId(resId: Int): BubbleShowCaseBuilder {
+ mImage = mActivity!!.get()?.let { ContextCompat.getDrawable(it, resId) }
+ return this
+ }
+
+ /**
+ * Image drawable to be inserted as close icon in the BubbleShowCase.
+ * - If this param is not defined, a default close icon is displayed
+ */
+ fun closeActionImage(image: Drawable?): BubbleShowCaseBuilder {
+ mCloseAction = image
+ return this
+ }
+
+ /**
+ * Image resource id to insert the corresponding drawable as close icon in the BubbleShowCase.
+ * - If this param is not defined, a default close icon is displayed
+ */
+ fun closeActionImageResourceId(resId: Int): BubbleShowCaseBuilder {
+ mCloseAction = mActivity!!.get()?.let { ContextCompat.getDrawable(it, resId) }
+ return this
+ }
+
+
+ /**
+ * Background color of the BubbleShowCase.
+ * - #3F51B5 color will be set if this param is not defined
+ */
+ fun backgroundColor(color: Int): BubbleShowCaseBuilder {
+ mBackgroundColor = color
+ return this
+ }
+
+ /**
+ * Background color of the BubbleShowCase.
+ * - #3F51B5 color will be set if this param is not defined
+ */
+ fun backgroundColorResourceId(colorResId: Int): BubbleShowCaseBuilder {
+ mBackgroundColor = mActivity!!.get()?.let { ContextCompat.getColor(it, colorResId) }
+ return this
+ }
+
+ /**
+ * Text color of the BubbleShowCase.
+ * - White color will be set if this param is not defined
+ */
+ fun textColor(color: Int): BubbleShowCaseBuilder {
+ mTextColor = color
+ return this
+ }
+
+ /**
+ * Text color of the BubbleShowCase.
+ * - White color will be set if this param is not defined
+ */
+ fun textColorResourceId(colorResId: Int): BubbleShowCaseBuilder {
+ mTextColor = mActivity!!.get()?.let { ContextCompat.getColor(it, colorResId) }
+ return this
+ }
+
+ /**
+ * Title text size in SP.
+ * - Default value -> 16 sp
+ */
+ fun titleTextSize(textSize: Int): BubbleShowCaseBuilder {
+ mTitleTextSize = textSize
+ return this
+ }
+
+ /**
+ * Description text size in SP.
+ * - Default value -> 14 sp
+ */
+ fun descriptionTextSize(textSize: Int): BubbleShowCaseBuilder {
+ mSubtitleTextSize = textSize
+ return this
+ }
+
+ /**
+ * If an unique id is passed in this function, this BubbleShowCase will only be showed once
+ * - ID to identify the BubbleShowCase
+ */
+ fun showOnce(id: String): BubbleShowCaseBuilder {
+ mShowOnce = id
+ return this
+ }
+
+ /**
+ * Target view to be highlighted. Set a TargetView is essential to figure out BubbleShowCase position
+ * - If a target view is not defined, the BubbleShowCase final position will be the center of the screen
+ */
+ fun targetView(targetView: View): BubbleShowCaseBuilder {
+ mTargetView = WeakReference(targetView)
+ return this
+ }
+
+ /**
+ * If this variable is true, when user clicks on the target, the showcase will not be dismissed
+ * Default value -> false
+ */
+ fun disableTargetClick(isDisabled: Boolean): BubbleShowCaseBuilder {
+ mDisableTargetClick = isDisabled
+ return this
+ }
+
+ /**
+ * If this variable is true, close action button will be gone
+ * Default value -> false
+ */
+ fun disableCloseAction(isDisabled: Boolean): BubbleShowCaseBuilder {
+ mDisableCloseAction = isDisabled
+ return this
+ }
+
+ /**
+ * Insert an arrowPosition to force the position of the BubbleShowCase.
+ * - ArrowPosition enum values: LEFT, RIGHT, TOP and DOWN
+ * - If an arrow position is not defined, the BubbleShowCase will be set in a default position depending on the targetView
+ */
+ fun arrowPosition(arrowPosition: BubbleShowCase.ArrowPosition): BubbleShowCaseBuilder {
+ mArrowPositionList.clear()
+ mArrowPositionList.add(arrowPosition)
+ return this
+ }
+
+ /**
+ * Insert a set of arrowPosition to force the position of the BubbleShowCase.
+ * - ArrowPosition enum values: LEFT, RIGHT, TOP and DOWN
+ * - If the number of arrow positions is 0 or more than 1, BubbleShowCase will be set on the center of the screen
+ */
+ fun arrowPosition(arrowPosition: List): BubbleShowCaseBuilder {
+ mArrowPositionList.clear()
+ mArrowPositionList.addAll(arrowPosition)
+ return this
+ }
+
+ /**
+ * 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
+ */
+ fun highlightMode(highlightMode: BubbleShowCase.HighlightMode): BubbleShowCaseBuilder {
+ mHighlightMode = highlightMode
+ return this
+ }
+
+ /**
+ * If this variable is true, Next button will be Visible
+ * Default value -> false
+ */
+ fun showNextButton(show: Boolean): BubbleShowCaseBuilder {
+ mShowNextButton = show
+ return this
+ }
+
+ /**
+ * Add a BubbleShowCaseListener in order to listen the user actions:
+ * - onTargetClick -> It is triggered when the user clicks on the target view
+ * - onCloseClick -> It is triggered when the user clicks on the close icon
+ */
+ fun listener(bubbleShowCaseListener: BubbleShowCaseListener): BubbleShowCaseBuilder {
+ mBubbleShowCaseListener = bubbleShowCaseListener
+ return this
+ }
+
+ /**
+ * Add a sequence listener in order to know when a BubbleShowCase has been dismissed to show another one
+ */
+ internal fun sequenceListener(sequenceShowCaseListener: SequenceShowCaseListener): BubbleShowCaseBuilder {
+ mSequenceShowCaseListener = sequenceShowCaseListener
+ return this
+ }
+
+ internal fun isFirstOfSequence(isFirst: Boolean): BubbleShowCaseBuilder {
+ mIsFirstOfSequence = isFirst
+ return this
+ }
+
+ internal fun isLastOfSequence(isLast: Boolean): BubbleShowCaseBuilder {
+ mIsLastOfSequence = isLast
+ return this
+ }
+
+ /**
+ * Build the BubbleShowCase object from the builder one
+ */
+ private fun build(): BubbleShowCase {
+ if (mIsFirstOfSequence == null)
+ mIsFirstOfSequence = true
+ if (mIsLastOfSequence == null)
+ mIsLastOfSequence = true
+
+ return BubbleShowCase(this)
+ }
+
+ /**
+ * Show the BubbleShowCase using the params added previously
+ */
+ fun show(): BubbleShowCase {
+ val bubbleShowCase = build()
+ if (mTargetView != null) {
+ val targetView = mTargetView!!.get()
+ if (targetView!!.height == 0 || targetView.width == 0) {
+ //If the view is not already painted, we wait for it waiting for view changes using OnGlobalLayoutListener
+ onGlobalLayoutListenerTargetView = ViewTreeObserver.OnGlobalLayoutListener {
+ bubbleShowCase.show()
+ targetView.viewTreeObserver.removeOnGlobalLayoutListener(
+ onGlobalLayoutListenerTargetView
+ )
+ }
+ targetView.viewTreeObserver.addOnGlobalLayoutListener(
+ onGlobalLayoutListenerTargetView
+ )
+ } else {
+ bubbleShowCase.show()
+ }
+ } else {
+ bubbleShowCase.show()
+ }
+ return bubbleShowCase
+ }
+
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseListener.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseListener.kt
new file mode 100644
index 0000000..5f52c1d
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseListener.kt
@@ -0,0 +1,33 @@
+package com.mohsin.onboardingbubbles
+
+/**
+ *
+ * Listener of user actions in a BubbleShowCase
+ */
+interface BubbleShowCaseListener {
+ /**
+ * It is called when the user clicks on the targetView
+ */
+ fun onTargetClick(bubbleShowCase: BubbleShowCase) {}
+
+ /**
+ * It is called when the user clicks on the close icon
+ */
+ fun onCloseActionImageClick(bubbleShowCase: BubbleShowCase) {}
+
+ /**
+ * It is called when the user clicks on the background dim
+ */
+ fun onBackgroundDimClick(bubbleShowCase: BubbleShowCase) {}
+
+ /**
+ * It is called when the user clicks on the bubble
+ */
+ fun onBubbleClick(bubbleShowCase: BubbleShowCase) {}
+
+ /**
+ * It is called when the user clicks on the Next Button
+ * if you set next button true, then you should implement this method
+ */
+ fun onNextButtonClick() {}
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseSequence.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseSequence.kt
new file mode 100644
index 0000000..d8f10d9
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/BubbleShowCaseSequence.kt
@@ -0,0 +1,46 @@
+package com.mohsin.onboardingbubbles
+
+class BubbleShowCaseSequence {
+ private val mBubbleShowCaseBuilderList = ArrayList()
+
+ init {
+ mBubbleShowCaseBuilderList.clear()
+ }
+
+ fun addShowCase(bubbleShowCaseBuilder: BubbleShowCaseBuilder): BubbleShowCaseSequence {
+ mBubbleShowCaseBuilderList.add(bubbleShowCaseBuilder)
+ return this
+ }
+
+ fun addShowCases(bubbleShowCaseBuilderList: List): BubbleShowCaseSequence {
+ mBubbleShowCaseBuilderList.addAll(bubbleShowCaseBuilderList)
+ return this
+ }
+
+ fun show() = show(0)
+
+ private fun show(position: Int) {
+ if (position >= mBubbleShowCaseBuilderList.size)
+ return
+
+ when (position) {
+ 0 -> {
+ mBubbleShowCaseBuilderList[position].isFirstOfSequence(true)
+ mBubbleShowCaseBuilderList[position].isLastOfSequence(false)
+ }
+ mBubbleShowCaseBuilderList.size - 1 -> {
+ mBubbleShowCaseBuilderList[position].isFirstOfSequence(false)
+ mBubbleShowCaseBuilderList[position].isLastOfSequence(true)
+ }
+ else -> {
+ mBubbleShowCaseBuilderList[position].isFirstOfSequence(false)
+ mBubbleShowCaseBuilderList[position].isLastOfSequence(false)
+ }
+ }
+ mBubbleShowCaseBuilderList[position].sequenceListener(object : SequenceShowCaseListener {
+ override fun onDismiss() {
+ show(position + 1)
+ }
+ }).show()
+ }
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/OnBubbleMessageViewListener.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/OnBubbleMessageViewListener.kt
new file mode 100644
index 0000000..4bf09e2
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/OnBubbleMessageViewListener.kt
@@ -0,0 +1,19 @@
+package com.mohsin.onboardingbubbles
+
+interface OnBubbleMessageViewListener {
+ /**
+ * It is called when a user clicks the close action image in the BubbleMessageView
+ */
+ fun onCloseActionImageClick()
+
+
+ /**
+ * It is called when a user clicks the BubbleMessageView
+ */
+ fun onBubbleClick()
+
+ /**
+ * It is called when a user clicks the Next Button
+ */
+ fun onNextButtonClick()
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/ScreenUtils.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/ScreenUtils.kt
new file mode 100644
index 0000000..5427a36
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/ScreenUtils.kt
@@ -0,0 +1,88 @@
+package com.mohsin.onboardingbubbles
+
+import android.app.Activity
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Point
+import android.util.DisplayMetrics
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import kotlin.math.roundToInt
+
+object ScreenUtils {
+
+ fun getScreenHeight(context: Context): Int {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val display = wm.defaultDisplay
+ val size = Point()
+ display.getSize(size)
+ return size.y
+ }
+
+ fun getScreenWidth(context: Context): Int {
+ val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+ val display = wm.defaultDisplay
+ val size = Point()
+ display.getSize(size)
+ return size.x
+ }
+
+ fun getAxisXPositionOfViewOnScreen(targetView: View): Int {
+ val locationTarget = IntArray(2)
+ targetView.getLocationOnScreen(locationTarget)
+ return locationTarget[0]
+ }
+
+ fun getAxisYPositionOfViewOnScreen(targetView: View): Int {
+ val locationTarget = IntArray(2)
+ targetView.getLocationOnScreen(locationTarget)
+ return locationTarget[1]
+ }
+
+ fun getVerticalScreenOffset(activity: Activity): Int {
+ val viewRoot = getViewRoot(activity)
+ return getScreenHeight(activity) - viewRoot.height
+ }
+
+ fun getHorizontalScreenOffset(activity: Activity): Int {
+ val viewRoot = getViewRoot(activity)
+ return getScreenWidth(activity) - viewRoot.width
+ }
+
+ private fun getViewRoot(activity: Activity): ViewGroup {
+ val androidContent = activity.findViewById(android.R.id.content)
+ return androidContent.parent.parent as ViewGroup
+ }
+
+ fun getStatusBarHeight(context: Context): Int {
+ var result = 0
+ val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
+ if (resourceId > 0) {
+ result = context.resources.getDimensionPixelSize(resourceId)
+ }
+ return result
+ }
+
+ fun pxToDp(px: Int): Int {
+ val metrics = Resources.getSystem().displayMetrics
+ return (px / (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
+ }
+
+ fun dpToPx(dp: Int): Int {
+ val metrics = Resources.getSystem().displayMetrics
+ return (dp * (metrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).roundToInt()
+ }
+
+ fun isViewLocatedAtHalfTopOfTheScreen(activity: Activity, targetView: View): Boolean {
+ val screenHeight = getScreenHeight(activity)
+ val positionTargetAxisY = getAxisYPositionOfViewOnScreen(targetView)
+ return screenHeight / 2 > positionTargetAxisY
+ }
+
+ fun isViewLocatedAtHalfLeftOfTheScreen(activity: Activity, targetView: View): Boolean {
+ val screenWidth = getScreenWidth(activity)
+ val positionTargetAxisX = getAxisXPositionOfViewOnScreen(targetView)
+ return screenWidth / 2 > positionTargetAxisX
+ }
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/SequenceShowCaseListener.kt b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/SequenceShowCaseListener.kt
new file mode 100644
index 0000000..8f98efa
--- /dev/null
+++ b/OnBoardingBubbles/src/main/java/com/mohsin/onboardingbubbles/SequenceShowCaseListener.kt
@@ -0,0 +1,5 @@
+package com.mohsin.onboardingbubbles
+
+interface SequenceShowCaseListener {
+ fun onDismiss()
+}
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/drawable/button_bg.xml b/OnBoardingBubbles/src/main/res/drawable/button_bg.xml
new file mode 100644
index 0000000..ba1f0b3
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/drawable/button_bg.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/drawable/ic_close_grey_16dp.xml b/OnBoardingBubbles/src/main/res/drawable/ic_close_grey_16dp.xml
new file mode 100644
index 0000000..81ed437
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/drawable/ic_close_grey_16dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/drawable/rounded_rectangle.xml b/OnBoardingBubbles/src/main/res/drawable/rounded_rectangle.xml
new file mode 100644
index 0000000..b452d87
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/drawable/rounded_rectangle.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/font/nunito.xml b/OnBoardingBubbles/src/main/res/font/nunito.xml
new file mode 100644
index 0000000..28b18c4
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_black.xml b/OnBoardingBubbles/src/main/res/font/nunito_black.xml
new file mode 100644
index 0000000..440dfc4
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_black.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_black_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_black_italic.xml
new file mode 100644
index 0000000..fc04f28
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_black_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_bold.xml b/OnBoardingBubbles/src/main/res/font/nunito_bold.xml
new file mode 100644
index 0000000..4484884
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_bold.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_bold_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_bold_italic.xml
new file mode 100644
index 0000000..47e54b4
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_bold_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_extrabold.xml b/OnBoardingBubbles/src/main/res/font/nunito_extrabold.xml
new file mode 100644
index 0000000..e35770f
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_extrabold.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_extrabold_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_extrabold_italic.xml
new file mode 100644
index 0000000..1d30f2d
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_extrabold_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_extralight.xml b/OnBoardingBubbles/src/main/res/font/nunito_extralight.xml
new file mode 100644
index 0000000..bd64b75
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_extralight.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_extralight_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_extralight_italic.xml
new file mode 100644
index 0000000..b836944
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_extralight_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_font_family.xml b/OnBoardingBubbles/src/main/res/font/nunito_font_family.xml
new file mode 100644
index 0000000..5b55a5a
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_font_family.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_italic.xml
new file mode 100644
index 0000000..d7a5204
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_light.xml b/OnBoardingBubbles/src/main/res/font/nunito_light.xml
new file mode 100644
index 0000000..4cd468f
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_light.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_light_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_light_italic.xml
new file mode 100644
index 0000000..42b58f2
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_light_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_semibold.xml b/OnBoardingBubbles/src/main/res/font/nunito_semibold.xml
new file mode 100644
index 0000000..52aa54c
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_semibold.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/font/nunito_semibold_italic.xml b/OnBoardingBubbles/src/main/res/font/nunito_semibold_italic.xml
new file mode 100644
index 0000000..02c25da
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/font/nunito_semibold_italic.xml
@@ -0,0 +1,7 @@
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/layout/view_bubble_message.xml b/OnBoardingBubbles/src/main/res/layout/view_bubble_message.xml
new file mode 100644
index 0000000..9e53324
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/layout/view_bubble_message.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/values/attrs.xml b/OnBoardingBubbles/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..f2849cc
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/values/attrs.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/values/bool.xml b/OnBoardingBubbles/src/main/res/values/bool.xml
new file mode 100644
index 0000000..f855de6
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/values/bool.xml
@@ -0,0 +1,4 @@
+
+
+ false
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/values/colors.xml b/OnBoardingBubbles/src/main/res/values/colors.xml
new file mode 100644
index 0000000..495c266
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #77000000
+ #0096FF
+ #FFFFFF
+
\ No newline at end of file
diff --git a/OnBoardingBubbles/src/main/res/values/font_certs.xml b/OnBoardingBubbles/src/main/res/values/font_certs.xml
new file mode 100644
index 0000000..d2226ac
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/OnBoardingBubbles/src/main/res/values/strings.xml b/OnBoardingBubbles/src/main/res/values/strings.xml
new file mode 100644
index 0000000..e45b3bc
--- /dev/null
+++ b/OnBoardingBubbles/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Next
+
\ No newline at end of file
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 0000000..8e1304b
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,43 @@
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+}
+
+android {
+ compileSdk 30
+
+ defaultConfig {
+ applicationId "com.mohsin.onboardingbubbles"
+ minSdk 21
+ targetSdk 30
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+ buildFeatures {
+ viewBinding true
+ }
+}
+
+dependencies {
+ implementation 'androidx.core:core-ktx:1.5.0'
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ implementation 'com.google.android.material:material:1.1.0-alpha09'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation project(path: ':OnBoardingBubbles')
+}
\ No newline at end of file
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..6833ffe
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/mohsin/onboardingbubbles/MainActivity.kt b/app/src/main/java/com/mohsin/onboardingbubbles/MainActivity.kt
new file mode 100644
index 0000000..829bb99
--- /dev/null
+++ b/app/src/main/java/com/mohsin/onboardingbubbles/MainActivity.kt
@@ -0,0 +1,122 @@
+package com.mohsin.onboardingbubbles
+
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.widget.Toast
+import androidx.core.content.ContextCompat
+import com.mohsin.onboardingbubbles.databinding.ActivityMainBinding
+
+class MainActivity : AppCompatActivity() {
+
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ setUpListeners()
+ }
+
+ private fun setUpListeners() {
+ binding.apply {
+ buttonSimpleShowCase.setOnClickListener { getSimpleShowCaseBuilder().show() }
+ buttonColorShowCase.setOnClickListener { getCustomColorShowCaseBuilder().show() }
+ buttonTextSizeShowCase.setOnClickListener { getTextSizeShowCaseBuilder().show() }
+ buttonArrowLeftShowCase.setOnClickListener { getArrowLeftShowCaseBuilder().show() }
+ buttonArrowRightShowCase.setOnClickListener { getArrowRightShowCaseBuilder().show() }
+ buttonEventListener.setOnClickListener { getListenerShowCaseBuilder().show() }
+ buttonSequence.setOnClickListener { getSequence().show() }
+ }
+ }
+
+ //SHOW CASES GETTERS
+
+ private fun getSimpleShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Welcome!!!")
+ .description("This is a simple BubbleShowCase with default values.")
+ .targetView(binding.buttonSimpleShowCase)
+ }
+
+ private fun getCustomColorShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Custom your bubble style!")
+ .description("It is possible to change the text color, background ... and you can even add an image into your bubble.")
+ .backgroundColor(ContextCompat.getColor(this, R.color.colorBlueGray))
+ .image(ContextCompat.getDrawable(this, R.drawable.ic_color)!!)
+ .textColor(ContextCompat.getColor(this, R.color.colorBlack))
+ .targetView(binding.buttonColorShowCase)
+ }
+
+ private fun getTextSizeShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Change text sizes!")
+ .description("You can also choose the best text size for you.")
+ .backgroundColor(ContextCompat.getColor(this, R.color.colorTeal))
+ .image(ContextCompat.getDrawable(this, R.drawable.ic_format_size)!!)
+ .titleTextSize(18)
+ .descriptionTextSize(16)
+ .closeActionImage(null)
+ .showNextButton(true)
+ .targetView(binding.buttonTextSizeShowCase)
+ }
+
+ private fun getArrowLeftShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Force the position of the bubble!")
+ .description("You only have to specify in which side you want the arrow, and the bubble will be located depending on it.")
+ .arrowPosition(BubbleShowCase.ArrowPosition.LEFT)
+ .backgroundColor(ContextCompat.getColor(this, R.color.colorRed))
+ .targetView(binding.buttonArrowLeftShowCase)
+ }
+
+ private fun getArrowRightShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Arrow set on right side this time :)")
+ .arrowPosition(BubbleShowCase.ArrowPosition.RIGHT)
+ .backgroundColor(ContextCompat.getColor(this, R.color.colorPink))
+ .targetView(binding.buttonArrowRightShowCase)
+ }
+
+
+ private fun getListenerShowCaseBuilder(): BubbleShowCaseBuilder {
+ return BubbleShowCaseBuilder(this)
+ .title("Listen user actions!")
+ .description("You can detect when the user interacts with the different view elements to act consequently.")
+ .backgroundColor(ContextCompat.getColor(this, R.color.colorOrange))
+ .image(ContextCompat.getDrawable(this, R.drawable.ic_sentiment_satisfied)!!)
+ .listener(object : BubbleShowCaseListener {
+ override fun onBubbleClick(bubbleShowCase: BubbleShowCase) {
+ Toast.makeText(this@MainActivity, "OnBubbleClick", Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onBackgroundDimClick(bubbleShowCase: BubbleShowCase) {
+ Toast.makeText(this@MainActivity, "OnBackgroundDimClick", Toast.LENGTH_SHORT)
+ .show()
+ }
+
+ override fun onTargetClick(bubbleShowCase: BubbleShowCase) {
+ Toast.makeText(this@MainActivity, "OnTargetClick", Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onCloseActionImageClick(bubbleShowCase: BubbleShowCase) {
+ Toast.makeText(this@MainActivity, "OnClose", Toast.LENGTH_SHORT).show()
+ }
+ })
+ .targetView(binding.buttonEventListener)
+ }
+
+ private fun getSequence(): BubbleShowCaseSequence {
+ return BubbleShowCaseSequence().addShowCases(
+ listOf(
+ getSimpleShowCaseBuilder(),
+ getCustomColorShowCaseBuilder(),
+ getTextSizeShowCaseBuilder(),
+ getArrowLeftShowCaseBuilder(),
+ getArrowRightShowCaseBuilder(),
+ getListenerShowCaseBuilder()
+ )
+ )
+ }
+
+}
diff --git a/app/src/main/res/drawable-v24/ic_close.png b/app/src/main/res/drawable-v24/ic_close.png
new file mode 100644
index 0000000..783a624
Binary files /dev/null and b/app/src/main/res/drawable-v24/ic_close.png differ
diff --git a/app/src/main/res/drawable-v24/ic_collections.png b/app/src/main/res/drawable-v24/ic_collections.png
new file mode 100644
index 0000000..7009edd
Binary files /dev/null and b/app/src/main/res/drawable-v24/ic_collections.png differ
diff --git a/app/src/main/res/drawable-v24/ic_color.png b/app/src/main/res/drawable-v24/ic_color.png
new file mode 100644
index 0000000..91409b2
Binary files /dev/null and b/app/src/main/res/drawable-v24/ic_color.png differ
diff --git a/app/src/main/res/drawable-v24/ic_format_size.png b/app/src/main/res/drawable-v24/ic_format_size.png
new file mode 100644
index 0000000..3dec6ef
Binary files /dev/null and b/app/src/main/res/drawable-v24/ic_format_size.png differ
diff --git a/app/src/main/res/drawable-v24/ic_launcher_background.xml b/app/src/main/res/drawable-v24/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-v24/ic_sentiment_satisfied.png b/app/src/main/res/drawable-v24/ic_sentiment_satisfied.png
new file mode 100644
index 0000000..431b12a
Binary files /dev/null and b/app/src/main/res/drawable-v24/ic_sentiment_satisfied.png differ
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..c2045e8
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..d8a70db
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..d8a70db
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..28d4b77
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9287f50
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
new file mode 100644
index 0000000..aa7d642
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..9126ae3
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..4282d8c
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..a885100
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,15 @@
+
+
+ #3F51B5
+ #303F9F
+ #33691E
+ #E65100
+ #C62828
+ #D81B60
+ #CFD8DC
+ #FFFFFF
+ #FF4081
+ #FFF59D
+ #000
+ #00BFA5
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..03a5bd6
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ OnBoardingBubbles
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..4282d8c
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..081a28e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,10 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '7.1.0' apply false
+ id 'com.android.library' version '7.1.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.6.21' apply false
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..cd0519b
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..447531b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Sep 02 21:35:41 PKT 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/local.properties b/local.properties
new file mode 100644
index 0000000..9d2e43b
--- /dev/null
+++ b/local.properties
@@ -0,0 +1,10 @@
+## This file is automatically generated by Android Studio.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file should *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+sdk.dir=/Users/macbook/Library/Android/sdk
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..950f9f5
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,17 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "OnBoardingBubbles"
+include ':app'
+include ':OnBoardingBubbles'