개요
FractionalTranslation은 자식을 그리기 전에 상대적인 이동 변환을 적용하는 위젯입니다.
이동은 자식의 크기에 대한 비율로 표현됩니다. 예를 들어, x가 0.25인 오프셋은 자식 너비의 1/4만큼 수평 이동을 의미합니다.
히트 테스트는 자식이 오버플로우되어 범위를 벗어나더라도 FractionalTranslation의 경계 내에서만 감지됩니다.
참조: https://api.flutter.dev/flutter/widgets/FractionalTranslation-class.html
언제 사용하나요?
- 자식의 크기에 상대적인 위치 조정이 필요할 때
- 반응형 애니메이션에서 크기 독립적인 이동이 필요할 때
- 오버레이나 툴팁의 위치를 동적으로 조정할 때
- 자식 위젯의 중앙점이나 모서리를 기준으로 이동시킬 때
- 슬라이드 인/아웃 애니메이션을 구현할 때
기본 사용법
// 오른쪽으로 절반, 아래로 1/4 이동
FractionalTranslation({
translation: { x: 0.5, y: 0.25 },
child: Container({
width: 100,
height: 100,
color: 'blue'
})
})
// 중앙으로 이동 (자신의 크기의 절반만큼 왼쪽 위로)
FractionalTranslation({
translation: { x: -0.5, y: -0.5 },
child: Container({
width: 50,
height: 50,
color: 'red'
})
})
// 위로 완전히 이동 (숨김)
FractionalTranslation({
translation: { x: 0, y: -1.0 },
child: Container({
width: 200,
height: 80,
color: 'green'
})
})
Props
translation (필수)
값: { x: number, y: number }
자식의 크기에 대한 비율로 표현되는 이동 오프셋입니다.
x
: 너비 대비 수평 이동 비율 (-무한대 ~ +무한대)y
: 높이 대비 수직 이동 비율 (-무한대 ~ +무한대)
값의 의미:
0.0
: 이동 없음1.0
: 자신의 너비(x) 또는 높이(y)만큼 이동-1.0
: 자신의 너비(x) 또는 높이(y)만큼 반대 방향 이동0.5
: 자신의 너비(x) 또는 높이(y)의 절반만큼 이동
FractionalTranslation({
translation: { x: 0.25, y: -0.5 }, // 오른쪽으로 1/4, 위로 1/2
child: child
})
FractionalTranslation({
translation: { x: -1.0, y: 0 }, // 완전히 왼쪽으로 이동
child: child
})
child
값: Widget | undefined
이동 변환이 적용될 자식 위젯입니다.
실제 사용 예제
예제 1: 슬라이드 인 애니메이션
const SlideInPanel = ({ isVisible, children }) => {
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (isVisible) {
setIsAnimating(true);
setTimeout(() => setIsAnimating(false), 300);
}
}, [isVisible]);
return AnimatedContainer({
duration: Duration.milliseconds(300),
curve: Curves.easeInOut,
child: FractionalTranslation({
translation: {
x: isVisible ? 0 : 1.0, // 화면 밖에서 슬라이드 인
y: 0
},
child: Container({
width: 300,
padding: EdgeInsets.all(20),
decoration: BoxDecoration({
color: 'white',
borderRadius: BorderRadius.only({
topLeft: Radius.circular(16),
topRight: Radius.circular(16)
}),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.2)',
blurRadius: 10,
offset: { x: 0, y: -2 }
})
]
}),
child: Column({
mainAxisSize: MainAxisSize.min,
children: children
})
})
})
});
};
예제 2: 커스텀 툴팁
const CustomTooltip = ({ text, position, isVisible }) => {
// 툴팁 위치에 따른 이동 계산
const getTranslation = () => {
switch (position) {
case 'top': return { x: -0.5, y: -1.1 }; // 위쪽, 중앙 정렬
case 'bottom': return { x: -0.5, y: 0.1 }; // 아래쪽, 중앙 정렬
case 'left': return { x: -1.1, y: -0.5 }; // 왼쪽, 세로 중앙
case 'right': return { x: 0.1, y: -0.5 }; // 오른쪽, 세로 중앙
default: return { x: 0, y: 0 };
}
};
return AnimatedOpacity({
opacity: isVisible ? 1.0 : 0.0,
duration: Duration.milliseconds(200),
child: FractionalTranslation({
translation: getTranslation(),
child: Container({
padding: EdgeInsets.symmetric({ horizontal: 12, vertical: 8 }),
decoration: BoxDecoration({
color: 'rgba(0,0,0,0.8)',
borderRadius: BorderRadius.circular(6)
}),
child: Text(text, {
style: TextStyle({
color: 'white',
fontSize: 12
})
})
})
})
});
};
예제 3: 오버레이 알림
const OverlayNotification = ({ message, type, onDismiss }) => {
const [isExiting, setIsExiting] = useState(false);
const handleDismiss = () => {
setIsExiting(true);
setTimeout(onDismiss, 300);
};
useEffect(() => {
const timer = setTimeout(handleDismiss, 4000);
return () => clearTimeout(timer);
}, []);
return FractionalTranslation({
translation: {
x: 0,
y: isExiting ? -1.2 : 0 // 위로 슬라이드아웃
},
child: AnimatedContainer({
duration: Duration.milliseconds(300),
margin: EdgeInsets.all(16),
padding: EdgeInsets.all(16),
decoration: BoxDecoration({
color: type === 'success' ? '#4CAF50' : '#F44336',
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow({
color: 'rgba(0,0,0,0.2)',
blurRadius: 8,
offset: { x: 0, y: 2 }
})
]
}),
child: Row({
children: [
Icon({
icon: type === 'success' ? Icons.check : Icons.error,
color: 'white'
}),
SizedBox({ width: 12 }),
Expanded({
child: Text(message, {
style: TextStyle({ color: 'white' })
})
}),
GestureDetector({
onTap: handleDismiss,
child: Icon({
icon: Icons.close,
color: 'white',
size: 18
})
})
]
})
})
});
};
예제 4: 이미지 갤러리 미리보기
const ImagePreview = ({ image, isExpanded, onToggle }) => {
return GestureDetector({
onTap: onToggle,
child: AnimatedContainer({
duration: Duration.milliseconds(400),
curve: Curves.easeInOut,
child: FractionalTranslation({
translation: isExpanded
? { x: -0.5, y: -0.5 } // 중앙으로 이동
: { x: 0, y: 0 }, // 원래 위치
child: Transform.scale({
scale: isExpanded ? 2.0 : 1.0,
child: Container({
width: 120,
height: 120,
decoration: BoxDecoration({
borderRadius: BorderRadius.circular(8),
boxShadow: isExpanded ? [
BoxShadow({
color: 'rgba(0,0,0,0.3)',
blurRadius: 20,
offset: { x: 0, y: 10 }
})
] : []
}),
child: ClipRRect({
borderRadius: BorderRadius.circular(8),
child: Image({
src: image.url,
objectFit: 'cover'
})
})
})
})
})
})
});
};
예제 5: 플로팅 액션 메뉴
const FloatingActionMenu = ({ isOpen, actions }) => {
return Column({
mainAxisSize: MainAxisSize.min,
children: [
// 액션 버튼들 (아래에서 위로 나타남)
...actions.map((action, index) =>
AnimatedContainer({
duration: Duration.milliseconds(200 + index * 50),
child: FractionalTranslation({
translation: {
x: 0,
y: isOpen ? 0 : 1.5 + (index * 0.2) // 아래로 숨김
},
child: Container({
margin: EdgeInsets.only({ bottom: 16 }),
child: Row({
mainAxisAlignment: MainAxisAlignment.end,
children: [
// 레이블
AnimatedOpacity({
opacity: isOpen ? 1.0 : 0.0,
duration: Duration.milliseconds(150),
child: Container({
padding: EdgeInsets.symmetric({
horizontal: 12,
vertical: 8
}),
decoration: BoxDecoration({
color: 'rgba(0,0,0,0.7)',
borderRadius: BorderRadius.circular(4)
}),
child: Text(action.label, {
style: TextStyle({
color: 'white',
fontSize: 12
})
})
})
}),
SizedBox({ width: 16 }),
// 액션 버튼
FloatingActionButton({
mini: true,
onPressed: action.onPressed,
backgroundColor: action.color,
child: Icon(action.icon)
})
]
})
})
})
})
),
// 메인 FAB
FloatingActionButton({
onPressed: () => setIsOpen(!isOpen),
child: AnimatedRotation({
turns: isOpen ? 0.125 : 0, // 45도 회전
duration: Duration.milliseconds(200),
child: Icon(Icons.add)
})
})
]
});
};
좌표계 이해
이동 방향
// 양수 방향
FractionalTranslation({
translation: { x: 1.0, y: 1.0 }, // 오른쪽 아래로 이동
child: child
})
// 음수 방향
FractionalTranslation({
translation: { x: -1.0, y: -1.0 }, // 왼쪽 위로 이동
child: child
})
// 한 방향만 이동
FractionalTranslation({
translation: { x: 0, y: -0.5 }, // 위로만 절반 이동
child: child
})
중앙 정렬 패턴
// 부모 중앙에 배치
Center({
child: FractionalTranslation({
translation: { x: -0.5, y: -0.5 },
child: Container({
width: 100,
height: 100,
color: 'blue'
})
})
})
주의사항
- 히트 테스트는 원래 위치 경계에서만 작동합니다
- 자식이 부모 영역을 벗어날 수 있으므로 클리핑을 고려해야 합니다
- 과도한 이동은 사용자 경험을 해칠 수 있습니다
- 애니메이션과 함께 사용할 때 성능을 고려해야 합니다
- 음수 값 사용 시 방향을 정확히 이해해야 합니다
관련 위젯
- Transform.translate: 절대적인 픽셀 단위 이동
- Positioned: Stack 내에서의 절대 위치 지정
- Align: 정렬 기반 위치 지정
- AnimatedPositioned: 애니메이션이 포함된 위치 지정
- Offset: 좌표 오프셋 표현 클래스