Skip to content

Commit e0c9865

Browse files
committed
Add custom composable for topbar/toolbar actions
(to ensure the right number of actions are displayed on the app bar based on screen width)
1 parent 6856327 commit e0c9865

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package app.grapheneos.camera.ktx
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.platform.LocalDensity
5+
import androidx.compose.ui.unit.Dp
6+
7+
8+
@Composable
9+
fun Dp.toPx() = with(LocalDensity.current) { this@toPx.toPx() }
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package app.grapheneos.camera.ui.composable.components
2+
3+
import androidx.compose.foundation.ExperimentalFoundationApi
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
7+
import androidx.compose.foundation.layout.Row
8+
import androidx.compose.foundation.layout.fillMaxWidth
9+
10+
import androidx.compose.foundation.layout.width
11+
import androidx.compose.material.icons.Icons
12+
import androidx.compose.material.icons.filled.MoreVert
13+
14+
import androidx.compose.material3.DropdownMenu
15+
import androidx.compose.material3.DropdownMenuItem
16+
import androidx.compose.material3.ExperimentalMaterial3Api
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.IconButton
19+
import androidx.compose.material3.Text
20+
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.getValue
23+
import androidx.compose.runtime.mutableStateOf
24+
import androidx.compose.runtime.remember
25+
import androidx.compose.runtime.setValue
26+
import androidx.compose.ui.Alignment
27+
28+
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.graphics.Color
30+
import androidx.compose.ui.graphics.vector.ImageVector
31+
import androidx.compose.ui.layout.onGloballyPositioned
32+
33+
import androidx.compose.ui.text.font.FontWeight
34+
35+
import androidx.compose.ui.unit.IntSize
36+
37+
import androidx.compose.ui.unit.dp
38+
import androidx.compose.ui.unit.sp
39+
import app.grapheneos.camera.ktx.toPx
40+
41+
data class TopBarAction(
42+
val id : Any,
43+
val title: String,
44+
val icon: ImageVector,
45+
val alwaysInMoreOptions : Boolean = false
46+
)
47+
48+
private const val TAG = "TopBarActions"
49+
50+
// 40.0.dp was taken from an constant internal to the material library
51+
// IconButtonTokens.StateLayerSize used by IconButton's code internally
52+
private val SPACE_PER_ICON_BUTTON = 40.dp
53+
54+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
55+
@Composable
56+
fun TopBarActions(
57+
actions : Iterable<TopBarAction>,
58+
onActionClicked: (id: Any) -> Unit,
59+
modifier: Modifier = Modifier,
60+
) {
61+
62+
var size by remember {
63+
mutableStateOf(IntSize.Zero)
64+
}
65+
66+
var dropDownMenuExpanded by remember {
67+
mutableStateOf(false)
68+
}
69+
70+
Box (
71+
modifier = Modifier
72+
.then(modifier)
73+
.onGloballyPositioned {
74+
size = it.size
75+
},
76+
contentAlignment = Alignment.CenterEnd
77+
) {
78+
if (size == IntSize.Zero) return@Box
79+
80+
val width = size.width
81+
82+
// Subtract 1 for the more options icon
83+
val maxVisibleActions = (width / SPACE_PER_ICON_BUTTON.toPx()).toInt() - 1
84+
85+
// Ensure there is space to at least have the more options icon
86+
assert(maxVisibleActions >= 0)
87+
{ "Please ensure that TopBarActions gets a width of 40.dp at least" }
88+
89+
val visibleActions = arrayListOf<TopBarAction>()
90+
val moreOptionsActions = arrayListOf<TopBarAction>()
91+
92+
for (action in actions) {
93+
if (action.alwaysInMoreOptions || visibleActions.size >= maxVisibleActions) {
94+
moreOptionsActions.add(action)
95+
} else {
96+
visibleActions.add(action)
97+
}
98+
}
99+
100+
Row (
101+
horizontalArrangement = Arrangement.End,
102+
modifier = Modifier.fillMaxWidth()
103+
) {
104+
for (visibleAction in visibleActions) {
105+
QuickTooltip(
106+
message = visibleAction.title
107+
) {
108+
IconButton(onClick = {
109+
onActionClicked(visibleAction.id)
110+
}) {
111+
Icon(visibleAction.icon, visibleAction.title)
112+
}
113+
}
114+
115+
}
116+
117+
Box {
118+
if (moreOptionsActions.isNotEmpty()) {
119+
QuickTooltip(
120+
message = "More Options"
121+
) {
122+
IconButton(onClick = {
123+
dropDownMenuExpanded = !dropDownMenuExpanded
124+
}) {
125+
Icon(
126+
Icons.Filled.MoreVert,
127+
"More Options"
128+
)
129+
}
130+
}
131+
132+
DropdownMenu(
133+
expanded = dropDownMenuExpanded,
134+
onDismissRequest = {
135+
dropDownMenuExpanded = false
136+
},
137+
modifier = Modifier.width(200.dp)
138+
) {
139+
for (moreOptionsAction in moreOptionsActions) {
140+
DropdownMenuItem(
141+
text = {
142+
Text(
143+
text = moreOptionsAction.title,
144+
color = Color.White,
145+
fontSize = 16.sp,
146+
fontWeight = FontWeight.Normal,
147+
)
148+
},
149+
150+
leadingIcon = {
151+
Icon(
152+
imageVector = moreOptionsAction.icon,
153+
contentDescription = moreOptionsAction.title,
154+
tint = Color.White,
155+
)
156+
},
157+
158+
onClick = {
159+
onActionClicked(moreOptionsAction.id)
160+
dropDownMenuExpanded = false
161+
},
162+
)
163+
}
164+
}
165+
}
166+
}
167+
}
168+
169+
170+
}
171+
172+
}

0 commit comments

Comments
 (0)