Skip to content

Commit eb4538b

Browse files
committed
Merge pull request #102590 from syntaxerror247/custom-snackbar
Android: Add Snackbar UI component
2 parents db7f9a0 + b89957e commit eb4538b

File tree

3 files changed

+124
-0
lines changed

3 files changed

+124
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="wrap_content"
5+
android:background="@android:drawable/dialog_holo_dark_frame"
6+
android:gravity="center_vertical"
7+
android:orientation="horizontal">
8+
9+
<TextView
10+
android:id="@+id/snackbar_text"
11+
android:layout_width="0dp"
12+
android:layout_height="wrap_content"
13+
android:layout_weight="1"
14+
android:text=""
15+
android:textColor="@android:color/white"
16+
android:textSize="14sp"
17+
android:padding="8dp"/>
18+
19+
<Button
20+
android:id="@+id/snackbar_action"
21+
android:layout_width="wrap_content"
22+
android:layout_height="wrap_content"
23+
android:background="#00FFFFFF"
24+
android:text="Action"
25+
android:textColor="#61B7FC"
26+
android:paddingHorizontal="8dp"/>
27+
</LinearLayout>

platform/android/java/lib/res/values/dimens.xml

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
<dimen name="button_padding">10dp</dimen>
66
<dimen name="dialog_padding_horizontal">16dp</dimen>
77
<dimen name="dialog_padding_vertical">8dp</dimen>
8+
<dimen name="snackbar_bottom_margin">10dp</dimen>
89
</resources>

platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt

+96
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,21 @@ package org.godotengine.godot.utils
3333
import android.app.Activity
3434
import android.app.AlertDialog
3535
import android.content.DialogInterface
36+
import android.os.Handler
37+
import android.os.Looper
38+
import android.view.Gravity
39+
import android.view.LayoutInflater
40+
import android.view.MotionEvent
41+
import android.view.View
42+
import android.view.ViewGroup
3643
import android.widget.Button
3744
import android.widget.EditText
3845
import android.widget.LinearLayout
46+
import android.widget.PopupWindow
47+
import android.widget.TextView
3948

4049
import org.godotengine.godot.R
50+
import kotlin.math.abs
4151

4252
/**
4353
* Utility class for managing dialogs.
@@ -183,5 +193,91 @@ internal class DialogUtils {
183193
dialog.show()
184194
}
185195
}
196+
197+
/**
198+
* Displays a Snackbar with an optional action button.
199+
*
200+
* @param context The Context in which the Snackbar should be displayed.
201+
* @param message The message to display in the Snackbar.
202+
* @param duration The duration for which the Snackbar should be visible (in milliseconds).
203+
* @param actionText (Optional) The text for the action button. If `null`, the button is hidden.
204+
* @param actionCallback (Optional) A callback function to execute when the action button is clicked. If `null`, no action is performed.
205+
*/
206+
fun showSnackbar(activity: Activity, message: String, duration: Long = 3000, actionText: String? = null, action: (() -> Unit)? = null) {
207+
activity.runOnUiThread {
208+
val bottomMargin = activity.resources.getDimensionPixelSize(R.dimen.snackbar_bottom_margin)
209+
val inflater = LayoutInflater.from(activity)
210+
val customView = inflater.inflate(R.layout.snackbar, null)
211+
212+
val popupWindow = PopupWindow(
213+
customView,
214+
ViewGroup.LayoutParams.MATCH_PARENT,
215+
ViewGroup.LayoutParams.WRAP_CONTENT,
216+
)
217+
218+
val messageView = customView.findViewById<TextView>(R.id.snackbar_text)
219+
messageView.text = message
220+
221+
val actionButton = customView.findViewById<Button>(R.id.snackbar_action)
222+
223+
if (actionText != null && action != null) {
224+
actionButton.text = actionText
225+
actionButton.visibility = View.VISIBLE
226+
actionButton.setOnClickListener {
227+
action.invoke()
228+
popupWindow.dismiss()
229+
}
230+
} else {
231+
actionButton.visibility = View.GONE
232+
}
233+
234+
addSwipeToDismiss(customView, popupWindow)
235+
popupWindow.showAtLocation(customView, Gravity.BOTTOM, 0, bottomMargin)
236+
237+
Handler(Looper.getMainLooper()).postDelayed({ popupWindow.dismiss() }, duration)
238+
}
239+
}
240+
241+
private fun addSwipeToDismiss(view: View, popupWindow: PopupWindow) {
242+
view.setOnTouchListener(object : View.OnTouchListener {
243+
private var initialX = 0f
244+
private var dX = 0f
245+
private val threshold = 300f // Swipe distance threshold.
246+
247+
override fun onTouch(v: View?, event: MotionEvent): Boolean {
248+
when (event.action) {
249+
MotionEvent.ACTION_DOWN -> {
250+
initialX = event.rawX
251+
dX = view.translationX
252+
}
253+
254+
MotionEvent.ACTION_MOVE -> {
255+
val deltaX = event.rawX - initialX
256+
view.translationX = dX + deltaX
257+
}
258+
259+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
260+
val finalX = event.rawX - initialX
261+
262+
if (abs(finalX) > threshold) {
263+
// If swipe exceeds threshold, dismiss smoothly.
264+
view.animate()
265+
.translationX(if (finalX > 0) view.width.toFloat() else -view.width.toFloat())
266+
.setDuration(200)
267+
.withEndAction { popupWindow.dismiss() }
268+
.start()
269+
} else {
270+
// If swipe is canceled, return smoothly.
271+
view.animate()
272+
.translationX(0f)
273+
.setDuration(200)
274+
.start()
275+
}
276+
}
277+
}
278+
return true
279+
}
280+
})
281+
}
186282
}
187283
}

0 commit comments

Comments
 (0)