Compose Neumorphism新拟态风格的指示器控件--ShadeValueIndicator

Neumorphism简介

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

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

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

ShadeValueIndicator简介

ShadeValueIndicator是一个Neumorphism风格的数值指示器组件,可以显示一个数值并带有阴影效果。

它能够以一种独特的方式展示数值,并具有引人注目的阴影效果。该指示器控件的呈现效果包括以下几个部分:

首先,它具备一个圆形背景,其背景上应用了Neumorphism风格的阴影,使整个指示器看起来更加立体和真实。

其次,指示器还包含一个圆形前景背景,同样采用了Neumorphism风格的前景阴影效果。这样一来,指示器的外观更加独特并且引人注目。

中央部分是用来显示具体数值的文本。这个文本可以根据需要显示任意数值,并且会随着数值的变化而更新。

最后,指示器中还有一个位于圆环上的指针。这个指针会根据数值的变化呈现动画效果,从而更加生动地展示数值的变化情况。

通过以上设计,ShadeValueIndicator呈现出了一种独特的美感和视觉效果,同时也提供了直观的数值展示和动画表现,为用户提供了一种全新的交互体验。

代码实现

1.绘制外圆的背景阴影

外圆的背景阴影光源方向是从左上到右下,这种布局方式可以为我们的界面增添一种动态和立体感。为了营造出光源照射的效果,我们需要在左上位置绘制浅色阴影,以模拟光线的明亮和聚焦效果。而在右下方向,则需要绘制深色的阴影,以模拟光源的投影和阴影效果。

为了实现这一效果,我们可以充分发挥kotlin的特性和Compose的声明式UI编写方式。通过为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.绘制内圆的前景阴影
  • 为Mdifier创建backgroundShadow的扩展函数,参数如下:
kotlin 复制代码
fun Modifier.foregroundShadow(
    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 = 22f,//阴影的偏移量
    cornerRadius:Dp = 0.dp,//阴影圆角
) 
  • 浅/深阴影画笔初始化,设置抗锯齿、防抖、颜色、模糊效果
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() //设置画笔颜色
            nativePaint.style = android.graphics.Paint.Style.STROKE
            nativePaint.strokeWidth = offset
            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() //设置画笔颜色
            nativePaint.style = android.graphics.Paint.Style.STROKE
            nativePaint.strokeWidth = offset //设置描边宽度
            if (offset>0)nativePaint.maskFilter = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) //设置模糊滤镜效果
        }
}
  • 分别设置不同阴影在光源方向的偏移量
scss 复制代码
//浅色阴影在光源方向的偏移量
val backgroundShadowLightOffset: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)}
}

//深色阴影在光源方向的偏移量
val backgroundShadowDarkOffset: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)}
}
  • 绘制前景浅/深色阴影

首先,通过调用 save() 方法保存当前画布的状态。

然后,创建一个 Path 对象 pathShadowDark,用于绘制深色阴影的路径。使用 moveTo() 方法将路径的起点移动到原点,然后使用 addRoundRect() 方法将圆角矩形的路径添加到 pathShadowDark 中。

接下来,通过调用 clipPath() 方法,将画布限制在 pathShadowDark 区域内。

然后,通过调用 translate() 方法,将画布移动到 backgroundShadowDarkOffset 的偏移量。

最后,调用 drawRoundRect() 方法,根据给定的参数绘制一个带有深色阴影效果的圆角矩形。

接着,通过调用 restore() 方法恢复之前保存的画布状态。

然后,通过调用 save() 方法再次保存当前画布的状态。

接下来,创建一个 Path 对象 pathShadowLight,用于绘制浅色阴影的路径,操作与绘制深色阴影的路径类似。

通过调用 clipPath() 方法,将画布限制在 pathShadowLight 区域内。

通过调用 translate() 方法,将画布移动到 backgroundShadowLightOffset 的偏移量。

最后,调用 drawRoundRect() 方法,根据给定的参数绘制一个带有浅色阴影效果的圆角矩形。

最后,通过调用 restore() 方法恢复之前保存的画布状态。

arduino 复制代码
//绘制深色阴影
it.save()
val pathShadowDark = Path().also { path ->
    path.moveTo(0f, 0f)
    path.addRoundRect(RoundRect(0f, 0f, this.size.width , this.size.height, cornerRadius.toPx(), cornerRadius.toPx()))
}
it.clipPath(pathShadowDark)
it.translate(backgroundShadowDarkOffset.x,backgroundShadowDarkOffset.y)
it.drawRoundRect(
    -offset,
    -offset,
    this.size.width + offset,
    this.size.height + offset,
    cornerRadius.toPx(),
    cornerRadius.toPx(),
    paintShadowDark
)
it.restore()
//绘制浅色阴影
it.save()
val pathShadowLight = Path().also { path ->
    path.moveTo(0f, 0f)
    path.addRoundRect(RoundRect(0f, 0f, this.size.width, this.size.height, cornerRadius.toPx(), cornerRadius.toPx()))
}
it.clipPath(pathShadowLight)
it.translate(backgroundShadowLightOffset.x,backgroundShadowLightOffset.y)
it.drawRoundRect(
    -offset,
    -offset,
    this.size.width + offset,
    this.size.height + offset,
    cornerRadius.toPx(),
    cornerRadius.toPx(),
    paintShadowLight
)
it.restore()
3.数值显示以及动效

我们可以使用使用Jetpack Compose的AnimatedContent组件来实现一个数字的动画效果。

(1) targetState = currentNumtargetState属性指定了动画的目标状态,即currentNum的值。

(2) transitionSpec属性定义了不同状态之间的过渡效果。根据lastNum.valuecurrentNum的值的比较情况,选择不同的过渡效果。

(3) 如果lastNum.value > currentNum,则使用向上滑动和淡出的效果:

  • slideIntoContainer(AnimatedContentScope.SlideDirection.Up)表示在容器内部向上滑动进入。
  • fadeOut()表示淡出效果。
  • slideOutOfContainer(AnimatedContentScope.SlideDirection.Up)表示在容器内部向上滑动退出。

(4) 如果lastNum.value < currentNum,则使用向下滑动和淡出的效果:

  • slideIntoContainer(AnimatedContentScope.SlideDirection.Down)表示在容器内部向下滑动进入。
  • fadeOut()表示淡出效果。
  • slideOutOfContainer(AnimatedContentScope.SlideDirection.Down)表示在容器内部向下滑动退出。

(5) 如果lastNum.valuecurrentNum相等,则使用淡入、缩放和淡出的效果:

  • fadeIn(animationSpec = tween(220, delayMillis = 90))表示淡入效果,动画时间为220ms,延迟90ms。
  • scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90))表示缩放效果,初始缩放比例为0.92,动画时间为220ms,延迟90ms。
  • fadeOut(animationSpec = tween(90))表示淡出效果,动画时间为90ms。

(6) lastNum.value = currentNum:在AnimatedContent的内容块中,将lastNum.value的值设置为currentNum,以便在下一次过渡时使用。

(7) Text(text = "$it°", style = textStyle):显示当前数字的文本组件。$it表示动画的当前状态。

通过使用AnimatedContent组件和不同的过渡效果,可以实现数字的平滑动画效果,让界面更加生动和吸引人。

scss 复制代码
AnimatedContent(
    targetState = currentNum,
    transitionSpec = if (lastNum.value > currentNum) {
        {
            slideIntoContainer(AnimatedContentScope.SlideDirection.Up) with
                    fadeOut() + slideOutOfContainer(AnimatedContentScope.SlideDirection.Up)
        }
    }else if (lastNum.value < currentNum){
        {
            slideIntoContainer(AnimatedContentScope.SlideDirection.Down) with
                    fadeOut() + slideOutOfContainer(AnimatedContentScope.SlideDirection.Down)
        }
    }else {
        {
            fadeIn(animationSpec = tween(220, delayMillis = 90)) +
                    scaleIn(initialScale = 0.92f, animationSpec = tween(220, delayMillis = 90)) with
                    fadeOut(animationSpec = tween(90))
        }
    }
) {
    lastNum.value = currentNum
    Text(
        text = "$it°",
        style = textStyle
    )
}
4.绘制指针

自定义的BoxWithConstraints组件,用于在一个具有限制条件的Box中绘制一个Canvas。

(1) BoxWithConstraints(modifier = modifier):这是一个自定义组件的构造函数,接受一个modifier参数用于设置外部传递进来的修饰符。

(2) Canvas(modifier = Modifier.size(maxWidth,maxHeight)):这是一个绘制图形的画布,通过modifier参数设置画布的尺寸为maxWidth和maxHeight。

(3) rotate(rotationAngle.value, pivot = Offset(this.size.width / 2, this.size.height / 2)):这是一个对画布进行旋转的操作,rotationAngle是一个可变的角度值,通过rotationAngle.value获取当前的角度值,pivot参数指定旋转的中心点为画布的中心点。

(4) drawLine(...):这是一个在画布上绘制直线的操作,color参数设置直线的颜色,start和end参数分别指定直线的起始点和结束点,strokeWidth参数设置直线的宽度,cap参数设置直线的端点样式为圆形。

通过以上代码,可以在一个具有限制条件的Box中绘制一个Canvas,并在Canvas上绘制一条旋转的直线。

ini 复制代码
...
    val rotationAngle =  animateFloatAsState(
        targetValue = (currentNum.toFloat() - min) / (max - min) * 360.0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioNoBouncy ,
            stiffness =  Spring.StiffnessLow
        )
    )
...

BoxWithConstraints(modifier = modifier) {
    Canvas(modifier = Modifier.size(maxWidth,maxHeight)){

        rotate(rotationAngle.value, pivot = Offset(this.size.width / 2, this.size.height / 2)) {
            drawLine(
                color = shadowColorDark,
                start = Offset(this.size.width / 2, padding),
                end = Offset(this.size.width / 2, padding + lineLength),
                strokeWidth = 20f,
                cap = StrokeCap.Round
            )
        }

    }
}

ShadeValueIndicator源码

ShadeValueIndicator是Neumorphism新拟态风格组件的一种,想了解更多的组件,移步Github

相关推荐
K1t04 分钟前
Android-UI设计
android·ui
吃汉堡吃到饱1 小时前
【Android】浅析MVC与MVP
android·mvc
深海呐8 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang8 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼8 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss9 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-194311 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男13 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽14 小时前
Android 源码集成可卸载 APP
android
码农明明14 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读