Skip to content

Commit b260c43

Browse files
committed
Add proper padding calculations and title, subtitle support with show once capability
1 parent 8b926fb commit b260c43

File tree

5 files changed

+184
-52
lines changed

5 files changed

+184
-52
lines changed

library/src/main/java/com/kemalatli/bubbleonboarding/BubbleOnboarding.kt

Lines changed: 136 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package com.kemalatli.bubbleonboarding
22

33
import android.app.Activity
4+
import android.content.Context
45
import android.graphics.Color
56
import android.view.LayoutInflater
67
import android.view.View
78
import android.view.ViewGroup
89
import androidx.annotation.ColorInt
910
import androidx.annotation.LayoutRes
11+
import androidx.annotation.StyleRes
12+
import androidx.core.view.doOnLayout
1013
import androidx.lifecycle.Lifecycle
1114
import androidx.lifecycle.LifecycleObserver
1215
import androidx.lifecycle.OnLifecycleEvent
16+
import com.google.android.material.button.MaterialButton
17+
import com.google.android.material.textview.MaterialTextView
1318
import com.kemalatli.bubbleonboarding.background.BubbleBackgroundView
1419
import com.kemalatli.bubbleonboarding.content.bubble.ArrowLocation
1520
import com.kemalatli.bubbleonboarding.content.bubble.BubbleDrawable
@@ -34,13 +39,39 @@ class BubbleOnboarding internal constructor():LifecycleObserver, View.OnClickLis
3439
private var arrowLocation: ArrowLocation = ArrowLocation.Bottom()
3540
private var bubbleType:BubbleType = BubbleType.SolidColor(Color.YELLOW)
3641
private var bubbleMargin:Int = 20
42+
private var bubbleId:String = "key"
43+
44+
private var title:String = ""
45+
@StyleRes
46+
private var titleTextAppearance:Int = R.style.TextAppearance_AppCompat_SearchResult_Title
47+
private var subtitle:String = ""
48+
@StyleRes
49+
private var subtitleTextAppearance:Int = R.style.TextAppearance_AppCompat_SearchResult_Subtitle
50+
private var okLabel:String = ""
51+
@StyleRes
52+
private var okLabelTextAppearance:Int = R.style.TextAppearance_AppCompat_Caption
53+
private var cancelLabel:String = ""
54+
@StyleRes
55+
private var cancelLabelTextAppearance:Int = R.style.TextAppearance_AppCompat_Caption
56+
57+
private var customViewUsed:Boolean = false
58+
private var showOnce:Boolean = false
3759

3860
companion object{
61+
62+
internal var isShowing:Boolean = false
63+
3964
fun with(activity: Activity): BubbleOnboarding {
4065
val bubbleOnboarding = BubbleOnboarding()
4166
bubbleOnboarding.activity=activity
4267
return bubbleOnboarding
4368
}
69+
70+
fun wasShownBefore(context:Context, bubbleId: String): Boolean {
71+
val sharedPreference = context.getSharedPreferences("bubbles", Context.MODE_PRIVATE)
72+
return sharedPreference.getBoolean(bubbleId, false)
73+
}
74+
4475
}
4576

4677
fun focusInCircle(target:View): BubbleOnboarding {
@@ -64,6 +95,7 @@ class BubbleOnboarding internal constructor():LifecycleObserver, View.OnClickLis
6495
}
6596

6697
fun customViewRes(@LayoutRes customViewRes:Int): BubbleOnboarding {
98+
customViewUsed = true
6799
this.customViewRes = customViewRes
68100
return this
69101
}
@@ -103,27 +135,111 @@ class BubbleOnboarding internal constructor():LifecycleObserver, View.OnClickLis
103135
return this
104136
}
105137

138+
fun title(title: String): BubbleOnboarding {
139+
this.title = title
140+
return this
141+
}
142+
143+
fun subtitle(subtitle: String): BubbleOnboarding {
144+
this.subtitle = subtitle
145+
return this
146+
}
147+
148+
fun okLabel(okLabel: String): BubbleOnboarding {
149+
this.okLabel = okLabel
150+
return this
151+
}
152+
153+
fun cancelLabel(cancelLabel: String): BubbleOnboarding {
154+
this.cancelLabel = cancelLabel
155+
return this
156+
}
157+
158+
fun titleTextAppearance(titleTextAppearance: Int): BubbleOnboarding {
159+
this.titleTextAppearance= titleTextAppearance
160+
return this
161+
}
162+
163+
fun subtitleTextAppearance(subtitleTextAppearance: Int): BubbleOnboarding {
164+
this.subtitleTextAppearance = subtitleTextAppearance
165+
return this
166+
}
167+
168+
fun okLabelTextAppearance(okLabelTextAppearance: Int): BubbleOnboarding {
169+
this.okLabelTextAppearance = okLabelTextAppearance
170+
return this
171+
}
172+
173+
fun cancelLabelTextAppearance(cancelLabelTextAppearance: Int): BubbleOnboarding {
174+
this.cancelLabelTextAppearance = cancelLabelTextAppearance
175+
return this
176+
}
177+
178+
fun showOnce(bubbleId:String): BubbleOnboarding {
179+
this.showOnce = true
180+
this.bubbleId = bubbleId
181+
return this
182+
}
183+
106184
fun show():BubbleOnboarding{
107185
activity.let {
108-
if(it==null) return@let
109-
// Lifecycle
110-
this.lifecycle?.addObserver(this)
111-
// Background View
112-
backgroundView = BubbleBackgroundView(it)
113-
backgroundView?.focalShape = focalShape
114-
backgroundView?.maskColor = backColor
115-
backgroundView?.setOnClickListener(this)
116-
(it.window.decorView as ViewGroup?)?.addView(backgroundView)
117-
// Informative view
118-
val bubble = LayoutInflater.from(it).inflate(customViewRes, null)
119-
val builder = BubbleDrawable.Builder()
120-
.angle(angle)
121-
.arrowWidth(arrowWidth)
122-
.arrowHeight(arrowHeight)
123-
.arrowLocation(arrowLocation)
124-
.bubbleMargin(bubbleMargin)
125-
.bubbleType(bubbleType)
126-
backgroundView?.addBubble(bubble, builder)
186+
187+
// Check
188+
if(isShowing) return@let
189+
requireNotNull(it)
190+
requireNotNull(focalShape)
191+
192+
// Check if the balloon was shown before
193+
val sharedPreference = it.getSharedPreferences("bubbles", Context.MODE_PRIVATE)
194+
if(showOnce && sharedPreference.getBoolean(bubbleId, false)){
195+
return@let
196+
}
197+
198+
// Wait for view layout
199+
focalShape?.prepare {
200+
// Lifecycle
201+
this.lifecycle?.addObserver(this)
202+
// Background View
203+
backgroundView = BubbleBackgroundView(it)
204+
backgroundView?.focalShape = focalShape
205+
backgroundView?.maskColor = backColor
206+
backgroundView?.setOnClickListener(this)
207+
(it.window.decorView as ViewGroup?)?.addView(backgroundView)
208+
// Informative view
209+
val bubble = LayoutInflater.from(it).inflate(customViewRes, null)
210+
// Set texts and styles
211+
if(!customViewUsed){
212+
bubble.findViewById<MaterialTextView>(R.id.title).apply {
213+
text = title
214+
setTextAppearance(context, titleTextAppearance)
215+
}
216+
bubble.findViewById<MaterialTextView>(R.id.subtitle).apply {
217+
text = subtitle
218+
setTextAppearance(context, subtitleTextAppearance)
219+
}
220+
bubble.findViewById<MaterialButton>(R.id.ok).apply {
221+
text = okLabel
222+
setTextAppearance(context, okLabelTextAppearance)
223+
setOnClickListener { clear() }
224+
}
225+
bubble.findViewById<MaterialButton>(R.id.cancel).apply {
226+
text = cancelLabel
227+
setTextAppearance(context, cancelLabelTextAppearance)
228+
setOnClickListener { clear() }
229+
}
230+
}
231+
// Build
232+
val builder = BubbleDrawable.Builder()
233+
.angle(angle)
234+
.arrowWidth(arrowWidth)
235+
.arrowHeight(arrowHeight)
236+
.arrowLocation(arrowLocation)
237+
.bubbleMargin(bubbleMargin)
238+
.bubbleType(bubbleType)
239+
backgroundView?.addBubble(bubble, builder)
240+
isShowing = true
241+
sharedPreference.edit().putBoolean(bubbleId, true).apply()
242+
}
127243
}
128244
return this
129245
}
@@ -133,6 +249,7 @@ class BubbleOnboarding internal constructor():LifecycleObserver, View.OnClickLis
133249
(activity?.window?.decorView as ViewGroup?)?.removeView(backgroundView)
134250
activity = null
135251
focalShape = null
252+
isShowing = false
136253
}
137254

138255
override fun onClick(v: View?) {

library/src/main/java/com/kemalatli/bubbleonboarding/background/BubbleBackgroundView.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ internal class BubbleBackgroundView @JvmOverloads constructor(context: Context,
4242
var arrowCenter = true
4343
var left = 0
4444
var top = 0
45-
var bottom = 0
46-
var right = 0
45+
var bubbleWidth = bubble.measuredWidth.toFloat()
46+
var bubbleHeight = bubble.measuredHeight.toFloat()
4747
when(builder.arrowLocation){
4848
is ArrowLocation.Bottom -> {
4949
left = (shape.viewX - (bubble.measuredWidth-shape.width)/2).toInt()
50-
top = (shape.viewY - bubble.measuredHeight - builder.bubbleMargin).toInt()
50+
top = (shape.viewY - bubble.measuredHeight - builder.bubbleMargin-builder.arrowHeight).toInt()
5151
// Check right
5252
if(left + bubble.measuredWidth > screenWidth){
5353
left = screenWidth - bubble.measuredWidth
@@ -60,10 +60,11 @@ internal class BubbleBackgroundView @JvmOverloads constructor(context: Context,
6060
builder.arrowPosition(shape.viewX+shape.width/2-left)
6161
arrowCenter = false
6262
}
63+
bubbleHeight += builder.arrowHeight
6364
}
6465
is ArrowLocation.Top -> {
6566
left = (shape.viewX - (bubble.measuredWidth-shape.width)/2).toInt()
66-
top = (shape.viewY + shape.heigth + builder.bubbleMargin).toInt()
67+
top = (shape.viewY + shape.heigth + builder.bubbleMargin+builder.arrowHeight).toInt()
6768
// Check right
6869
if(left + bubble.measuredWidth > screenWidth){
6970
left = screenWidth - bubble.measuredWidth
@@ -76,22 +77,25 @@ internal class BubbleBackgroundView @JvmOverloads constructor(context: Context,
7677
builder.arrowPosition(shape.viewX+shape.width/2-left)
7778
arrowCenter = false
7879
}
80+
bubbleHeight += builder.arrowHeight
7981
}
8082
is ArrowLocation.Left -> {
81-
left = (shape.viewX + shape.width + builder.bubbleMargin).toInt()
83+
left = (shape.viewX + shape.width + builder.bubbleMargin+builder.arrowHeight).toInt()
8284
top = (shape.viewY - (bubble.measuredHeight-shape.heigth)/2).toInt()
85+
bubbleWidth += builder.arrowHeight
8386
}
8487
is ArrowLocation.Right -> {
85-
left = (shape.viewX - builder.bubbleMargin - bubble.measuredWidth).toInt()
88+
left = (shape.viewX - builder.bubbleMargin - bubble.measuredWidth-builder.arrowHeight).toInt()
8689
top = (shape.viewY - (bubble.measuredHeight-shape.heigth)/2).toInt()
90+
bubbleWidth += builder.arrowHeight
8791
}
8892
}
8993
val layoutParams = LayoutParams(WRAP_CONTENT, WRAP_CONTENT)
90-
layoutParams.setMargins(left, top, right, bottom)
94+
layoutParams.setMargins(left, top, 0, 0)
9195
bubble.layoutParams = layoutParams
9296
addView(bubble)
9397
// Add bubble background
94-
bubble.background = builder.rect(RectF(0f,0f, bubble.measuredWidth.toFloat(), bubble.measuredHeight.toFloat())).arrowCenter(arrowCenter).build()
98+
bubble.background = builder.rect(RectF(0f,0f, bubbleWidth, bubbleHeight)).arrowCenter(arrowCenter).build()
9599

96100
}
97101

library/src/main/java/com/kemalatli/bubbleonboarding/content/bubble/BubbleDrawable.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class BubbleDrawable: Drawable {
4242
// Create bitmap
4343
val bitmap = Bitmap.createBitmap(rectangle.width().toInt() , rectangle.height().toInt(), Bitmap.Config.ARGB_8888)
4444
val bitmapCanvas = Canvas(bitmap)
45-
val linearGradient = LinearGradient(0f, 0f, rectangle.width(), rectangle.width() * tan(Math.toRadians(type.ange.toDouble())).toFloat(), type.startColor, type.endColor, Shader.TileMode.CLAMP)
45+
val linearGradient = LinearGradient(0f, 0f, rectangle.height() * tan(Math.toRadians(type.ange.toDouble())).toFloat(), rectangle.height(), type.startColor, type.endColor, Shader.TileMode.CLAMP)
4646
paint.isDither = true
4747
paint.shader = linearGradient
4848
bitmapCanvas.drawRect(rectangle, paint)

library/src/main/java/com/kemalatli/bubbleonboarding/focus/base/FocalShape.kt

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ package com.kemalatli.bubbleonboarding.focus.base
33
import android.graphics.Canvas
44
import android.graphics.Paint
55
import android.view.View
6+
import androidx.core.view.doOnLayout
67

7-
abstract class FocalShape(private val view:View){
8+
abstract class FocalShape(val view:View){
89

9-
val viewX:Float
10-
val viewY:Float
11-
val width:Float
12-
val heigth:Float
10+
var viewX:Float = 0f
11+
var viewY:Float = 0f
12+
var width:Float = 0f
13+
var heigth:Float = 0f
1314

14-
init {
15-
val points = IntArray(2)
16-
view.getLocationOnScreen(points)
17-
viewX = points[0].toFloat()
18-
viewY = points[1].toFloat()
19-
width = view.width.toFloat()
20-
heigth = view.height.toFloat()
15+
fun prepare(ready:()->Unit){
16+
view.doOnLayout {
17+
val points = IntArray(2)
18+
view.getLocationOnScreen(points)
19+
viewX = points[0].toFloat()
20+
viewY = points[1].toFloat()
21+
width = view.width.toFloat()
22+
heigth = view.height.toFloat()
23+
ready.invoke()
24+
}
2125
}
2226

2327
abstract fun draw(canvas: Canvas?, paint: Paint?)

library/src/main/res/layout/view_bubble.xml

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,52 +12,59 @@
1212

1313
<TextView
1414
android:id="@+id/title"
15-
android:layout_marginTop="22dp"
15+
android:maxWidth="300dp"
16+
android:layout_marginTop="10dp"
1617
android:layout_marginHorizontal="22dp"
1718
android:textColor="@android:color/black"
18-
android:textSize="16sp"
1919
android:text="¿Lo has probado y te ha gustado? "
2020
app:layout_constraintTop_toTopOf="parent"
2121
app:layout_constraintStart_toStartOf="parent"
22-
app:layout_constraintEnd_toEndOf="parent"
2322
android:layout_width="wrap_content"
2423
android:layout_height="wrap_content">
2524
</TextView>
2625

2726
<TextView
2827
android:id="@+id/subtitle"
28+
android:maxWidth="300dp"
2929
android:layout_marginTop="8dp"
3030
android:layout_marginHorizontal="22dp"
3131
android:textColor="@android:color/black"
32-
android:textSize="14sp"
3332
android:text="¡Cuenta a otrxs Pickers qué es lo que más te ha gustado de este producto!"
3433
app:layout_constraintTop_toBottomOf="@id/title"
3534
app:layout_constraintStart_toStartOf="parent"
3635
app:layout_constraintEnd_toEndOf="parent"
37-
android:layout_width="0dp"
36+
android:layout_width="wrap_content"
3837
android:layout_height="wrap_content">
3938
</TextView>
4039

4140
<com.google.android.material.button.MaterialButton
4241
android:id="@+id/ok"
43-
android:textSize="14sp"
4442
app:layout_constraintStart_toStartOf="parent"
4543
app:layout_constraintTop_toBottomOf="@id/subtitle"
44+
app:layout_constraintBottom_toBottomOf="parent"
4645
style="@style/Widget.MaterialComponents.Button.TextButton"
47-
android:text="¡Top!"
46+
android:text="Ok"
47+
android:textAllCaps="false"
48+
android:layout_marginTop="10dp"
49+
android:layout_marginBottom="10dp"
50+
android:padding="0dp"
4851
android:layout_width="wrap_content"
49-
android:layout_height="wrap_content">
52+
android:layout_height="30dp">
5053
</com.google.android.material.button.MaterialButton>
5154

5255
<com.google.android.material.button.MaterialButton
53-
android:id="@+id/skip"
54-
android:textSize="14sp"
56+
android:id="@+id/cancel"
5557
app:layout_constraintStart_toEndOf="@id/ok"
5658
app:layout_constraintTop_toBottomOf="@id/subtitle"
59+
app:layout_constraintBottom_toBottomOf="parent"
5760
style="@style/Widget.MaterialComponents.Button.TextButton"
58-
android:text="¡Top!"
61+
android:text="Cancel"
62+
android:textAllCaps="false"
63+
android:layout_marginTop="10dp"
64+
android:layout_marginBottom="10dp"
65+
android:padding="0dp"
5966
android:layout_width="wrap_content"
60-
android:layout_height="wrap_content">
67+
android:layout_height="30dp">
6168
</com.google.android.material.button.MaterialButton>
6269

6370
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)