Neumorphism简介
新拟态设计风格(Neumorphism)是一种近年来兴起的UI设计趋势,它结合了扁平化设计和拟物化设计的元素,创造出一种具有立体感和质感的视觉效果。
新拟态设计风格的特点是通过使用柔和的阴影和光线效果,让界面元素看起来像是凸起或凹陷的,仿佛是真实的物体。这种风格常常使用浅色或中性色调的背景,搭配柔和的色彩来营造温暖和舒适的感觉。
与传统的拟物化设计不同的是,新拟态设计更加注重细节和柔和的过渡效果。它强调了元素之间的层次感和深度,使用户可以更直观地理解界面中不同元素之间的关系。
ShadeTabElevated简介
ShadeTabElevated是一个采用新拟态设计风格的底部导航控件,通过巧妙运用阴影和动画效果,使用户能够直观地理解界面元素之间的逻辑关系。
ShadeTabElevated是使用Jetpack Compose编写的控件,它的实现逻辑非常酷炫。让我们一起来分享一下代码的实现原理。
ShadeTabElevated的代码使用了Compose的声明式方式来描述界面的外观和行为。它利用阴影效果和动画特性,使导航控件看起来像是凸起的,给用户一种立体感。
通过使用Compose提供的动画和过渡效果支持,我们可以轻松地为界面添加各种交互和视觉效果。这使得ShadeTabElevated的界面更加生动和有吸引力。
同时,ShadeTabElevated的代码还充分利用了数据驱动的思想,通过使用不可变数据模型和可观察状态,界面可以在数据变化时自动更新。这样可以简化界面的管理和维护,并提供更好的性能和可测试性。
实现思路
底部导航控件由背景控件和五个图标组成。在未选中状态下,图标呈灰色,位于背景控件的垂直中央,并且没有阴影效果。而在选中状态下,图标呈橙红色,位于背景控件上方,并且带有阴影效果。
实现这个控件时,主要需要解决以下两个问题:
- 为图标控件添加阴影效果
- 图标状态切换时添加动画效果
实现底部导航控件的代码思路是:通过Compose的修饰符为图标控件添加阴影效果,并利用动画支持实现图标状态切换的动画效果。
代码实现
1.图标控件添加阴影效果
Compose中使用Modifier修饰符来装饰控件样式,我们同样使用这个思路,为Modifier创建添加阴影的扩展函数,那么所有的控件都可以使用这一特性。
- 为Mdifier创建backgroundShadow的扩展函数,参数如下
less
fun Modifier.backgroundShadow(
shadowColorLight: Color = Color(ConstantColor.THEME_LIGHT_COLOR_SHADOW_LIGHT),//浅色阴影颜色
shadowColorDark: Color = Color(ConstantColor.THEME_LIGHT_COLOR_SHADOW_DARK),//深色阴影颜色
blurRadius:Float = 8f,//阴影模糊系数
lightSource: Int = LightSource.DEFAULT,//光源方向
offset:Float = 10f,//阴影偏移量
cornerRadius:Dp = 0.dp,//阴影圆角大小
shape:Int = Shape.Rectangle,//阴影形状
borderWidth :Dp = 20.dp,//Shape.Circle中作为圆环宽度
)
- 阴影画笔设置抗锯齿、防抖、颜色、模糊效果
ini
//浅色阴影画笔
val paintShadowLight = Paint().also { paint: androidx.compose.ui.graphics.Paint ->
paint.asFrameworkPaint() //将自定义的绘制操作转换成底层渲染引擎能够理解的渲染描述对象,从而实现更加高效和灵活的绘制操作。
.also {nativePaint: NativePaint ->
nativePaint.isAntiAlias = true //设置抗锯齿
nativePaint.isDither = true //开启防抖
nativePaint.color = shadowColorLight.toArgb() //设置画笔颜色
if (offset>0)nativePaint.maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) //设置模糊滤镜效果
}
}
//深色阴影画笔
val paintShadowDark = Paint().also { paint: androidx.compose.ui.graphics.Paint ->
paint.asFrameworkPaint() //将自定义的绘制操作转换成底层渲染引擎能够理解的渲染描述对象,从而实现更加高效和灵活的绘制操作。
.also {nativePaint: NativePaint ->
nativePaint.isAntiAlias = true //设置抗锯齿
nativePaint.isDither = true //开启防抖
nativePaint.color = shadowColorDark.toArgb() //设置画笔颜色
if (offset>0)nativePaint.maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) //设置模糊滤镜效果
}
}
- 获取不同阴影在光源方向的偏移量
scss
//浅色阴影在光源方向的偏移量
val backgroundShadowLightOffset:Offset = when(lightSource){
LightSource.LEFT_TOP -> Offset(-offset,-offset)
LightSource.LEFT_BOTTOM -> Offset(-offset,offset)
LightSource.RIGHT_TOP -> Offset(offset, -offset)
LightSource.RIGHT_BOTTOM -> Offset(offset, offset)
else -> {Offset(0f,0f)}
}
//深色阴影在光源方向的偏移量
val backgroundShadowDarkOffset:Offset = when(LightSource.opposite(lightSource)){
LightSource.LEFT_TOP -> Offset(-offset,-offset)
LightSource.LEFT_BOTTOM -> Offset(-offset,offset)
LightSource.RIGHT_TOP -> Offset(offset, -offset)
LightSource.RIGHT_BOTTOM -> Offset(offset, offset)
else -> {Offset(0f,0f)}
}
- 绘制浅/深色阴影
首先,我们保存画布的当前状态,以便在绘制完成后可以恢复。然后,通过translate
函数将画布平移,以便绘制阴影的偏移量。
接下来,根据形状的类型,使用不同的绘制方法来绘制阴影。如果形状是圆形,则使用drawCircle
方法来绘制圆形的阴影。我们传入圆心的偏移量(根据组件的大小计算得出),以及半径(组件宽度减去边框宽度的一半),以及画笔paintShadowLight
来绘制阴影的样式和颜色。
如果形状是矩形,则使用drawRoundRect
方法来绘制圆角矩形的阴影。我们传入矩形的四个角的坐标、圆角的半径(通过将cornerRadius
转换为像素值)以及画笔paintShadowLight
来绘制阴影的样式和颜色。
arduino
//画布平移绘制浅色阴影
it.save()
it.translate(backgroundShadowLightOffset.x,backgroundShadowLightOffset.y)
when(shape){
Shape.Circle ->{
paintShadowLight.style = PaintingStyle.Stroke
paintShadowLight.strokeWidth = borderWidth.toPx()
it.drawCircle(
Offset(this.size.width/2,this.size.height/2),
(this.size.width - borderWidth.toPx() )/2,
paintShadowLight
)
}
Shape.Rectangle ->{
it.drawRoundRect(
0f,
0f,
this.size.width,
this.size.height,
cornerRadius.toPx(),
cornerRadius.toPx(),
paintShadowLight
)
}
}
it.restore()
//画布平移绘制深色阴影
it.save()
it.translate(backgroundShadowDarkOffset.x,backgroundShadowDarkOffset.y)
when(shape){
Shape.Circle ->{
paintShadowDark.style = PaintingStyle.Stroke
paintShadowDark.strokeWidth = borderWidth.toPx()
it.drawCircle(
Offset(this.size.width/2,this.size.height/2),
(this.size.width - borderWidth.toPx() )/2,
paintShadowDark
)
}
Shape.Rectangle ->{
it.drawRoundRect(
0f,
0f,
this.size.width,
this.size.height,
cornerRadius.toPx(),
cornerRadius.toPx(),
paintShadowDark
)
}
}
it.restore()
}
2.自定义可添加阴影效果的图标控件
ShadeImageButton是新拟态风格的图标控件。设计思路如下:
- 组件继承自Compose框架的@Composable注解修饰的函数。
- 添加阴影效果。设置浅色/深色阴影颜色、模糊系数、光源方向、阴影偏移量、圆角大小。
- 添加交互。对手势的点击操作进行监听。
- 控制形状。控制图标背景的圆角大小。
- 显示图标。通过Image控件来显示图标。
ShadeImageButton的代码如下:
less
@Composable
fun ShadeImageButton(
modifier: Modifier = Modifier,//修饰器
shadowColorLight: Color = Color(THEME_LIGHT_COLOR_SHADOW_LIGHT),//浅色阴影颜色
shadowColorDark: Color = Color(THEME_LIGHT_COLOR_SHADOW_DARK),//深色阴影颜色
blurRadius: Float = BlurProvider.getDefaultBlurRadius(App.context()),//阴影模糊系数
lightSource: Int = LightSource.DEFAULT,//光源方向,默认从左上向右下
offset: Float = 20f,//阴影偏移量
cornerRadius: Dp = 0.dp,//圆角大小,控制阴影和图标背景的圆角。
onClick: () -> Unit,//点击监听
backgroundColor: Color = NeumorphismLightBackgroundColor,//背景颜色
size: Dp,//图标大小
painter: Painter,
contentDescription: String? = null,
iconColor: Color? = null,//图标颜色
shape: Int = Shape.Rectangle,//阴影/图标背景形状
iconPadding: Dp = 0.dp //图标内边距
){
var currentOffset by remember { mutableStateOf(offset) }
LaunchedEffect(offset) { // 使用LaunchedEffect监听offset的变化
currentOffset = offset
}
var currentCornerRadius by remember { mutableStateOf(cornerRadius) }
if (shape == Shape.Circle) {
currentCornerRadius = (size + size / 5 * 2) / 2
}
Card(
modifier = modifier
.backgroundShadow(//设置图标阴影
shadowColorLight,
shadowColorDark,
blurRadius,
lightSource,
currentOffset,
currentCornerRadius,
)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
currentOffset = 0f
}
MotionEvent.ACTION_UP -> {
currentOffset = offset
onClick()
}
}
true
},
shape = RoundedCornerShape(currentCornerRadius),//设置圆角大小
elevation = 0.dp
) {
Box(
Modifier
.background(backgroundColor)//设置背景颜色
.padding(size / 5)
) {
Image(
painter = painter,
contentDescription = contentDescription,
modifier = Modifier
.align(Alignment.Center)//对齐方式
.size(size)//大小
.padding(iconPadding),//设置内边距
colorFilter = if (iconColor == null) null else ColorFilter.tint(iconColor),//设置图标颜色
)
}
}
}
ShadeTabElevated底部导航实现
ShadeTabElevated是一个自定义的组件,用于显示带阴影效果的选项卡。该组件实现的设计思路如下:
- 使用@Composable注解将函数声明为可组合函数。
- 定义ShadeTabElevated函数,接受一些参数用于自定义组件的外观和行为。
- 使用BoxWithConstraints组件包裹整个组件,以便获取容器的宽度和高度。
- 使用remember和mutableStateOf创建一个可变的currentIndex变量,用于跟踪当前选中的选项卡的索引。
- 定义一些动画所需的变量和属性,包括偏移量、大小和颜色。使用animateDpAsState和animateColorAsState函数创建动画效果,并在点击选项卡时更新currentIndex的值。
- 使用Row和Box组件创建一个水平布局,用于显示多个选项卡。
- 在每个选项卡的Box中,使用ShadeImageButton组件来显示带阴影效果的圆形图标按钮。根据currentIndex的值确定当前选中的选项卡,并根据动画属性来设置按钮的大小、偏移量、背景颜色和图标颜色。
- 在点击选项卡时,更新currentIndex的值,并调用回调函数onSelectedChange来通知父组件选项卡的变化。
示例代码:
ini
//上下移动动画
val animateOffset_0 :Dp by animateDpAsState(
targetValue = if (currentIndex.value == 0) offsetUp else 0.dp,
animationSpec = tween( durationMillis = 300)
)
Row(modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth()) {
Box(modifier = Modifier.weight(1f)){
ShadeImageButton(
modifier = Modifier
.align(Alignment.Center)
.offset(0.dp, -animateOffset_0),
onClick = {
currentIndex.value = 0
onSelectedChange.invoke(0)
},
size = if (currentIndex.value == 0) imgSize * 1.2f else imgSize,
painter = painterResource(id = R.drawable.time),
shape = Shape.Circle,
iconColor = animateColor_0,
offset = if (currentIndex.value == 0) offsetShadowDefault else 0f,
backgroundColor = if (currentIndex.value == 0) backgroundColor else Color.White,
blurRadius = blurRadius
)
}
....
}