물리 시뮬레이션을 사용하여 위젯 애니메이션하기

샤워실의 바보·2024년 2월 29일
0

Flutter Animation

목록 보기
21/31
post-thumbnail

flutter 공식 문서 : 물리 시뮬레이션을 사용하여 위젯 애니메이션하기 번역

물리 시뮬레이션은 앱 상호작용을 현실적이고 상호적으로 느끼게 만들 수 있습니다. 예를 들어, 위젯을 마치 스프링이나 중력에 의해 떨어지는 것처럼 애니메이션하고 싶을 수 있습니다.

이 레시피는 스프링 시뮬레이션을 사용하여 드래그된 지점에서 중심으로 위젯을 이동하는 방법을 보여줍니다.

이 레시피는 다음 단계를 사용합니다:

  1. 애니메이션 컨트롤러 설정
  2. 제스처를 사용하여 위젯 이동
  3. 위젯 애니메이션
  4. 스프링 모션을 시뮬레이션하기 위한 속도 계산

1단계: 애니메이션 컨트롤러 설정

DraggableCard라는 상태가 있는 위젯으로 시작합니다:

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, super.key});

  final Widget child;

  
  State<DraggableCard> createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard> {
  
  void initState() {
    super.initState();
  }

  
  void dispose() {
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Align(
      child: Card(
        child: widget.child,
      ),
    );
  }
}

_DraggableCardState 클래스를 SingleTickerProviderStateMixin에서 확장하게 만들고, initState에서 AnimationController를 구성하고 vsyncthis로 설정합니다.

참고: SingleTickerProviderStateMixin을 확장하면 상태 객체가 AnimationControllerTickerProvider가 될 수 있습니다. 자세한 정보는 TickerProvider 문서를 참조하세요.

2단계: 제스처를 사용하여 위젯 이동

위젯이 드래그될 때 이동하게 만들고, _DraggableCardState 클래스에 Alignment 필드를 추가합니다:

// _DraggableCardState 클래스에 Alignment 필드 추가
Alignment _dragAlignment = Alignment.center;

onPanDown, onPanUpdate, onPanEnd 콜백을 처리하는 GestureDetector를 추가합니다. Align 위젯의 alignment_dragAlignment로 설정하여 정렬을 조정합니다:


Widget build(BuildContext context) {
  var size = MediaQuery.of(context).size;
  return GestureDetector(
    onPanDown: (details) {},
    onPanUpdate: (details) {
      setState(() {
        _dragAlignment += Alignment(
          details.delta.dx / (size.width / 2),
          details.delta.dy / (size.height / 2),
        );
      });
    },
    onPanEnd: (details) {},
    child: Align(
      alignment: _dragAlignment,
      child: Card(
        child: widget.child,
      ),
    ),
  );
}

3단계: 위젯 애니메이션

위젯이 놓였을 때, 그것이 중심으로 스프링 되돌아가야 합니다.

Animation<Alignment> 필드와 _runAnimation 메소드를 추가합니다. 이 메소드는 위젯이 드래그된 지점에서 중심점으로 보간하는 Tween을 정의합니다:



void _runAnimation() {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  _controller.reset();
  _controller.forward();
}

AnimationController가 값을 생성할 때 _dragAlignment를 업데이트합니다.

4단계: 스프링 모션 시뮬레이션을 위한 속도 계산

드래그가 끝난 후 위젯의 속도를 계산하여, 위젯이 그 속도로 실제로 계속 움직인 후에 스냅되도록 합니다. 먼저 flutter/physics.dart 패키지를 가져옵니다:

import 'package:flutter/physics.dart';

onPanEnd 콜백은 화면에서 손가락이 떨어졌을 때 포인터의 속도를 제공하는 DragEndDetails 객체를 제공합니다. 이 속도는 초당 픽셀 단위이지만, Align 위젯은 픽셀을 사용하지 않습니다. 대신 [-1.0, -1.0]에서 [1.0, 1.0] 사이의 좌표 값을 사용합니다. 여기서 [0.0, 0.0]은 중심을 나타냅니다. 2단계에서 계산된 크기를 사용하여 픽셀을 이 범위의 좌표 값으로 변환합니다.

마지막으로, AnimationController에는 SpringSimulation을 제공할 수 있는 animateWith() 메소드가 있습니다:

void _runAnimation(Offset pixelsPerSecond, Size size) {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  final unitsPerSecondX = pixelsPerSecond.dx / size.width;
  final unitsPerSecondY = pixelsPerSecond.dy / size.height;
  final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
  final unitVelocity = unitsPerSecond.distance;

  const spring = SpringDescription(
    mass: 30,
    stiffness: 1,
    damping: 1,
  );

  final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

  _controller.animateWith(simulation);
}

속도와 크기를 사용하여 _runAnimation()을 호출하는 것을 잊지 마세요:

onPanEnd: (details) {
  _runAnimation(details.velocity.pixelsPerSecond, size);
}

5. 코드 예시

```dart
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';

void main() {
  runApp(const MaterialApp(home: PhysicsCardDragDemo()));
}

class PhysicsCardDragDemo extends StatelessWidget {
  const PhysicsCardDragDemo({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

/// 놓았을 때 [Alignment.center]로 돌아가는 드래그 가능한 카드입니다.
class DraggableCard extends StatefulWidget {
  const DraggableCard({required this.child, super.key});

  final Widget child;

  
  State<DraggableCard> createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  /// 카드가 드래그되거나 애니메이션되는 동안의 정렬입니다.
  ///
  /// 카드가 드래그되는 동안 이 값은 GestureDetector onPanUpdate 콜백에서 계산된 값으로 설정됩니다.
  /// 애니메이션이 실행 중이면, 이 값은 [_animation]의 값으로 설정됩니다.
  Alignment _dragAlignment = Alignment.center;

  late Animation<Alignment> _animation;

  /// [SpringSimulation]을 계산하고 실행합니다.
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      AlignmentTween(
        begin: _dragAlignment,
        end: Alignment.center,
      ),
    );
    // 애니메이션 컨트롤러에서 사용되는 단위 구간, [0,1]에 대해 상대적인 속도를 계산합니다.
    final unitsPerSecondX = pixelsPerSecond.dx / size.width;
    final unitsPerSecondY = pixelsPerSecond.dy / size.height;
    final unitsPerSecond = Offset(unitsPerSecondX, unitsPerSecondY);
    final unitVelocity = unitsPerSecond.distance;

    const spring = SpringDescription(
      mass: 30,
      stiffness: 1,
      damping: 1,
    );

    final simulation = SpringSimulation(spring, 0, 1, -unitVelocity);

    _controller.animateWith(simulation);
  }

  
  void initState() {
    super.initState();
    _controller = AnimationController(vsync: this);

    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final size = MediaQuery.of(context).size;
    return GestureDetector(
      onPanDown: (details) {
        _controller.stop();
      },
      onPanUpdate: (details) {
        setState(() {
          _dragAlignment += Alignment(
            details.delta.dx / (size.width / 2),
            details.delta.dy / (size.height / 2),
          );
        });
      },
      onPanEnd: (details) {
        _runAnimation(details.velocity.pixelsPerSecond, size);
      },
      child: Align(
        alignment: _dragAlignment,
        child: Card(
          child: widget.child,
        ),
      ),
    );
  }
}
profile
공부하는 개발자

0개의 댓글