Browse Source

Android: Add Snackbar UI component

Anish Mishra 6 months ago
parent
commit
b89957efce

+ 27 - 0
platform/android/java/lib/res/layout/snackbar.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="@android:drawable/dialog_holo_dark_frame"
+    android:gravity="center_vertical"
+    android:orientation="horizontal">
+
+    <TextView
+        android:id="@+id/snackbar_text"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:text=""
+        android:textColor="@android:color/white"
+        android:textSize="14sp"
+        android:padding="8dp"/>
+
+    <Button
+        android:id="@+id/snackbar_action"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="#00FFFFFF"
+        android:text="Action"
+        android:textColor="#61B7FC"
+        android:paddingHorizontal="8dp"/>
+</LinearLayout>

+ 1 - 0
platform/android/java/lib/res/values/dimens.xml

@@ -5,4 +5,5 @@
 	<dimen name="button_padding">10dp</dimen>
 	<dimen name="dialog_padding_horizontal">16dp</dimen>
 	<dimen name="dialog_padding_vertical">8dp</dimen>
+	<dimen name="snackbar_bottom_margin">10dp</dimen>
 </resources>

+ 96 - 0
platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt

@@ -33,11 +33,21 @@ package org.godotengine.godot.utils
 import android.app.Activity
 import android.app.AlertDialog
 import android.content.DialogInterface
+import android.os.Handler
+import android.os.Looper
+import android.view.Gravity
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
 import android.widget.Button
 import android.widget.EditText
 import android.widget.LinearLayout
+import android.widget.PopupWindow
+import android.widget.TextView
 
 import org.godotengine.godot.R
+import kotlin.math.abs
 
 /**
  * Utility class for managing dialogs.
@@ -183,5 +193,91 @@ internal class DialogUtils {
 				dialog.show()
 			}
 		}
+
+		/**
+		 * Displays a Snackbar with an optional action button.
+		 *
+		 * @param context The Context in which the Snackbar should be displayed.
+		 * @param message The message to display in the Snackbar.
+		 * @param duration The duration for which the Snackbar should be visible (in milliseconds).
+		 * @param actionText (Optional) The text for the action button. If `null`, the button is hidden.
+		 * @param actionCallback (Optional) A callback function to execute when the action button is clicked. If `null`, no action is performed.
+		 */
+		fun showSnackbar(activity: Activity, message: String, duration: Long = 3000, actionText: String? = null, action: (() -> Unit)? = null) {
+			activity.runOnUiThread {
+				val bottomMargin = activity.resources.getDimensionPixelSize(R.dimen.snackbar_bottom_margin)
+				val inflater = LayoutInflater.from(activity)
+				val customView = inflater.inflate(R.layout.snackbar, null)
+
+				val popupWindow = PopupWindow(
+					customView,
+					ViewGroup.LayoutParams.MATCH_PARENT,
+					ViewGroup.LayoutParams.WRAP_CONTENT,
+				)
+
+				val messageView = customView.findViewById<TextView>(R.id.snackbar_text)
+				messageView.text = message
+
+				val actionButton = customView.findViewById<Button>(R.id.snackbar_action)
+
+				if (actionText != null && action != null) {
+					actionButton.text = actionText
+					actionButton.visibility = View.VISIBLE
+					actionButton.setOnClickListener {
+						action.invoke()
+						popupWindow.dismiss()
+					}
+				} else {
+					actionButton.visibility = View.GONE
+				}
+
+				addSwipeToDismiss(customView, popupWindow)
+				popupWindow.showAtLocation(customView, Gravity.BOTTOM, 0, bottomMargin)
+
+				Handler(Looper.getMainLooper()).postDelayed({ popupWindow.dismiss() }, duration)
+			}
+		}
+
+		private fun addSwipeToDismiss(view: View, popupWindow: PopupWindow) {
+			view.setOnTouchListener(object : View.OnTouchListener {
+				private var initialX = 0f
+				private var dX = 0f
+				private val threshold = 300f  // Swipe distance threshold.
+
+				override fun onTouch(v: View?, event: MotionEvent): Boolean {
+					when (event.action) {
+						MotionEvent.ACTION_DOWN -> {
+							initialX = event.rawX
+							dX = view.translationX
+						}
+
+						MotionEvent.ACTION_MOVE -> {
+							val deltaX = event.rawX - initialX
+							view.translationX = dX + deltaX
+						}
+
+						MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+							val finalX = event.rawX - initialX
+
+							if (abs(finalX) > threshold) {
+								// If swipe exceeds threshold, dismiss smoothly.
+								view.animate()
+									.translationX(if (finalX > 0) view.width.toFloat() else -view.width.toFloat())
+									.setDuration(200)
+									.withEndAction { popupWindow.dismiss() }
+									.start()
+							} else {
+								// If swipe is canceled, return smoothly.
+								view.animate()
+									.translationX(0f)
+									.setDuration(200)
+									.start()
+							}
+						}
+					}
+					return true
+				}
+			})
+		}
 	}
 }