onboarding: 转场动画、名字步取消自动跳页、标题个性化招呼语、Skip/提醒步等文案与交互优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
雷汀岚
2026-02-09 17:33:53 +08:00
parent aa4e1e9947
commit b4ec17fcac
11 changed files with 105 additions and 35 deletions

View File

@@ -95,8 +95,7 @@ export function NameInputStep({ value, onChangeText, onNext }: NameInputStepProp
blurOnSubmit={true}
onSubmitEditing={() => {
Keyboard.dismiss();
// 有输入时,“完成”直接进入下一步,避免真机卡在键盘上
if (value.trim().length > 0) onNext();
// 不再自動跳頁,僅收起鍵盤;前進需點擊底部 ➡️
}}
/>
</View>

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform } from 'react-native';
import React, { useRef, useEffect } from 'react';
import { View, StyleSheet, SafeAreaView, TouchableOpacity, StatusBar, Text, Image, Platform, Animated, Easing } from 'react-native';
import { useTranslation } from 'react-i18next';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
const TRANSITION_OFFSET = 24;
const TRANSITION_DURATION = 280;
interface OnboardingLayoutProps {
children: React.ReactNode;
title?: string;
@@ -11,6 +14,8 @@ interface OnboardingLayoutProps {
onSkip: () => void;
onBack?: () => void;
showBackButton?: boolean;
/** 用户名字仅在名字步骤之后的第一个问题currentStep === 1且非空时显示招呼语 */
userName?: string;
}
export function OnboardingLayout({
@@ -20,9 +25,48 @@ export function OnboardingLayout({
totalSteps,
onSkip,
onBack,
showBackButton = false
showBackButton = false,
userName = '',
}: OnboardingLayoutProps) {
const { t } = useTranslation();
const showGreeting = currentStep === 1 && userName.trim().length > 0;
const displayName = userName.trim();
const prevStepRef = useRef(currentStep);
const isFirstRenderRef = useRef(true);
const translateX = useRef(new Animated.Value(0)).current;
const opacity = useRef(new Animated.Value(1)).current;
useEffect(() => {
if (isFirstRenderRef.current) {
isFirstRenderRef.current = false;
prevStepRef.current = currentStep;
return;
}
if (prevStepRef.current === currentStep) return;
const direction = currentStep > prevStepRef.current ? 'forward' : 'back';
prevStepRef.current = currentStep;
const startX = direction === 'forward' ? TRANSITION_OFFSET : -TRANSITION_OFFSET;
translateX.setValue(startX);
opacity.setValue(0.72);
Animated.parallel([
Animated.timing(translateX, {
toValue: 0,
duration: TRANSITION_DURATION,
useNativeDriver: true,
easing: Easing.out(Easing.cubic),
}),
Animated.timing(opacity, {
toValue: 1,
duration: TRANSITION_DURATION,
useNativeDriver: true,
easing: Easing.out(Easing.cubic),
}),
]).start();
}, [currentStep, translateX, opacity]);
return (
<View style={styles.container}>
<StatusBar barStyle="dark-content" />
@@ -49,16 +93,29 @@ export function OnboardingLayout({
</TouchableOpacity>
</View>
{/* Title & Progress Row */}
{/* Title & Progress Row(名字步骤后第一步且名字非空时显示招呼语 + 问题) */}
<View style={styles.titleRow}>
<Text style={styles.questionTitle}>{title}</Text>
<View style={styles.titleBlock}>
{showGreeting && (
<Text style={styles.greetingText}>{t('onboardingSurvey.greeting', { name: displayName })}</Text>
)}
<Text style={styles.questionTitle}>{title}</Text>
</View>
<Text style={styles.progressText}>({currentStep}/{totalSteps})</Text>
</View>
{/* Content */}
<View style={styles.content}>
{/* Contentstep 切换时滑动 + 淡入 */}
<Animated.View
style={[
styles.content,
{
opacity,
transform: [{ translateX }],
},
]}
>
{children}
</View>
</Animated.View>
</SafeAreaView>
</View>
);
@@ -114,13 +171,22 @@ const styles = StyleSheet.create({
alignItems: 'flex-end',
paddingHorizontal: 20,
marginTop: 20,
marginBottom: 20,
marginBottom: 8,
},
titleBlock: {
flex: 1,
justifyContent: 'flex-end',
},
greetingText: {
fontSize: 22,
color: OnboardingColors.questionTitle,
fontFamily: OnboardingFont.question,
marginBottom: 4,
},
questionTitle: {
fontSize: 22,
color: OnboardingColors.questionTitle,
fontFamily: OnboardingFont.question,
flex: 1,
},
progressText: {
fontSize: 18,

View File

@@ -19,8 +19,8 @@ export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) {
const insets = useSafeAreaInsets();
const handleReduce = () => {
// 允许 050 表示关闭每日提醒
if (value > 0) onChange(value - 1);
// 本页最小为 1不接收提醒请使用右上角 Skip
if (value > 1) onChange(value - 1);
};
const handleAdd = () => {
@@ -36,7 +36,9 @@ export function ReminderStep({ value, onChange, onFinish }: ReminderStepProps) {
<View style={styles.numberWrapper}>
<Text style={styles.numberText}>{value}</Text>
<Text style={styles.unitText}>{t('dailyReminder.timesUnit')}</Text>
<Text style={styles.unitText}>
{value === 1 ? t('dailyReminder.timesUnitSingular') : t('dailyReminder.timesUnit')}
</Text>
</View>
<TouchableOpacity onPress={handleAdd} activeOpacity={0.7}>

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { View, StyleSheet, TouchableOpacity, ScrollView, Text } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { OnboardingColors, OnboardingFont } from '@/constants/OnboardingTheme';
import SelectedIcon from '@/assets/images/icon/selected_icon.svg';
import BtnNotClicked from '@/assets/images/icon/btn_Notclicked.svg';
import BtnClicked from '@/assets/images/icon/btn_clicked.svg';
@@ -39,16 +38,11 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
return (
<TouchableOpacity
key={option.id}
style={styles.optionCard}
style={[styles.optionCard, isSelected && styles.optionCardSelected]}
onPress={() => onToggle(option.id)}
activeOpacity={0.7}
>
<Text style={styles.optionText}>{option.label}</Text>
{isSelected && (
<View style={styles.iconWrapper}>
<SelectedIcon width={20} height={20} />
</View>
)}
</TouchableOpacity>
);
})}
@@ -67,7 +61,7 @@ export function SelectionStep({ options, selectedIds, onToggle, onNext, onSkip }
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 20,
paddingTop: 8,
},
scroll: {
flex: 1,
@@ -82,7 +76,7 @@ const styles = StyleSheet.create({
borderRadius: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
justifyContent: 'center',
paddingHorizontal: 24,
marginBottom: 12,
shadowColor: '#000',
@@ -91,15 +85,14 @@ const styles = StyleSheet.create({
shadowRadius: 10,
elevation: 2,
},
optionCardSelected: {
backgroundColor: OnboardingColors.cardSelected,
},
optionText: {
fontSize: 18,
color: OnboardingColors.textPrimary,
fontWeight: '500',
fontFamily: OnboardingFont.question,
flex: 1,
},
iconWrapper: {
marginLeft: 10,
},
footer: {
position: 'absolute',