扁平还是立体
设计界有过一场"扁平化 vs 拟物化"的争论。扁平化追求简洁,去掉阴影、渐变、纹理。拟物化追求真实,模拟现实世界的光影效果。
现在的主流是"半扁平"------整体简洁,但在需要的地方加一点阴影来表示层次。卡片浮在背景上方,按钮浮在卡片上方,阴影告诉用户"这个元素在上面"。
我们的任务卡片就有阴影。不是很重的阴影,只是淡淡的一层,让卡片看起来有一点点"浮起来"的感觉。这种微妙的立体感能让界面更有质感。
任务卡片的阴影代码
tsx
<Animated.View style={[styles.taskCard, {
backgroundColor: theme.card,
borderColor: theme.border,
opacity: itemAnim,
transform: [{translateX: itemAnim.interpolate({inputRange: [0, 1], outputRange: [-50, 0]})}],
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: darkMode ? 0.3 : 0.1,
shadowRadius: 4,
elevation: 3
}]}>
阴影相关的属性有五个:shadowColor、shadowOffset、shadowOpacity、shadowRadius、elevation。前四个是 iOS 的,最后一个是 Android 的。
iOS 的阴影属性
iOS 用四个属性控制阴影:
shadowColor
tsx
shadowColor: '#000'
阴影的颜色。大多数情况下用黑色 #000,因为现实世界的阴影就是黑色的。
也可以用彩色阴影。比如我们的 FAB 用紫色阴影 shadowColor: '#6c5ce7',让阴影带一点品牌色,更有设计感。
shadowOffset
tsx
shadowOffset: {width: 0, height: 2}
阴影的偏移量。width 是水平偏移,height 是垂直偏移。
{width: 0, height: 2} 表示阴影向下偏移 2 像素,水平不偏移。这模拟了光从正上方照射的效果,阴影在元素下方。
如果设置 {width: 2, height: 2},阴影会向右下方偏移,模拟光从左上方照射。
大多数情况下,width 设为 0,只调整 height。因为我们习惯光从上方来,阴影在下方。
shadowOpacity
tsx
shadowOpacity: darkMode ? 0.3 : 0.1
阴影的透明度,0 到 1 之间。0 是完全透明(看不见),1 是完全不透明(纯黑)。
我们根据主题调整透明度。深色模式下用 0.3,阴影更明显。浅色模式下用 0.1,阴影更淡。
为什么深色模式阴影要更明显?因为深色背景本身就暗,淡阴影会看不见。浅色背景上,淡阴影就够了,太重会显得脏。
shadowRadius
tsx
shadowRadius: 4
阴影的模糊半径。数值越大,阴影越模糊、越扩散。数值越小,阴影越清晰、越集中。
shadowRadius: 4 是一个适中的值,阴影有一定的模糊但不会太散。
如果 shadowRadius: 0,阴影会是一个清晰的边缘,像元素的复制品。这种效果很少用。
Android 的阴影属性
Android 不支持 iOS 的 shadow* 属性,用 elevation 代替:
tsx
elevation: 3
elevation 是"海拔高度"的意思。数值越大,元素越"高",阴影越明显。
Material Design 定义了不同层级的 elevation:
- 0:在背景上,没有阴影
- 1-2:轻微浮起,如卡片
- 3-4:中等浮起,如按钮
- 6-8:明显浮起,如 FAB、弹窗
- 12-24:很高,如对话框、抽屉
我们的任务卡片用 elevation: 3,是轻微到中等的浮起效果。
elevation 的局限
elevation 只能控制阴影的"强度",不能控制颜色、偏移、模糊等细节。Android 的阴影是系统自动计算的,基于 Material Design 的规范。
这意味着 iOS 和 Android 的阴影效果可能不完全一致。iOS 可以精细控制,Android 只能大致控制。对于大多数应用来说,这种差异可以接受。
跨平台阴影的写法
为了在两个平台都有阴影,我们同时写 iOS 和 Android 的属性:
tsx
{
// iOS
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 4,
// Android
elevation: 3,
}
iOS 会忽略 elevation,Android 会忽略 shadow*。两个平台各取所需。
Platform 判断
如果想针对不同平台设置不同的值,可以用 Platform:
tsx
import {Platform} from 'react-native';
const shadow = Platform.select({
ios: {
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 3,
},
});
不过对于阴影来说,同时写两套属性更简单,不需要 Platform.select。
阴影和圆角
任务卡片有圆角:
tsx
taskCard: {
...
borderRadius: 12,
overflow: 'hidden',
}
在 iOS 上,阴影会自动适应圆角。圆角的卡片,阴影也是圆角的。
但如果设置了 overflow: 'hidden',阴影可能会被裁掉。因为阴影在元素外面,overflow: 'hidden' 会隐藏元素外面的内容。
解决方案是把阴影和内容分开:
tsx
// 外层负责阴影
<View style={{shadowColor: '#000', shadowOffset: {width: 0, height: 2}, shadowOpacity: 0.1, shadowRadius: 4, elevation: 3}}>
// 内层负责圆角和裁剪
<View style={{borderRadius: 12, overflow: 'hidden'}}>
{/* 内容 */}
</View>
</View>
我们的任务卡片没有这个问题,因为内容没有超出卡片边界,不需要 overflow: 'hidden'。
阴影的性能
阴影会影响渲染性能吗?
在 iOS 上,shadow* 属性会触发离屏渲染,有一定的性能开销。如果列表里有很多带阴影的卡片,滚动时可能会卡顿。
优化方法:
shouldRasterizeIOS
tsx
<View style={shadowStyle} shouldRasterizeIOS={true}>
shouldRasterizeIOS 会把元素光栅化成位图缓存,避免每帧都重新计算阴影。但如果元素内容经常变化,光栅化反而会更慢。
减少阴影元素
不是每个元素都需要阴影。只给重要的元素加阴影,减少阴影元素的数量。
简化阴影
减小 shadowRadius,阴影计算会更快。或者干脆用边框代替阴影,边框没有性能问题。
我们的任务列表通常只有几十个卡片,阴影的性能影响可以忽略。如果列表有几百上千个卡片,可能需要考虑优化。
阴影的层次感
阴影不只是装饰,它传达了层次信息。
背景层
背景没有阴影,是最底层。
卡片层
任务卡片有轻微阴影 elevation: 3,浮在背景上方。
按钮层
FAB 有明显阴影 elevation: 8,浮在卡片上方。
弹窗层
Modal 弹窗有最强的阴影(如果有的话),浮在所有内容上方。
这种层次关系让用户直觉地理解界面结构。阴影越重的元素越"近",越容易被注意到。
深色模式下的阴影
深色模式下,阴影需要调整:
tsx
shadowOpacity: darkMode ? 0.3 : 0.1
深色背景上,淡阴影几乎看不见。需要增加透明度让阴影更明显。
但也不能太重。深色模式的目的是减少亮度,太重的阴影会让界面显得"脏"。0.3 是一个平衡点。
有些设计师认为深色模式不需要阴影,用边框或颜色差异来表示层次。这也是一种选择。我们保留了阴影,但调整了透明度。
彩色阴影
FAB 用了彩色阴影:
tsx
fab: {
...
shadowColor: '#6c5ce7',
shadowOffset: {width: 0, height: 4},
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}
shadowColor: '#6c5ce7' 是紫色,和 FAB 的背景色一致。
彩色阴影让 FAB 看起来像是在"发光",更有存在感。这是一种设计技巧,用于强调重要的元素。
注意彩色阴影只在 iOS 上有效。Android 的 elevation 阴影永远是灰色的。
内阴影
CSS 有 box-shadow: inset 可以做内阴影,但 React Native 不支持内阴影。
如果需要内阴影效果,可以用渐变或者图片模拟:
tsx
// 用 LinearGradient 模拟顶部内阴影
<LinearGradient
colors={['rgba(0,0,0,0.1)', 'transparent']}
style={{position: 'absolute', top: 0, left: 0, right: 0, height: 10}}
/>
这种方式比较 hack,效果也有限。大多数情况下,不用内阴影也能做出好看的界面。
阴影的动画
阴影可以动画吗?
iOS 的 shadow* 属性不能用 useNativeDriver: true,动画性能较差。
Android 的 elevation 可以动画,但效果可能不明显。
如果想做"按下时阴影变小"的效果:
tsx
const elevationAnim = useRef(new Animated.Value(3)).current;
const onPressIn = () => {
Animated.timing(elevationAnim, {toValue: 1, duration: 100, useNativeDriver: false}).start();
};
const onPressOut = () => {
Animated.timing(elevationAnim, {toValue: 3, duration: 100, useNativeDriver: false}).start();
};
<Animated.View style={{elevation: elevationAnim, ...}}>
按下时 elevation 从 3 变成 1,阴影变小,看起来像是按钮被按下去了。松开时恢复。
这种动画在 Android 上效果不错,iOS 上需要同时动画 shadowOpacity 或 shadowRadius。
不用阴影的替代方案
如果不想用阴影,有其他方式表示层次:
边框
tsx
borderWidth: 1,
borderColor: theme.border,
边框能清晰地划分元素边界,但没有立体感。
背景色差异
卡片背景色比页面背景色浅一点(浅色模式)或深一点(深色模式),形成对比。
我们的设计同时用了边框和阴影。边框在两个平台上效果一致,阴影增加立体感。
小结
阴影让卡片有"浮起来"的感觉,增加界面的层次感。iOS 用 shadowColor、shadowOffset、shadowOpacity、shadowRadius 四个属性,Android 用 elevation。深色模式下阴影透明度要调高一些。彩色阴影可以强调重要元素,但只在 iOS 上有效。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
