Last active
April 2, 2023 09:38
-
-
Save premacck/a048652c4e6444b1dfe0a42c90b6f89e to your computer and use it in GitHub Desktop.
Custom Tooltip using https://github.com/faruktoptas/FancyShowCaseView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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() | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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