319 lines
8.0 KiB
TypeScript
319 lines
8.0 KiB
TypeScript
import * as Haptics from 'expo-haptics';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import React from 'react';
|
|
import {
|
|
StyleSheet,
|
|
Text,
|
|
View,
|
|
} from 'react-native';
|
|
import {
|
|
PanGestureHandler,
|
|
PanGestureHandlerGestureEvent,
|
|
} from 'react-native-gesture-handler';
|
|
import Animated, {
|
|
interpolate,
|
|
interpolateColor,
|
|
runOnJS,
|
|
useAnimatedGestureHandler,
|
|
useAnimatedStyle,
|
|
useSharedValue,
|
|
withSpring,
|
|
} from 'react-native-reanimated';
|
|
|
|
interface MoodIntensitySliderProps {
|
|
value: number;
|
|
onValueChange: (value: number) => void;
|
|
min?: number;
|
|
max?: number;
|
|
step?: number;
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
export default function MoodIntensitySlider({
|
|
value,
|
|
onValueChange,
|
|
min = 1,
|
|
max = 10,
|
|
step = 1,
|
|
width = 320,
|
|
height = 16, // 更粗的进度条
|
|
}: MoodIntensitySliderProps) {
|
|
const thumbSize = 32; // 合适的触摸区域
|
|
const translateX = useSharedValue(0);
|
|
const isDragging = useSharedValue(0);
|
|
const sliderWidth = width - thumbSize; // thumb中心移动的有效范围
|
|
|
|
// 计算初始位置
|
|
React.useEffect(() => {
|
|
const initialPosition = ((value - min) / (max - min)) * sliderWidth;
|
|
translateX.value = withSpring(initialPosition);
|
|
}, [value, min, max, sliderWidth, translateX]);
|
|
|
|
const triggerHaptics = () => {
|
|
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
|
};
|
|
|
|
const gestureHandler = useAnimatedGestureHandler<
|
|
PanGestureHandlerGestureEvent,
|
|
{ startX: number; lastValue: number }
|
|
>({
|
|
onStart: (_, context) => {
|
|
context.startX = translateX.value;
|
|
context.lastValue = value;
|
|
isDragging.value = withSpring(1);
|
|
runOnJS(triggerHaptics)();
|
|
},
|
|
onActive: (event, context) => {
|
|
const newX = context.startX + event.translationX;
|
|
const clampedX = Math.max(0, Math.min(sliderWidth, newX));
|
|
translateX.value = clampedX;
|
|
|
|
// 计算当前值
|
|
const currentValue = Math.round((clampedX / sliderWidth) * (max - min) + min);
|
|
|
|
// 当值改变时触发震动和回调
|
|
if (currentValue !== context.lastValue) {
|
|
context.lastValue = currentValue;
|
|
runOnJS(triggerHaptics)();
|
|
runOnJS(onValueChange)(currentValue);
|
|
}
|
|
},
|
|
onEnd: () => {
|
|
// 计算最终值并吸附到最近的步长
|
|
const currentValue = Math.round((translateX.value / sliderWidth) * (max - min) + min);
|
|
const snapPosition = ((currentValue - min) / (max - min)) * sliderWidth;
|
|
|
|
translateX.value = withSpring(snapPosition);
|
|
isDragging.value = withSpring(0);
|
|
runOnJS(triggerHaptics)();
|
|
runOnJS(onValueChange)(currentValue);
|
|
},
|
|
});
|
|
|
|
const thumbStyle = useAnimatedStyle(() => {
|
|
const positionScale = interpolate(
|
|
translateX.value,
|
|
[0, sliderWidth],
|
|
[1, 1.1]
|
|
);
|
|
|
|
const dragScale = interpolate(
|
|
isDragging.value,
|
|
[0, 1],
|
|
[1, 1.2]
|
|
);
|
|
|
|
const finalScale = positionScale * dragScale;
|
|
|
|
// 让thumb在滑动条中正确居中
|
|
const thumbPosition = translateX.value + thumbSize / 2;
|
|
|
|
return {
|
|
transform: [
|
|
{ translateX: thumbPosition },
|
|
{ scale: withSpring(finalScale) }
|
|
],
|
|
};
|
|
});
|
|
|
|
const thumbInnerStyle = useAnimatedStyle(() => {
|
|
const borderColor = interpolateColor(
|
|
isDragging.value,
|
|
[0, 1],
|
|
['#7a5af8', '#ff6b6b']
|
|
);
|
|
|
|
return {
|
|
borderColor: borderColor,
|
|
};
|
|
});
|
|
|
|
const progressStyle = useAnimatedStyle(() => {
|
|
const progressWidth = translateX.value + thumbSize / 2;
|
|
return {
|
|
width: progressWidth,
|
|
};
|
|
});
|
|
|
|
// 动态颜色配置 - 根据进度变化颜色
|
|
const getProgressColors = (progress: number) => {
|
|
if (progress <= 0.25) {
|
|
return ['#22c55e', '#84cc16'] as const; // 绿色到浅绿色
|
|
} else if (progress <= 0.5) {
|
|
return ['#84cc16', '#eab308'] as const; // 浅绿色到黄色
|
|
} else if (progress <= 0.75) {
|
|
return ['#eab308', '#f97316'] as const; // 黄色到橙色
|
|
} else {
|
|
return ['#f97316', '#ef4444'] as const; // 橙色到红色
|
|
}
|
|
};
|
|
|
|
const progressColorsStyle = useAnimatedStyle(() => {
|
|
const progress = translateX.value / sliderWidth;
|
|
return {
|
|
backgroundColor: interpolateColor(
|
|
progress,
|
|
[0, 0.25, 0.5, 0.75, 1],
|
|
['#22c55e', '#84cc16', '#eab308', '#f97316', '#ef4444']
|
|
),
|
|
};
|
|
});
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
<View style={[styles.sliderContainer, { width: width }]}>
|
|
{/* 背景轨道 - 更粗的灰色轨道 */}
|
|
<View style={[styles.track, { height }]}>
|
|
<LinearGradient
|
|
colors={['#f3f4f6', '#e5e7eb']}
|
|
style={[styles.trackGradient, { height }]}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 0 }}
|
|
/>
|
|
</View>
|
|
|
|
{/* 进度条 - 动态颜色 */}
|
|
<Animated.View style={[styles.progress, { height }, progressStyle, progressColorsStyle]}>
|
|
<LinearGradient
|
|
colors={getProgressColors(translateX.value / sliderWidth)}
|
|
style={[styles.progressGradient, { height }]}
|
|
start={{ x: 1, y: 0 }}
|
|
end={{ x: 0, y: 0 }}
|
|
/>
|
|
</Animated.View>
|
|
|
|
{/* 可拖拽的thumb */}
|
|
<PanGestureHandler onGestureEvent={gestureHandler}>
|
|
<Animated.View style={[styles.thumb, { width: thumbSize, height: thumbSize }, thumbStyle]}>
|
|
{/* <LinearGradient
|
|
colors={['#ffffff', '#f8fafc']}
|
|
style={styles.thumbGradient}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 0, y: 1 }}
|
|
/> */}
|
|
<Animated.View style={[styles.thumbInner, thumbInnerStyle]} />
|
|
</Animated.View>
|
|
</PanGestureHandler>
|
|
</View>
|
|
|
|
{/* 标签 */}
|
|
<View style={[styles.labelsContainer, { width: width }]}>
|
|
<Text style={styles.labelText}>轻微</Text>
|
|
<Text style={styles.labelText}>强烈</Text>
|
|
</View>
|
|
|
|
{/* 刻度 */}
|
|
<View style={[styles.scaleContainer, { width: width }]}>
|
|
{Array.from({ length: max - min + 1 }, (_, i) => i + min).map((num) => (
|
|
<View key={num} style={styles.scaleItem}>
|
|
<View style={[styles.scaleMark, value === num && styles.scaleMarkActive]} />
|
|
</View>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
alignItems: 'center',
|
|
paddingTop: 8,
|
|
paddingBottom: 4,
|
|
},
|
|
sliderContainer: {
|
|
height: 10,
|
|
justifyContent: 'center',
|
|
position: 'relative',
|
|
},
|
|
track: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
borderRadius: 6,
|
|
overflow: 'hidden',
|
|
},
|
|
trackGradient: {
|
|
flex: 1,
|
|
borderRadius: 6,
|
|
},
|
|
progress: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
borderRadius: 6,
|
|
overflow: 'hidden',
|
|
},
|
|
progressGradient: {
|
|
flex: 1,
|
|
borderRadius: 6,
|
|
},
|
|
thumb: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
overflow: 'hidden',
|
|
},
|
|
thumbGradient: {
|
|
flex: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
thumbInner: {
|
|
width: 16,
|
|
height: 28,
|
|
borderRadius: 8,
|
|
backgroundColor: '#ffffff',
|
|
borderWidth: 2,
|
|
borderColor: '#7a5af8',
|
|
},
|
|
valueContainer: {
|
|
marginTop: 20,
|
|
marginBottom: 12,
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 10,
|
|
backgroundColor: '#7a5af8',
|
|
borderRadius: 16,
|
|
shadowColor: '#7a5af8',
|
|
shadowOffset: { width: 0, height: 3 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 6,
|
|
elevation: 6,
|
|
},
|
|
valueText: {
|
|
fontSize: 20,
|
|
fontWeight: '800',
|
|
color: '#ffffff',
|
|
textAlign: 'center',
|
|
minWidth: 28,
|
|
},
|
|
labelsContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginTop: 6,
|
|
},
|
|
labelText: {
|
|
fontSize: 12,
|
|
color: '#5d6676',
|
|
fontWeight: '500',
|
|
},
|
|
scaleContainer: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginTop: 12,
|
|
},
|
|
scaleItem: {
|
|
alignItems: 'center',
|
|
flex: 1,
|
|
},
|
|
scaleMark: {
|
|
width: 1.5,
|
|
height: 8,
|
|
backgroundColor: '#e5e7eb',
|
|
borderRadius: 0.75,
|
|
},
|
|
scaleMarkActive: {
|
|
backgroundColor: '#7a5af8',
|
|
width: 2,
|
|
height: 10,
|
|
},
|
|
});
|