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 = currentNum
:targetState
属性指定了动画的目标状态,即currentNum
的值。
(2) transitionSpec
属性定义了不同状态之间的过渡效果。根据lastNum.value
和currentNum
的值的比较情况,选择不同的过渡效果。
(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.value
和currentNum
相等,则使用淡入、缩放和淡出的效果:
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
)
}
}
}