diff --git a/catalog/src/main/java/com/telefonica/mistica/catalog/ui/classic/components/SnackBarCatalogFragment.kt b/catalog/src/main/java/com/telefonica/mistica/catalog/ui/classic/components/SnackBarCatalogFragment.kt
index 71e9d784d..5f78c5b9f 100644
--- a/catalog/src/main/java/com/telefonica/mistica/catalog/ui/classic/components/SnackBarCatalogFragment.kt
+++ b/catalog/src/main/java/com/telefonica/mistica/catalog/ui/classic/components/SnackBarCatalogFragment.kt
@@ -7,11 +7,11 @@ import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Button
-import android.widget.RadioButton
import androidx.fragment.app.Fragment
import com.telefonica.mistica.catalog.R
import com.telefonica.mistica.feedback.SnackbarBuilder
import com.telefonica.mistica.feedback.SnackbarLength
+import com.telefonica.mistica.input.CheckBoxInput
import com.telefonica.mistica.input.DropDownInput
import com.telefonica.mistica.input.TextInput
@@ -33,7 +33,8 @@ class SnackBarCatalogFragment : Fragment() {
val inputAction: TextInput = view.findViewById(R.id.input_snackbar_action)
val dropDownInput: DropDownInput = view.findViewById(R.id.dropdown_snackbar_type)
val createButton: Button = view.findViewById(R.id.button_create_snackbar)
- val snackbarLength10: RadioButton = view.findViewById(R.id.radio_button_10_sec)
+ val snackbarIndefiniteLength: CheckBoxInput = view.findViewById(R.id.infinite_length_checkbox)
+ val alwaysShowDismiss: CheckBoxInput = view.findViewById(R.id.always_show_dismiss_checkbox)
with(dropDownInput.dropDown) {
setAdapter(
@@ -51,21 +52,34 @@ class SnackBarCatalogFragment : Fragment() {
SnackbarBuilder(view, inputText.text.toString()).apply {
inputAction.text.toString().let { actionText ->
if (actionText.isNotEmpty()) {
- withAction(actionText, { })
+ withAction(actionText) { }
}
}
- val duration = when {
- snackbarLength10.isChecked -> SnackbarLength.LONG
- else -> SnackbarLength.SHORT
+ if (alwaysShowDismiss.isChecked()) {
+ withDismiss()
}
+
+ val withIndefiniteLength = snackbarIndefiniteLength.isChecked()
when (SnackBarType.valueOf(dropDownInput.dropDown.text.toString())) {
- SnackBarType.INFORMATIVE -> showInformative(duration)
- SnackBarType.CRITICAL -> showCritical(duration)
+ SnackBarType.INFORMATIVE -> show(withIndefiniteLength, SnackbarBuilder::showInformative, SnackbarBuilder::showInformative)
+ SnackBarType.CRITICAL -> show(withIndefiniteLength, SnackbarBuilder::showCritical, SnackbarBuilder::showCritical)
}
}
}
}
+ private inline fun SnackbarBuilder.show(
+ withIndefiniteLength: Boolean,
+ showWithLength: SnackbarBuilder.(SnackbarLength) -> Unit,
+ showWithoutLength: SnackbarBuilder.() -> Unit,
+ ) {
+ if (withIndefiniteLength) {
+ showWithLength(SnackbarLength.INDEFINITE)
+ } else {
+ showWithoutLength()
+ }
+ }
+
private fun View.hideKeyboard() {
val inputMethodManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
diff --git a/catalog/src/main/res/layout/screen_snackbar_catalog.xml b/catalog/src/main/res/layout/screen_snackbar_catalog.xml
index ece7ced60..8b5cb5db3 100644
--- a/catalog/src/main/res/layout/screen_snackbar_catalog.xml
+++ b/catalog/src/main/res/layout/screen_snackbar_catalog.xml
@@ -31,41 +31,29 @@
android:inputType="text"
app:inputHint="SnackBar Action Text" />
-
-
-
-
-
+ android:layout_gravity="start"
+ app:inputChecked="false"
+ app:inputCheckText="Always show dismiss"/>
-
+ android:layout_marginTop="8dp"
+ android:layout_gravity="start"
+ app:inputChecked="false"
+ app:inputCheckText="Infinite length"/>
-
-
-
+ app:inputHint="SnackBar Type" />
@@ -20,9 +21,25 @@ Builder allows SnackBar customization:
* Adds an action with the given string resource and the given click listener
* `withCallback(Callback callback)`
* Adds a callback for dismiss action. Dismiss action by definition will only work when using a coordinator layout as anchor view for the SnackBar.
+* `withDismiss()`
+ * Adds a dismiss button to the Snackbar layout.
-Finally, depending on the type of SnackBar, use one of the following to display it.
-These methods allow an argument to choose the duration betweeen SHORT (5 seconds) or LONG (10 seconds). SHORT is the default.
+Finally, depending on the type of SnackBar, use one of the following methods to display it:
+* `showInformative(snackbarLength: SnackbarLength)`
* `showInformative()`
-* `showInformative(SnackbarLength.SHORT)`
+* `showCritical(snackbarLength: SnackbarLength)`
* `showCritical()`
+
+Where `SnackbarLength` has three different possible values:
+* `SHORT`: 5 seconds
+* `LONG`: 10 seconds
+* `INDEFINITE`: The Snackbar won't dismiss unless it is done manually
+
+If no `SnackbarLength` is provided, the following logic will be applied:
+* If no action is provided: default length will be `SHORT`
+* If an action is provided: default length will be `LONG`
+
+However, if a `SnackbarLength` is provided, the following logic is applied:
+- If `LONG` length is provided and there is no action, `SHORT` will be set instead.
+- If `SHORT` length is provided and there is an action, `LONG` will be set instead.
+- `INFINTE` length is valid with and without an action.
diff --git a/library/src/main/java/com/telefonica/mistica/feedback/SnackbarBuilder.kt b/library/src/main/java/com/telefonica/mistica/feedback/SnackbarBuilder.kt
index 8b2ac5b27..ce8180dea 100644
--- a/library/src/main/java/com/telefonica/mistica/feedback/SnackbarBuilder.kt
+++ b/library/src/main/java/com/telefonica/mistica/feedback/SnackbarBuilder.kt
@@ -1,16 +1,19 @@
package com.telefonica.mistica.feedback
+import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.text.Spannable
import android.text.SpannableString
import android.text.style.ForegroundColorSpan
+import android.view.LayoutInflater
import android.view.View
-import android.widget.TextView
import androidx.annotation.AttrRes
import androidx.annotation.StringRes
+import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
import com.google.android.material.snackbar.Snackbar
import com.telefonica.mistica.R
import com.telefonica.mistica.feedback.SnackBarBehaviorConfig.areSticky
+import com.telefonica.mistica.feedback.snackbar.CustomSnackbarLayout
import com.telefonica.mistica.util.getThemeColor
open class SnackbarBuilder(view: View?, text: String) {
@@ -20,6 +23,10 @@ open class SnackbarBuilder(view: View?, text: String) {
private var actionText: String? = null
private var actionListener: View.OnClickListener? = null
private var callback: Snackbar.Callback? = null
+ private var withDismiss = false
+
+ private val hasAction: Boolean
+ get() = actionText != null
constructor(view: View, @StringRes resId: Int) : this(view, view.resources.getString(resId))
@@ -41,6 +48,10 @@ open class SnackbarBuilder(view: View?, text: String) {
this.callback = callback
}
+ open fun withDismiss(): SnackbarBuilder = apply{
+ this.withDismiss = true
+ }
+
@JvmOverloads
open fun showInformative(snackbarLength: SnackbarLength = SnackbarLength.SHORT): Snackbar {
val spannable = getSpannable(R.attr.colorTextPrimaryInverse)
@@ -62,7 +73,7 @@ open class SnackbarBuilder(view: View?, text: String) {
}
private fun setActionTextColor(snackbar: Snackbar, @AttrRes colorRes: Int) {
- snackbar.setActionTextColor(view.context.getThemeColor(colorRes))
+ snackbar.getCustomLayout().setActionTextColor(view.context.getThemeColor(colorRes))
}
private fun setBackgroundColor(snackbar: Snackbar, @AttrRes colorRes: Int) {
@@ -82,47 +93,100 @@ open class SnackbarBuilder(view: View?, text: String) {
return spannable
}
- @Suppress("DEPRECATION")
private fun createSnackbar(text: CharSequence, snackbarLength: SnackbarLength): Snackbar {
val duration = when {
- areSticky() -> Snackbar.LENGTH_INDEFINITE
- actionText != null -> SnackbarLength.LONG.duration()
- else -> snackbarLength.duration()
- }
- val snackbar = Snackbar.make(view, text, duration)
- setTextStyles(snackbar)
- if (actionText != null) {
- snackbar.setAction(actionText, actionListener)
- }
- if (callback != null) {
- snackbar.setCallback(callback)
- }
+ areSticky() -> SnackbarLength.INDEFINITE
+ isInvalidLengthWhenThereIsAction(snackbarLength) -> SnackbarLength.LONG
+ isInvalidLengthWhenThereIsNoAction(snackbarLength) -> SnackbarLength.SHORT
+ else -> snackbarLength
+ }.duration()
+
+ val snackbar = inflateCustomSnackbar(duration)
+
+ snackbar.getCustomLayout().setText(text)
+ snackbar.setCustomAction()
+ snackbar.showDismissActionIfNeeded(hasInfiniteDuration = snackbarLength == SnackbarLength.INDEFINITE)
+ snackbar.addCallbackIfNeeded()
+
+ return snackbar
+ }
+
+ private fun isInvalidLengthWhenThereIsAction(length: SnackbarLength): Boolean =
+ hasAction && length == SnackbarLength.SHORT
+
+ private fun isInvalidLengthWhenThereIsNoAction(length: SnackbarLength): Boolean =
+ !hasAction && length == SnackbarLength.LONG
+
+ // We are inflating a custom layout instead of reusing existing Snackbar layout implementation because
+ // we need to add a dismiss X button and despite of it being included by Material 3 definition,
+ // that dismiss button is not supported at the moment in the Android Material library.
+ // See: https://github.com/material-components/material-components-android/issues/3049
+ @SuppressLint("ShowToast")
+ private fun inflateCustomSnackbar(duration: Int): Snackbar {
+ // Since we are inflating a custom layout, we pass a dummy text and apply
+ // the expected one later on to our custom TextView
+ val snackbar = Snackbar.make(view, "", duration)
+ val snackbarLayout = snackbar.view as Snackbar.SnackbarLayout
+
+ snackbarLayout.removeAllViews()
+ val customLayout = LayoutInflater.from(snackbarLayout.context).inflate(R.layout.snackbar_layout, snackbarLayout, false)
+ snackbarLayout.addView(customLayout)
+
return snackbar
}
- @Suppress("DEPRECATION")
- private fun setTextStyles(snackbar: Snackbar) {
- val text = snackbar.view.findViewById(R.id.snackbar_text)
- text.maxLines = MAX_TEXT_LINES
- text.setTextAppearance(text.context, R.style.AppTheme_TextAppearance_Preset2)
- val action = snackbar.view.findViewById(R.id.snackbar_action)
- action.setTextAppearance(action.context, R.style.AppTheme_TextAppearance_PresetLink)
- action.isAllCaps = false
+ private fun Snackbar.setCustomAction() {
+ actionText?.let { text ->
+ getCustomLayout().setAction(
+ actionText = text,
+ listener = {
+ actionListener?.onClick(it)
+ dispatchDismissedByActionEvent()
+ }
+ )
+ }
}
- companion object {
- private const val MAX_TEXT_LINES = 4
+ private fun Snackbar.dispatchDismissedByActionEvent() {
+ // We are overwriting the Snackbar implementation in order to have support for certain UI elements like the dismiss button.
+ // Given that, we are losing some built in capabilities such as BaseCallback.DISMISS_EVENT_ACTION event.
+ // The correct way to dispatch a BaseCallback.DISMISS_EVENT_ACTION event would be to use Snackbar::dispatchDismiss method.
+ // However that method is protected (we don't have access to it) and invoking Snackbar::dismiss with a registered callback would trigger
+ // the DISMISS_EVENT_MANUAL event. The workaround is to remove the callback if present, manually invoke the callback method and then invoking
+ // a dismiss that won't trigger a second event.
+ removeCallback(callback)
+ callback?.onDismissed(this, BaseCallback.DISMISS_EVENT_ACTION)
+ dismiss()
+ }
+
+ private fun Snackbar.getCustomLayout(): CustomSnackbarLayout =
+ this.view.findViewById(R.id.custom_layout)
+
+ private fun Snackbar.showDismissActionIfNeeded(hasInfiniteDuration: Boolean) {
+ val userShouldBeAbleToDismissSnackbar = !hasAction && hasInfiniteDuration
+
+ if (withDismiss || userShouldBeAbleToDismissSnackbar) {
+ getCustomLayout().setOnDismissClickListener { dismiss() }
+ }
+ }
+
+ private fun Snackbar.addCallbackIfNeeded() {
+ if (callback != null) {
+ addCallback(callback)
+ }
}
}
enum class SnackbarLength {
SHORT,
- LONG;
+ LONG,
+ INDEFINITE;
fun duration(): Int =
when (this) {
SHORT -> DURATION_WITHOUT_ACTION
LONG -> DURATION_WITH_ACTION
+ INDEFINITE -> Snackbar.LENGTH_INDEFINITE
}
companion object {
diff --git a/library/src/main/java/com/telefonica/mistica/feedback/snackbar/CustomSnackbarLayout.kt b/library/src/main/java/com/telefonica/mistica/feedback/snackbar/CustomSnackbarLayout.kt
new file mode 100644
index 000000000..09a6a8f95
--- /dev/null
+++ b/library/src/main/java/com/telefonica/mistica/feedback/snackbar/CustomSnackbarLayout.kt
@@ -0,0 +1,95 @@
+package com.telefonica.mistica.feedback.snackbar
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.ColorInt
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.constraintlayout.widget.ConstraintSet
+import com.telefonica.mistica.R
+
+internal class CustomSnackbarLayout @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0,
+): ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private val maxActionLengthPx: Int
+
+ init {
+ LayoutInflater.from(context).inflate(R.layout.snackbar_custom_layout_merge, this, true)
+
+ maxActionLengthPx = context.resources.getDimensionPixelSize(R.dimen.mistica_snackbar_maxActionInlineWidth)
+ }
+
+ fun setText(text: CharSequence) {
+ getText().text = text
+ }
+
+ fun setAction(actionText: CharSequence, listener: OnClickListener) {
+ getAction().run {
+ visibility = View.VISIBLE
+ text = actionText
+ setOnClickListener(listener)
+ }
+ }
+
+ fun setActionTextColor(@ColorInt color: Int) {
+ getAction().setTextColor(color)
+ }
+
+ fun setOnDismissClickListener(onDismissed: () -> Unit) {
+ getDismissButton().visibility = View.VISIBLE
+ getDismissButton().setOnClickListener { onDismissed() }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ if (actionIsTooLong()) {
+ rearrangeLayout()
+ }
+ }
+
+ private fun actionIsTooLong(): Boolean =
+ getAction().measuredWidth > maxActionLengthPx
+
+ private fun rearrangeLayout() {
+ val text = getText()
+ val action = getAction()
+ val dismissButton = getDismissButton()
+ val parent = this
+
+ val constraintSet = ConstraintSet()
+ constraintSet.clone(parent)
+
+ constraintSet.connect(text.id, ConstraintSet.END, dismissButton.id, ConstraintSet.START)
+ constraintSet.connect(text.id, ConstraintSet.BOTTOM, action.id, ConstraintSet.TOP)
+
+ constraintSet.connect(dismissButton.id, ConstraintSet.START, text.id, ConstraintSet.END)
+ constraintSet.connect(dismissButton.id, ConstraintSet.END, parent.id, ConstraintSet.END)
+ constraintSet.setHorizontalBias(dismissButton.id, 1.0F)
+ constraintSet.setVerticalBias(dismissButton.id, 0.0F)
+ constraintSet.setMargin(dismissButton.id, ConstraintSet.TOP, context.resources.getDimensionPixelSize(R.dimen.mistica_snackbar_padding_vertical))
+
+ constraintSet.connect(action.id, ConstraintSet.START, parent.id, ConstraintSet.START)
+ constraintSet.connect(action.id, ConstraintSet.BOTTOM, parent.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(action.id, ConstraintSet.TOP, text.id, ConstraintSet.BOTTOM)
+ constraintSet.connect(action.id, ConstraintSet.END, parent.id, ConstraintSet.END)
+ constraintSet.setHorizontalBias(action.id, 1.0F)
+ constraintSet.setMargin(action.id, ConstraintSet.END, context.resources.getDimensionPixelSize(R.dimen.mistica_snackbar_padding_horizontal))
+
+ constraintSet.applyTo(this)
+ }
+
+ private fun getText(): TextView =
+ findViewById(R.id.custom_snackbar_text)
+
+ private fun getAction(): TextView =
+ findViewById(R.id.custom_snackbar_action)
+
+ private fun getDismissButton(): View =
+ findViewById(R.id.custom_snackbar_dismiss)
+}
diff --git a/library/src/main/res/layout/snackbar_custom_layout_merge.xml b/library/src/main/res/layout/snackbar_custom_layout_merge.xml
new file mode 100644
index 000000000..a4e8ab913
--- /dev/null
+++ b/library/src/main/res/layout/snackbar_custom_layout_merge.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/library/src/main/res/layout/snackbar_layout.xml b/library/src/main/res/layout/snackbar_layout.xml
new file mode 100644
index 000000000..b9f23f903
--- /dev/null
+++ b/library/src/main/res/layout/snackbar_layout.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/library/src/main/res/values/dimens_snackbar.xml b/library/src/main/res/values/dimens_snackbar.xml
new file mode 100644
index 000000000..5a72531fe
--- /dev/null
+++ b/library/src/main/res/values/dimens_snackbar.xml
@@ -0,0 +1,8 @@
+
+
+ 14dp
+ 12dp
+ 128dp
+ 128dp
+ 4
+
\ No newline at end of file