Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 170 additions & 36 deletions android/src/main/java/com/menu/MenuView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
Expand All @@ -23,7 +24,9 @@ import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.RCTEventEmitter

class MenuView(context: Context) : FrameLayout(context) {
private var menuItems: List<Map<String, String>> = emptyList()
private var themeVariant: String = "system"
private var title: String = ""
private var menuItems: List<Map<String, Any>> = emptyList()
private var selectedItemIdentifier: String? = null
private var checkedColor: String = "#007AFF" // Default iOS blue
private var uncheckedColor: String = "#8E8E93" // Default iOS gray
Expand Down Expand Up @@ -105,17 +108,61 @@ class MenuView(context: Context) : FrameLayout(context) {
isFocusable = !disabled
}

fun setTitle(title: String?) {
this.title = title ?: ""
}

fun setThemeVariant(themeVariant: String?) {
this.themeVariant = themeVariant ?: "system"
}

private fun isDarkMode(): Boolean {
return when (themeVariant) {
"dark" -> true
"light" -> false
else -> {
val currentNightMode = context.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK
currentNightMode == android.content.res.Configuration.UI_MODE_NIGHT_YES
}
}
}

private fun getBackgroundColor(): Int {
return if (isDarkMode()) {
Color.parseColor("#1C1C1E") // iOS Dark Gray
} else {
Color.WHITE
}
}

private fun getTextColor(): Int {
return if (isDarkMode()) {
Color.WHITE
} else {
Color.BLACK
}
}

fun setMenuItems(menuItems: ReadableArray?) {
val items = mutableListOf<Map<String, String>>()
val items = mutableListOf<Map<String, Any>>()

menuItems?.let { array ->
for (i in 0 until array.size()) {
val item = array.getMap(i)
if (item != null) {
val menuItem = mapOf(
val menuItem = mutableMapOf<String, Any>(
"identifier" to (item.getString("identifier") ?: ""),
"title" to (item.getString("title") ?: "")
)

if (item.hasKey("subtitle")) {
menuItem["subtitle"] = item.getString("subtitle") ?: ""
}

if (item.hasKey("destructive")) {
menuItem["destructive"] = item.getBoolean("destructive")
}

items.add(menuItem)
}
}
Expand Down Expand Up @@ -178,26 +225,38 @@ class MenuView(context: Context) : FrameLayout(context) {
LinearLayout.LayoutParams.WRAP_CONTENT
)

// Set background with rounded corners - always white will add new prop soon
// Set background with rounded corners - based on theme
val drawable = android.graphics.drawable.GradientDrawable().apply {
setColor(Color.WHITE)
setColor(getBackgroundColor())
cornerRadius = 24f
}
background = drawable
}

// Header - removed as per user request
// val headerText = TextView(context).apply {
// text = "--sous-thème--"
// setTextColor(Color.WHITE)
// textSize = 18f
// setPadding(60, 40, 60, 40)
// gravity = android.view.Gravity.CENTER
// setTypeface(null, android.graphics.Typeface.BOLD)
// }
// container.addView(headerText)
// Header
if (title.isNotEmpty()) {
val headerText = TextView(context).apply {
text = title
setTextColor(getTextColor())
textSize = 18f
setPadding(60, 40, 60, 40)
gravity = android.view.Gravity.CENTER
setTypeface(null, android.graphics.Typeface.BOLD)
}
container.addView(headerText)

// Add separator after title
val separator = View(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(0.5 * context.resources.displayMetrics.density).toInt()
)
setBackgroundColor(if (isDarkMode()) Color.parseColor("#38383A") else Color.parseColor("#E0E0E0"))
}
container.addView(separator)
}

// Create a ScrollView to contain the radio group with dynamic height
// Create a ScrollView to contain the items with dynamic height
val displayMetrics = context.resources.displayMetrics
// Max 90% of screen height - allows near full screen, only scrolls when content exceeds this
val maxScrollHeight = (displayMetrics.heightPixels * 0.9).toInt()
Expand Down Expand Up @@ -239,14 +298,29 @@ class MenuView(context: Context) : FrameLayout(context) {
}

menuItems.forEachIndexed { index, item ->
// We'll create a custom view that mimics a RadioButton but allows rich content
// However, since the user specifically requested "RadioButton", we will use a RadioButton
// but we'll need to customize it heavily or wrap it to support subtitle/icon.
// A standard RadioButton in Android is a TextView with a button drawable.
// It's hard to add a subtitle/icon *inside* the RadioButton text easily without Spannables or custom compound drawables.

// To respect "restore RadioButton" but also "rich features", we can use a RadioButton
// but set its text to empty and put it inside a container with our rich views,
// OR we can just use RadioButton and try to use SpannableString for title/subtitle
// and compound drawables for icons.

// Let's try the container approach where the RadioButton is the "checkmark"
// and the whole row is clickable.

// RadioButton as the selection indicator - created first to be referenced in itemContainer listener
val radioButton = RadioButton(context).apply {
text = item["title"]
setTextColor(Color.BLACK) // Changed to black for white background
textSize = 16f
setPadding(0, 30, 0, 30)
isChecked = item["identifier"] == selectedItemIdentifier
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)

// Custom radio button styling with dynamic colors
// Custom tinting
val colorStateList = android.content.res.ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
Expand All @@ -258,33 +332,93 @@ class MenuView(context: Context) : FrameLayout(context) {
)
)
buttonTintList = colorStateList

// Make sure text wraps properly for long content
layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.MATCH_PARENT,
RadioGroup.LayoutParams.WRAP_CONTENT

// Handle click directly on radio button (though hidden from accessibility, it might still be clickable)
setOnClickListener {
selectMenuItem(item["identifier"] as String, item["title"] as String)
currentDialog?.dismiss()
}

// Hide from accessibility as the container will represent the item
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
}

val itemContainer = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 8, 0, 8)
setMargins(0, 16, 0, 16)
}
gravity = Gravity.CENTER_VERTICAL
isClickable = true
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
isFocusable = true

setOnClickListener {
selectMenuItem(item["identifier"] ?: "", item["title"] ?: "")
// Close dialog immediately when item is selected
currentDialog?.dismiss()
radioButton.performClick()
}

// Accessibility delegate to make the row act like a radio button
accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.className = RadioButton::class.java.name
info.isCheckable = true
info.isChecked = radioButton.isChecked
}
}
}

// Text Container (Title + Subtitle)
val textContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.0f
)
}

val titleView = TextView(context).apply {
text = item["title"] as String
textSize = 16f

val isDestructive = item["destructive"] as? Boolean == true
if (isDestructive) {
setTextColor(Color.parseColor("#FF3B30"))
} else {
setTextColor(getTextColor())
}
}
textContainer.addView(titleView)

val subtitle = item["subtitle"] as? String
if (!subtitle.isNullOrEmpty()) {
val subtitleView = TextView(context).apply {
text = subtitle
textSize = 14f
setTextColor(Color.parseColor("#8E8E93")) // Gray
setPadding(0, 4, 0, 0)
}
textContainer.addView(subtitleView)
}
radioGroup.addView(radioButton)

itemContainer.addView(textContainer)
itemContainer.addView(radioButton)

radioGroup.addView(itemContainer)

// Add divider between items (except after the last item)
if (index < menuItems.size - 1) {
val divider = View(context).apply {
layoutParams = RadioGroup.LayoutParams(
RadioGroup.LayoutParams.MATCH_PARENT,
(0.5 * context.resources.displayMetrics.density).toInt() // 0.5px converted to dp
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(0.5 * context.resources.displayMetrics.density).toInt()
).apply {
setMargins(0, 8, 0, 8) // Small margins around divider
setMargins(0, 8, 0, 8)
}
setBackgroundColor(Color.parseColor("#E0E0E0")) // Light gray color
setBackgroundColor(if (isDarkMode()) Color.parseColor("#38383A") else Color.parseColor("#E0E0E0"))
}
radioGroup.addView(divider)
}
Expand Down
10 changes: 10 additions & 0 deletions android/src/main/java/com/menu/MenuViewManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ class MenuViewManager : ViewGroupManager<MenuView>() {
return MenuView(reactContext)
}

@ReactProp(name = "title")
fun setTitle(view: MenuView, title: String?) {
view.setTitle(title)
}

@ReactProp(name = "themeVariant")
fun setThemeVariant(view: MenuView, themeVariant: String?) {
view.setThemeVariant(themeVariant)
}

@ReactProp(name = "color")
fun setColor(view: MenuView, color: String?) {
view.setColor(color)
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ PODS:
- hermes-engine (0.81.1):
- hermes-engine/Pre-built (= 0.81.1)
- hermes-engine/Pre-built (0.81.1)
- Menu (0.2.3):
- Menu (0.3.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2609,7 +2609,7 @@ SPEC CHECKSUMS:
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
hermes-engine: 4f8246b1f6d79f625e0d99472d1f3a71da4d28ca
Menu: 579defda82c781de66a61a4ccf37ccd026258adf
Menu: 32510b4b6f4c679ee0e6ca720c83c5a985fe5b50
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
RCTDeprecation: c4b9e2fd0ab200e3af72b013ed6113187c607077
RCTRequired: e97dd5dafc1db8094e63bc5031e0371f092ae92a
Expand Down
Loading
Loading