Skip to content

Commit 74d396b

Browse files
TinyKittenclaude
andcommitted
Reanimated 4.2のmapperバグ回避のためRN Animated APIに移行
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fdd1bf8 commit 74d396b

File tree

2 files changed

+100
-80
lines changed

2 files changed

+100
-80
lines changed

src/components/ChevronYamanote.tsx

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import { useId } from 'react';
2-
import { StyleSheet, View } from 'react-native';
3-
import Animated, {
4-
type SharedValue,
5-
useAnimatedStyle,
6-
} from 'react-native-reanimated';
2+
import { Animated, StyleSheet, View } from 'react-native';
73
import { LinearGradient, Path, Polygon, Stop, Svg } from 'react-native-svg';
84

95
type Props = {
10-
backgroundScaleSV?: SharedValue<number>;
6+
backgroundScaleAV?: Animated.Value;
117
arrived: boolean;
128
};
139

@@ -21,14 +17,9 @@ const localStyles = StyleSheet.create({
2117
},
2218
});
2319

24-
export const ChevronYamanote = ({ backgroundScaleSV, arrived }: Props) => {
20+
export const ChevronYamanote = ({ backgroundScaleAV, arrived }: Props) => {
2521
const id = useId();
2622

27-
// 赤い塗り部分だけを拡縮するアニメーションスタイル
28-
const fillScaleStyle = useAnimatedStyle(() => ({
29-
transform: [{ scale: backgroundScaleSV?.value ?? 1 }],
30-
}));
31-
3223
if (!arrived) {
3324
return (
3425
<Svg viewBox="0 0 393 296" width="100%" height="100%">
@@ -56,7 +47,14 @@ export const ChevronYamanote = ({ backgroundScaleSV, arrived }: Props) => {
5647
<Path fill="#fff" d="M268 4H4v288h264l120-144z" />
5748
</Svg>
5849
{/* 赤い塗りだけ Animated.View で拡縮 */}
59-
<Animated.View style={[localStyles.fill, fillScaleStyle]}>
50+
<Animated.View
51+
style={[
52+
localStyles.fill,
53+
backgroundScaleAV
54+
? { transform: [{ scale: backgroundScaleAV }] }
55+
: undefined,
56+
]}
57+
>
6058
<Svg viewBox="0 0 393.2 296" width="100%" height="100%">
6159
<LinearGradient
6260
id={id}

src/components/PadArch.tsx

Lines changed: 89 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { darken } from 'polished';
2-
import React, { useCallback, useEffect, useMemo } from 'react';
3-
import { StyleSheet, useWindowDimensions, View } from 'react-native';
4-
import Animated, {
5-
cancelAnimation,
6-
useAnimatedStyle,
7-
useSharedValue,
8-
withRepeat,
9-
withSequence,
10-
withTiming,
11-
} from 'react-native-reanimated';
2+
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
3+
import {
4+
Animated,
5+
Easing,
6+
StyleSheet,
7+
useWindowDimensions,
8+
View,
9+
} from 'react-native';
1210
import { Path, Svg } from 'react-native-svg';
1311
import type { Line, LineNested, Station } from '~/@types/graphql';
1412
import { isBusLine } from '~/utils/line';
@@ -415,71 +413,81 @@ const PadArch: React.FC<Props> = ({
415413
}: Props) => {
416414
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
417415

418-
// 共有値(Reanimated)
419-
const bgScale = useSharedValue(0.95);
420-
// シェブロンのアニメーションは 0..1 の単一タイムラインで駆動
421-
const chevronTimeline = useSharedValue(0);
422-
const fillHeight = useSharedValue(0);
416+
// Animated.Value(RN Animated API — Reanimated 4.2 の mapper バグ回避)
417+
const bgScale = useRef(new Animated.Value(0.95)).current;
418+
const chevronTimeline = useRef(new Animated.Value(0)).current;
419+
const fillHeight = useRef(new Animated.Value(0)).current;
423420

424421
// エフェクト: シェブロンと背景のアニメーション制御
425-
// biome-ignore lint/correctness/useExhaustiveDependencies: SharedValue は安定した参照のため依存配列に含めません
426422
useEffect(() => {
427-
// 既存のアニメーションを停止してから新しいアニメーションを開始
428-
cancelAnimation(bgScale);
429-
cancelAnimation(chevronTimeline);
430-
431423
if (arrived) {
432-
// 背景スケールを鼓動させる
433-
bgScale.value = withRepeat(
434-
withSequence(
435-
withTiming(0.8, { duration: YAMANOTE_CHEVRON_SCALE_DURATION }),
436-
withTiming(0.95, { duration: YAMANOTE_CHEVRON_SCALE_DURATION })
437-
),
438-
-1,
439-
false
440-
);
424+
chevronTimeline.stopAnimation();
425+
chevronTimeline.setValue(0);
426+
Animated.loop(
427+
Animated.sequence([
428+
Animated.timing(bgScale, {
429+
toValue: 0.8,
430+
duration: YAMANOTE_CHEVRON_SCALE_DURATION,
431+
easing: Easing.inOut(Easing.ease),
432+
useNativeDriver: true,
433+
}),
434+
Animated.timing(bgScale, {
435+
toValue: 0.95,
436+
duration: YAMANOTE_CHEVRON_SCALE_DURATION,
437+
easing: Easing.inOut(Easing.ease),
438+
useNativeDriver: true,
439+
}),
440+
])
441+
).start();
441442
} else {
442-
// タイムラインは2フェーズ(移動→フェード)でループ(合計 2x の所要時間)
443-
chevronTimeline.value = 0;
444-
chevronTimeline.value = withRepeat(
445-
withSequence(
446-
withTiming(1, { duration: YAMANOTE_CHEVRON_MOVE_DURATION * 2 }),
447-
withTiming(0, { duration: 0 })
448-
),
449-
-1,
450-
false
451-
);
443+
bgScale.stopAnimation();
444+
bgScale.setValue(0.95);
445+
Animated.loop(
446+
Animated.sequence([
447+
Animated.timing(chevronTimeline, {
448+
toValue: 1,
449+
duration: YAMANOTE_CHEVRON_MOVE_DURATION * 2,
450+
easing: Easing.linear,
451+
useNativeDriver: false,
452+
}),
453+
Animated.timing(chevronTimeline, {
454+
toValue: 0,
455+
duration: 0,
456+
useNativeDriver: false,
457+
}),
458+
])
459+
).start();
452460
}
453-
}, [arrived]);
461+
return () => {
462+
bgScale.stopAnimation();
463+
chevronTimeline.stopAnimation();
464+
};
465+
}, [arrived, bgScale, chevronTimeline]);
454466

455-
// エフェクト: マウント時と到着/出発切替またはウィンドウサイズ変更ごとに塗りつぶしアニメーション
456-
// biome-ignore lint/correctness/useExhaustiveDependencies: SharedValue は安定した参照のため依存配列に含めません
467+
// エフェクト: 塗りつぶしアニメーション(arrived 切替時にもリセットしたいため依存に含める)
468+
// biome-ignore lint/correctness/useExhaustiveDependencies: arrived は値変化時にアニメーションを再開するため必要
457469
useEffect(() => {
458-
fillHeight.value = 0;
459-
fillHeight.value = withTiming(windowHeight, {
470+
fillHeight.setValue(0);
471+
Animated.timing(fillHeight, {
472+
toValue: windowHeight,
460473
duration: YAMANOTE_LINE_BOARD_FILL_DURATION,
461-
});
474+
easing: Easing.out(Easing.ease),
475+
useNativeDriver: false,
476+
}).start();
477+
return () => {
478+
fillHeight.stopAnimation();
479+
};
462480
}, [arrived, fillHeight, windowHeight]);
463481

464-
// アニメーション用スタイル
465-
const fillStyle = useAnimatedStyle(() => ({ height: fillHeight.value }));
466-
const chevronContainerStyle = useAnimatedStyle(() => {
467-
if (arrived) return {};
468-
const p = chevronTimeline.value; // サイクル全体で 0..1 の進行度
469-
// 前半(0..0.5): 上方向に 24px 移動、後半は維持
470-
const movePhase = Math.min(p / 0.5, 1); // 前半中は 0..1
471-
// 後半(0.5..1): 不透明度 1 → 0.2、前半は 1 を維持
472-
const fadePhase = Math.max((p - 0.5) / 0.5, 0); // 後半中は 0..1
473-
const opacity = 0.2 + (1 - fadePhase) * 0.8; // 0.2..1 の範囲
474-
const translateY = -movePhase * 24;
475-
return {
476-
// 既定の rotate(-20deg) を維持したまま並記(transform は配列全体が上書きされるためここで回転も指定)
477-
transform: [{ rotate: '-20deg' }, { translateY }],
478-
opacity,
479-
};
482+
// シェブロン用の補間スタイル(非到着時のみ使用)
483+
const chevronOpacity = chevronTimeline.interpolate({
484+
inputRange: [0, 0.5, 1],
485+
outputRange: [1, 1, 0.2],
486+
});
487+
const chevronTranslateY = chevronTimeline.interpolate({
488+
inputRange: [0, 0.5, 1],
489+
outputRange: [0, -24, -24],
480490
});
481-
482-
// AnimatedChevron不要。SharedValueを直接渡す
483491

484492
const paths = useMemo(
485493
() => ({
@@ -627,7 +635,11 @@ const PadArch: React.FC<Props> = ({
627635

628636
{/* 暗色層: 区間ごとにViewクリッピングで色分け */}
629637
<Animated.View
630-
style={[styles.clipViewStyle, dynamicStyles.clipViewStyle, fillStyle]}
638+
style={[
639+
styles.clipViewStyle,
640+
dynamicStyles.clipViewStyle,
641+
{ height: fillHeight },
642+
]}
631643
>
632644
{colorSegments.map((seg) => (
633645
<View
@@ -660,7 +672,11 @@ const PadArch: React.FC<Props> = ({
660672
</Animated.View>
661673
{/* 主色層: 区間ごとにViewクリッピングで色分け */}
662674
<Animated.View
663-
style={[styles.clipViewStyle, dynamicStyles.clipViewStyle, fillStyle]}
675+
style={[
676+
styles.clipViewStyle,
677+
dynamicStyles.clipViewStyle,
678+
{ height: fillHeight },
679+
]}
664680
>
665681
{colorSegments.map((seg) => (
666682
<View
@@ -698,10 +714,16 @@ const PadArch: React.FC<Props> = ({
698714
dynamicStyles.chevron,
699715
arrived
700716
? [styles.chevronArrived, dynamicStyles.chevronArrived]
701-
: chevronContainerStyle,
717+
: {
718+
transform: [
719+
{ rotate: '-20deg' },
720+
{ translateY: chevronTranslateY },
721+
],
722+
opacity: chevronOpacity,
723+
},
702724
]}
703725
>
704-
<ChevronYamanote backgroundScaleSV={bgScale} arrived={arrived} />
726+
<ChevronYamanote backgroundScaleAV={bgScale} arrived={arrived} />
705727
</Animated.View>
706728

707729
<View style={styles.stationNames}>

0 commit comments

Comments
 (0)