likes
comments
collection
share

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

作者站长头像
站长
· 阅读数 3

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

Neumorphism简介

新拟态设计风格(Neumorphism)是一种近年来兴起的UI设计趋势,它结合了扁平化设计和拟物化设计的元素,创造出一种具有立体感和质感的视觉效果。

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

新拟态设计风格的特点是通过使用柔和的阴影和光线效果,让界面元素看起来像是凸起或凹陷的,仿佛是真实的物体。这种风格常常使用浅色或中性色调的背景,搭配柔和的色彩来营造温暖和舒适的感觉。

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

与传统的拟物化设计不同的是,新拟态设计更加注重细节和柔和的过渡效果。它强调了元素之间的层次感和深度,使用户可以更直观地理解界面中不同元素之间的关系。

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

ShadeTabElevated简介

ShadeTabElevated是一个采用新拟态设计风格的底部导航控件,通过巧妙运用阴影和动画效果,使用户能够直观地理解界面元素之间的逻辑关系。

ShadeTabElevated是使用Jetpack Compose编写的控件,它的实现逻辑非常酷炫。让我们一起来分享一下代码的实现原理。

ShadeTabElevated的代码使用了Compose的声明式方式来描述界面的外观和行为。它利用阴影效果和动画特性,使导航控件看起来像是凸起的,给用户一种立体感。

通过使用Compose提供的动画和过渡效果支持,我们可以轻松地为界面添加各种交互和视觉效果。这使得ShadeTabElevated的界面更加生动和有吸引力。

同时,ShadeTabElevated的代码还充分利用了数据驱动的思想,通过使用不可变数据模型和可观察状态,界面可以在数据变化时自动更新。这样可以简化界面的管理和维护,并提供更好的性能和可测试性。

Compose Neumorphism新拟态风格底部导航控件--ShadeTabElevated

实现思路

底部导航控件由背景控件和五个图标组成。在未选中状态下,图标呈灰色,位于背景控件的垂直中央,并且没有阴影效果。而在选中状态下,图标呈橙红色,位于背景控件上方,并且带有阴影效果。

实现这个控件时,主要需要解决以下两个问题:

  1. 为图标控件添加阴影效果
  2. 图标状态切换时添加动画效果

实现底部导航控件的代码思路是:通过Compose的修饰符为图标控件添加阴影效果,并利用动画支持实现图标状态切换的动画效果。

代码实现

1.图标控件添加阴影效果

Compose中使用Modifier修饰符来装饰控件样式,我们同样使用这个思路,为Modifier创建添加阴影的扩展函数,那么所有的控件都可以使用这一特性。

  • 为Mdifier创建backgroundShadow的扩展函数,参数如下
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中作为圆环宽度
) 

  • 阴影画笔设置抗锯齿、防抖、颜色、模糊效果
//浅色阴影画笔
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) //设置模糊滤镜效果
        }
}

  • 获取不同阴影在光源方向的偏移量
//浅色阴影在光源方向的偏移量
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来绘制阴影的样式和颜色。

 //画布平移绘制浅色阴影
    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的代码如下:

@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来通知父组件选项卡的变化。

示例代码:

//上下移动动画
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
                )
            }
            ....
}

ShadeTabElevated源码

ShadeTabElevated是Neumorphism新拟态风格组件的一种,想了解更多的组件,移步[Github](DingMouRen/ShadeCraft: Neumorphism-style Compose component)