Skip to content

Instantly share code, notes, and snippets.

@TheMelody
Last active November 20, 2021 06:43
Show Gist options
  • Save TheMelody/5681affaf39600b1f974f4db214a7b0a to your computer and use it in GitHub Desktop.
Save TheMelody/5681affaf39600b1f974f4db214a7b0a to your computer and use it in GitHub Desktop.
Jetpack Compose - FloatingActionButton 展开/折叠 的多级悬浮菜单
/**
* 折叠/展开多个FloatingActionButton菜单
*/
@Composable
fun MultiFloatingActionButton(
modifier: Modifier = Modifier,
srcIcon: ImageVector,
srcIconColor: Color = Color.White,
fabBackgroundColor: Color = Color.Unspecified,
showLabels: Boolean = true,
items: List<MultiFabItem>,
onFabItemClicked: (item: MultiFabItem) -> Unit
) {
//当前菜单默认状态处于:Collapsed
val currentState = remember { mutableStateOf(MultiFabState.Collapsed) }
//创建过渡对象,用于管理多个动画值,并且根据状态变化运行这些值
val transition = updateTransition(targetState = currentState, label = "")
//用于+号按钮的旋转动画
val rotateAnim: Float by transition.animateFloat(
transitionSpec = {
if (targetState.value == MultiFabState.Expanded) {
spring(stiffness = Spring.StiffnessLow)
} else {
spring(stiffness = Spring.StiffnessMedium)
}
}, label = ""
) { state ->
//根据state来设置最终的角度
if (state.value == MultiFabState.Collapsed) 0F else -45F
}
//透明度动画
val alphaAnim: Float by transition.animateFloat(transitionSpec = {
tween(durationMillis = 200)
}, label = "") { state ->
if (state.value == MultiFabState.Expanded) 1F else 0F
}
//记录每个Item的收缩动画的Transition
val shrinkListAnim:MutableList<Float> = mutableListOf()
items.forEachIndexed { index, _ ->
//循环生成Transition
val shrinkAnim by transition.animateFloat(targetValueByState = { state ->
when (state.value) {
MultiFabState.Collapsed -> 5F
//根据位置,递增每个item的位置高度
MultiFabState.Expanded -> (index + 1) * 60F + if(index == 0) 5F else 0F
}
}, label = "", transitionSpec = {
if (targetState.value == MultiFabState.Expanded) {
//dampingRatio属性删除等于默认1F,没有回弹效果
spring(stiffness = Spring.StiffnessLow,dampingRatio = 0.58F)
} else {
spring(stiffness = Spring.StiffnessMedium)
}
})
//添加到收缩列表中
shrinkListAnim.add(index,shrinkAnim)
}
Box(modifier = modifier, contentAlignment = Alignment.BottomEnd) {
//创建多个Item,Fab按钮
items.forEachIndexed{index, item ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(
//从收缩列表中获取
bottom = shrinkListAnim[index].dp,
top = 5.dp,
end = 30.dp
)
.alpha(animateFloatAsState(alphaAnim).value)
) {
if (showLabels) {
Text(
item.label,
color = item.labelTextColor,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier
.clip(Shapes.medium)
.alpha(animateFloatAsState(alphaAnim).value)
.background(color = item.labelBackgroundColor)
.padding(start = 6.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
)
Spacer(modifier = Modifier.width(16.dp))
}
FloatingActionButton(
backgroundColor = if (item.fabBackgroundColor == Color.Unspecified) MaterialTheme.colors.primary else item.fabBackgroundColor,
modifier = Modifier.size(46.dp),
onClick = {
//更新状态 => 折叠菜单
currentState.value = MultiFabState.Collapsed
onFabItemClicked(item)
},
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 2.dp,
pressedElevation = 4.dp
)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = item.icon,
tint = item.srcIconColor,
contentDescription = item.label
)
}
}
}
//"+"号,切换按钮
FloatingActionButton(
modifier = Modifier.padding(0.dp, end = 25.dp),
backgroundColor = if(fabBackgroundColor == Color.Unspecified) MaterialTheme.colors.primary else fabBackgroundColor,
onClick = {
//更新状态执行:收缩动画
currentState.value =
if (currentState.value == MultiFabState.Collapsed) MultiFabState.Expanded else MultiFabState.Collapsed
}) {
Icon(
imageVector = srcIcon,
modifier = Modifier.rotate(rotateAnim),
tint = srcIconColor,
contentDescription = null
)
}
}
}
/**
* FloatingActionButton填充的数据
*/
class MultiFabItem(
val icon: ImageVector,
val label: String,
val srcIconColor: Color = Color.White,
val labelTextColor: Color = Color.White,
val labelBackgroundColor: Color = Color.Black.copy(alpha = 0.6F),
val fabBackgroundColor: Color = Color.Unspecified,
)
/**
* 定义FloatingActionButton状态
*/
enum class MultiFabState {
Collapsed,
Expanded
}
MIT License
Copyright (c) 2021 fuqiang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
//屏幕右下角显示Fab按钮
Box(modifier = Modifier.padding(bottom = 25.dp)
.fillMaxSize().wrapContentSize(align = Alignment.BottomEnd)){
TestMultiFab()
}
/**
* 测试代码
**/
@Composable
fun TestMultiFab(){
val expandFbItemList: MutableList<MultiFabItem> = mutableListOf(
MultiFabItem(
srcIconColor = Color.White,
fabBackgroundColor = Color.Black,
labelBackgroundColor = Color.Red.copy(alpha = 0.45F),
labelTextColor = Color.Blue.copy(alpha = 0.45F),
//自己设置一个图片或者svg
icon = ImageVector.vectorResource(id = R.drawable.ic_edit_file),
label = "创建文件"
),
MultiFabItem(
srcIconColor = Color.Yellow,
fabBackgroundColor = Color.DarkGray,
//自己设置一个图片或者svg
icon = ImageVector.vectorResource(id = R.drawable.ic_import_file),
label = "导入文件"
)
)
val context = LocalContext.current
MultiFloatingActionButton(srcIcon = Icons.Filled.Add, showLabels = true,items = expandFbItemList) {
//弹出来的item被点击了
Toast.makeText(context.applicationContext,"点击了:${it.label}",Toast.LENGTH_SHORT).show()
}
}
@MrMaxxan
Copy link

MrMaxxan commented Oct 1, 2021

Thanks for the example. There will be a problem if you add this to a scaffold and have labels = true and isFloatingActionButtonDocked = true. Maybe you want to update with a bug fix for that. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment