Skip to content

Instantly share code, notes, and snippets.

@premacck
Last active May 15, 2020
Embed
What would you like to do?
/**
* Function to start circular tooltip queue form a fragment
*/
fun Fragment.roundTooltipOf(tooltipItem: TooltipItem, anchor: View?) = if (anchor != null) {
activity?.let { TooltipHandler.prepare(it, tooltipItem, anchor, false) }
} else null
/**
* Function to start rounded rectangular tooltip queue form a fragment
*/
fun Fragment.rectTooltipOf(tooltipItem: TooltipItem, anchor: View?) = if (anchor != null) {
activity?.let { TooltipHandler.prepare(it, tooltipItem, anchor, true) }
} else null
class TooltipFragmentExample : Fragment() {
private fun showTooltip() {
// Make sure to dispose the handler when the fragment's view is destroyed
handler.postDelayed(1000) {
TooltipQueue.inside(nested_scroll_view_parent).withHandlers(
rectTooltipOf(TooltipItem.tooltip1(), anchor_1_view),
roundTooltipOf(TooltipItem.tooltip2(), anchor_2_view),
rectTooltipOf(TooltipItem.tooltip3 { /* Some custom action */ }, anchor_3_view)
).startShowing()
}
}
}
/**
* Prem's creation, on 2020-02-27
*
* Helper class for initializing the [FancyShowCaseView] tooltip with custom [R.layout.layout_onboarding_tooltip] layout XML
* Note: this class should only be responsible for INITIALIZING the tooltip, along with the listeners to hide it and show next tooltip, but not showing it.
*/
class TooltipHandler {
var tooltipView: FancyShowCaseView? = null
var anchor: View? = null
var onSkipListener: TooltipSkipListener? = null
companion object {
/**
* Function to initialize the [FancyShowCaseView] with the given parameters
*
* @param activity the [Activity] in which the tooltip should be shown
* @param tooltipItem the [TooltipItem] containing the data of the tooltip to be shown
* @param anchor the view on which the focus should be
* @param isRoundRect whether the focus of the [anchor] should be round-rectangular or not. Pass false to show circular focus
*/
fun prepare(activity: Activity, tooltipItem: TooltipItem, anchor: View, isRoundRect: Boolean) = TooltipHandler().apply {
this.anchor = anchor
tooltipView = FancyShowCaseView.Builder(activity).apply {
focusOn(anchor)
customView(R.layout.layout_onboarding_tooltip, object : OnViewInflateListener {
override fun onViewInflated(view: View) {
view.setLayout(tooltipItem)
view.setListeners(tooltipItem)
(view.layoutParams as? ViewGroup.MarginLayoutParams)?.let { params ->
params.topMargin = tooltipView?.focusCenterY.orZero() + tooltipView?.focusHeight.orZero() / 2 + view.dip(if (isRoundRect) 14 else 32)
}
(view.iv_pointer?.layoutParams as? ViewGroup.MarginLayoutParams)?.let { params ->
params.leftMargin = tooltipView?.focusCenterX.orZero() - view.dip(13)
}
}
})
if (isRoundRect) {
focusShape(FocusShape.ROUNDED_RECTANGLE)
roundRectRadius(activity.dip(8))
}
closeOnTouch(false)
showOnce(tooltipItem.type)
}.build()
}
}
private fun View.setLayout(tooltip: TooltipItem) {
iv_onboarding_thumbnail?.imageResource = tooltip.icon
tv_onboarding_title?.textResource = tooltip.title
tv_onboarding_message?.textResource = tooltip.message
btn_finish?.textResource = tooltip.positiveButtonTextRes
btn_skip?.textResource = tooltip.negativeButtonTextRes
cpi_onboarding?.showImageIconIndicator(tooltip.totalTooltipsInSeries)
cpi_onboarding?.handleViewPagerScroll(tooltip.totalTooltipsInSeries, tooltip.orderInSeries)
}
private fun View.setListeners(tooltip: TooltipItem) {
if (tooltip.action != null) {
btn_finish?.onDebounceClick {
onSkipListener?.skipAll()
tooltip.action?.invoke()
}
} else {
btn_finish?.onDebounceClick {
hide()
}
}
btn_skip?.onDebounceClick {
onSkipListener?.skipAll()
}
}
private fun hide() {
tooltipView?.hide()
}
interface TooltipSkipListener {
fun skipAll()
}
}
data class TooltipItem(
@Type val type: String,
@DrawableRes val icon: Int,
@StringRes val title: Int,
@StringRes val message: Int,
@StringRes val positiveButtonTextRes: Int,
@StringRes val negativeButtonTextRes: Int,
val orderInSeries: Int,
val totalTooltipsInSeries: Int,
var action: (() -> Unit)? = null
) {
companion object {
// Identifiers for the tooltips. Use your own
const val TOOLTIP_1 = "TOOLTIP_1"
const val TOOLTIP_2 = "TOOLTIP_2"
const val TOOLTIP_3 = "TOOLTIP_3"
fun tooltip1() = TooltipItem(
TOOLTIP_1,
R.drawable.ic_tooltip_icon,
R.string.title_1,
R.string.message_1,
R.string.continue_1,
R.string.skip_1,
orderInSeries = 0,
totalTooltipsInSeries = 3
)
fun tooltip2() = TooltipItem(
TOOLTIP_2,
R.drawable.ic_tooltip_icon,
R.string.title_2,
R.string.message_2,
R.string.continue_2,
R.string.skip_2,
orderInSeries = 1,
totalTooltipsInSeries = 3
)
fun tooltip3(action: () -> Unit) = TooltipItem(
TOOLTIP_3,
R.drawable.ic_tooltip_icon,
R.string.title_3,
R.string.message_3,
R.string.continue_3,
R.string.skip_3,
orderInSeries = 2,
totalTooltipsInSeries = 3,
action = action
)
}
@Retention @StringDef(TOOLTIP_1, TOOLTIP_2, TOOLTIP_3)
annotation class Type
}
/**
* Prem's creation, on 2020-02-27
*
* Helper class for queuing and handling the display multiple tooltips, along with the scroll of the scroll parent of anchor
* This class should be initialized by calling [inside] function and chaining [withHandlers] function to add the tooltips in the queue
* Call [startShowing] to show the first tooltip
*/
class TooltipQueue : OnQueueListener, TooltipHandler.TooltipSkipListener {
private val queue: Queue<FancyShowCaseView> = LinkedList()
private val anchors: Queue<View?> = LinkedList()
private val tooltipHandlers: ArrayList<TooltipHandler?> = ArrayList()
private var current: FancyShowCaseView? = null
private var completeListener: OnCompleteListener? = null
private var scrollParent: NestedScrollView? = null
companion object {
/**
* Initialize the class with this function.
*
* @param scrollParent the [NestedScrollView] parent containing the anchor View(s)
*/
fun inside(scrollParent: NestedScrollView?) = TooltipQueue().apply {
this.scrollParent = scrollParent
}
}
/**
* Function to add [TooltipHandler]s to the queue
*
* @param tooltipHandlers the [TooltipHandler] instances that you want to show, in ordered manner.
*
* The [tooltipHandlers] should be created using the following extension functions:
* - Fragment.[roundTooltipOf] to show round focus
* - Fragment.[rectTooltipOf] to show round-rectangular focus
*/
fun withHandlers(vararg tooltipHandlers: TooltipHandler?) = apply {
this.tooltipHandlers.addAll(tooltipHandlers.toList().apply {
forEach { it?.onSkipListener = this@TooltipQueue }
})
}
/**
* Function to start the queue and show the first tooltip
*/
fun startShowing() {
tooltipHandlers.forEach { add(it?.tooltipView, it?.anchor) }
show()
}
/**
* Adds a FancyShowCaseView and its anchor View to the queue
*
* @param showCaseView the view that should be added to the queue
* @param anchor the view that should be the anchor for the [showCaseView]
*/
fun add(showCaseView: FancyShowCaseView?, anchor: View?) {
if (showCaseView != null && anchor != null) {
queue.add(showCaseView)
anchors.add(anchor)
}
}
/**
* Starts displaying all views in order of their insertion in the queue, one after another
*/
fun show() {
if (queue.isNotEmpty()) {
current = queue.poll()?.apply {
anchors.poll()?.let { currentAnchor ->
if (!isShownBefore()) scrollToView(currentAnchor)
}
queueListener = this@TooltipQueue
scrollParent?.postDelayed(500) { show() }
}
} else {
completeListener?.onComplete()
}
}
/**
* Function to scroll the specified [scrollParent] to scroll to the [anchor] with some space on top.
* the [anchor] must be a direct descendant of the [scrollParent] for the scrolling to work properly
*/
private fun scrollToView(anchor: View) {
val scrollHeight = displayHeight / 6
val scrollY = if (anchor.top <= scrollHeight) scrollHeight else anchor.top - scrollHeight
scrollParent?.smoothScrollTo(0, scrollY)
}
/**
* Cancels the queue
* @param hideCurrent hides current FancyShowCaseView
*/
fun cancel(hideCurrent: Boolean = true) {
if (hideCurrent) current?.hide()
if (queue.isNotEmpty()) queue.clear()
}
override fun skipAll() = cancel()
override fun onNext() {
show()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment