|
| 1 | +package org.wordpress.android.ui |
| 2 | + |
| 3 | +import android.animation.Animator |
| 4 | +import android.animation.AnimatorListenerAdapter |
| 5 | +import android.content.Context |
| 6 | +import android.util.AttributeSet |
| 7 | +import android.view.Gravity |
| 8 | +import android.view.View |
| 9 | +import android.widget.LinearLayout |
| 10 | +import android.widget.TextView |
| 11 | +import androidx.annotation.LayoutRes |
| 12 | +import org.wordpress.android.R |
| 13 | +import org.wordpress.android.ui.WPTooltipView.TooltipPosition.ABOVE |
| 14 | +import org.wordpress.android.ui.WPTooltipView.TooltipPosition.BELOW |
| 15 | +import org.wordpress.android.ui.WPTooltipView.TooltipPosition.LEFT |
| 16 | +import org.wordpress.android.ui.WPTooltipView.TooltipPosition.RIGHT |
| 17 | +import org.wordpress.android.util.RtlUtils |
| 18 | +import java.lang.IllegalArgumentException |
| 19 | + |
| 20 | +/** |
| 21 | + * Partially based on https://stackoverflow.com/a/42756576 |
| 22 | + * |
| 23 | + * NOTE: this is a very basic implementation of a tooltip component |
| 24 | + * mainly used to cover the need of onboarding/announcing in the IA Project main create FAB. |
| 25 | + * More work/rework is needed to make it a full custom view tooltip component. |
| 26 | + * A different and more dynamic approach that can be used is with PopupWindow taking care of behaviors like |
| 27 | + * CoordinatorLayout behavior (as in this scenario with FAB and snackbars) |
| 28 | + */ |
| 29 | + |
| 30 | +private const val HIDE_ANIMATION_DURATION = 50L |
| 31 | + |
| 32 | +class WPTooltipView @JvmOverloads constructor ( |
| 33 | + context: Context, |
| 34 | + attrs: AttributeSet? = null, |
| 35 | + defStyleAttr: Int = 0 |
| 36 | +) : LinearLayout(context, attrs, defStyleAttr) { |
| 37 | + private var position = LEFT |
| 38 | + private var messageId = 0 |
| 39 | + private var arrowHorizontalOffsetFromEndResId = 0 |
| 40 | + private var arrowHorizontalOffsetFromStartResId = 0 |
| 41 | + private var arrowHorizontalOffsetFromEnd = -1 |
| 42 | + private var arrowHorizontalOffsetFromStart = -1 |
| 43 | + private var animationDuration: Int |
| 44 | + |
| 45 | + init { |
| 46 | + attrs?.also { |
| 47 | + val stylesAttributes = context.theme.obtainStyledAttributes( |
| 48 | + attrs, |
| 49 | + R.styleable.WPTooltipView, |
| 50 | + 0, |
| 51 | + 0 |
| 52 | + ) |
| 53 | + |
| 54 | + try { |
| 55 | + position = TooltipPosition.fromInt( |
| 56 | + stylesAttributes.getInt(R.styleable.WPTooltipView_wpTooltipPosition, 0) |
| 57 | + ) |
| 58 | + messageId = stylesAttributes.getResourceId(R.styleable.WPTooltipView_wpTooltipMessage, 0) |
| 59 | + arrowHorizontalOffsetFromEndResId = stylesAttributes.getResourceId( |
| 60 | + R.styleable.WPTooltipView_wpArrowHorizontalOffsetFromEnd, 0 |
| 61 | + ) |
| 62 | + arrowHorizontalOffsetFromStartResId = stylesAttributes.getResourceId( |
| 63 | + R.styleable.WPTooltipView_wpArrowHorizontalOffsetFromStart, 0 |
| 64 | + ) |
| 65 | + } finally { |
| 66 | + stylesAttributes.recycle() |
| 67 | + } |
| 68 | + } |
| 69 | + |
| 70 | + inflate(getContext(), position.layout, this) |
| 71 | + val root = findViewById<LinearLayout>(R.id.root_view) |
| 72 | + val tvMessage = findViewById<TextView>(R.id.tooltip_message) |
| 73 | + val arrow = findViewById<View>(R.id.tooltip_arrow) |
| 74 | + animationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) |
| 75 | + |
| 76 | + if (messageId > 0) { |
| 77 | + tvMessage.setText(messageId) |
| 78 | + } |
| 79 | + |
| 80 | + if (RtlUtils.isRtl(context)) { |
| 81 | + when (position) { |
| 82 | + LEFT -> arrow.rotation = -90f |
| 83 | + RIGHT -> arrow.rotation = 90f |
| 84 | + ABOVE, BELOW -> {} |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + if (position == ABOVE || position == BELOW) { |
| 89 | + if (arrowHorizontalOffsetFromEndResId > 0) { |
| 90 | + arrowHorizontalOffsetFromEnd = resources.getDimensionPixelSize(arrowHorizontalOffsetFromEndResId) |
| 91 | + root.gravity = Gravity.END |
| 92 | + val lp = arrow.layoutParams as LayoutParams |
| 93 | + lp.marginEnd = arrowHorizontalOffsetFromEnd |
| 94 | + arrow.layoutParams = lp |
| 95 | + } else if (arrowHorizontalOffsetFromStartResId > 0) { |
| 96 | + arrowHorizontalOffsetFromStart = resources.getDimensionPixelSize(arrowHorizontalOffsetFromStartResId) |
| 97 | + root.gravity = Gravity.START |
| 98 | + val lp = arrow.layoutParams as LayoutParams |
| 99 | + lp.marginStart = arrowHorizontalOffsetFromStart |
| 100 | + arrow.layoutParams = lp |
| 101 | + } |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + enum class TooltipPosition(val value: Int, @LayoutRes val layout: Int) { |
| 106 | + LEFT(0, R.layout.tooltip_left), |
| 107 | + RIGHT(1, R.layout.tooltip_right), |
| 108 | + ABOVE(2, R.layout.tooltip_above), |
| 109 | + BELOW(3, R.layout.tooltip_below); |
| 110 | + |
| 111 | + companion object { |
| 112 | + fun fromInt(value: Int): TooltipPosition = |
| 113 | + values().firstOrNull { it.value == value } |
| 114 | + ?: throw IllegalArgumentException("TooltipPosition wrong value $value") |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + fun show() { |
| 119 | + this.postDelayed({ |
| 120 | + this.apply { |
| 121 | + alpha = 0f |
| 122 | + visibility = View.VISIBLE |
| 123 | + animate() |
| 124 | + .alpha(1f) |
| 125 | + .setDuration(animationDuration.toLong()) |
| 126 | + .setListener(null) |
| 127 | + } |
| 128 | + }, 400) |
| 129 | + } |
| 130 | + |
| 131 | + fun hide() { |
| 132 | + this.apply { |
| 133 | + animate() |
| 134 | + .alpha(0f) |
| 135 | + .setDuration(HIDE_ANIMATION_DURATION) |
| 136 | + .setListener(object : AnimatorListenerAdapter() { |
| 137 | + override fun onAnimationEnd(animation: Animator?) { |
| 138 | + super.onAnimationEnd(animation) |
| 139 | + visibility = View.GONE |
| 140 | + } |
| 141 | + }) |
| 142 | + } |
| 143 | + } |
| 144 | +} |
0 commit comments