원본 공식 문서

1. Set up an animation controller

2. Move the widget using gestures

3. Animate the widget

4. Calculate the velocity to simulate a springing motion

이 페이지를 번역하면서 공부하려고 했는 데, 실제로 코드를 작성해보고 실행이 안되었다. 워닝과 에러가 너무 많았다. DraggableCard 클래스의 this.child 에 어떠한 값도 입력이 되지 않았고, _DraggableCardState 클래스의 _controller_animation 가 초기화되지 않았다. 그냥 번역하면서 공부한 것으로 만족해야겠다.

다음부터는 공부하기 전에 미리 full code를 실행해보고 동작하면 공부해봐야겠다.

1. Set up an animation controller

먼저, Stateful Widget인 DraggableCard을 작성하고 시작한다.

import 'package:flutter/material.dart';

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

class PhysicsCardDragDemo extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: DraggableCard(
        child: FlutterLogo(
          size: 128,
	        )
      ),
    );
  }
}

class DraggableCard extends StatefulWidget {
  final Widget child;
  DraggableCard({ this.child }); // <-- 오류난다.

  @override
  _DraggableCardState createState() => _DraggableCardState();
}

class _DraggableCardState extends State<DraggableCard> {
  @override
  void initState() {
    super.initState();
  }
  @override
  void dispose() {
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Align(
      child: Card(
        child: widget.child,
      ),
    );
  }
}

큰 틀인 PhysicsCardDragDemo는 바뀌지 않아도 되기 때문에 StatelessWidget을 상속받는 것같아 보인다.

  • DraggableCardState 클래스가 SingleTickerProviderStateMixin로부터 상속받도록 해라, 그러면 위젯을 껐다/켰다 가능하게하는 Ticker를 사용할 수 있게 된다.

    SingleTickerProviderStateMixin으로부터 상속받으려면 다음과 같이 State를 사용하여 상속을 받으면된다. [TickerMode class](https://api.flutter.dev/flutter/widgets/TickerMode-class.html).

    참 신기하게 만들었다. State에 넣으면 SingleTikcerProviderStateMixin으로부터 상속받게했다.

  • _AnimationController 생성자를 다음과 같이 initState, vsync 설정을 해라.

    • initState로 AnimationController를 객체화
    • vsync는 애니메이션 최적화 및 언제 재생할 지 시간을 세어주는 등의 기능을 위해 필요하다. 여기서 this로 되어있는 것은 레퍼런스가 존재하는 객체가 vsync역할을 해준다는 의미이다.

    여기가 정말 잘 설명되어있다

    @override
      void initState() {
        _controller = //<-- AnimationController의 instance
            AnimationController(vsync: this, duration: Duration(seconds: 1));
        super.initState();
      }
    

_DraggableCardState 클래스에 다음을 추가한다.

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

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(seconds: 1));
    super.initState();
  }


  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  //...

_controller 부분은 생성자인 것같고, _controller.dispose(); 는 소멸자 부분인 것같다.

2. Move the widget using gestures

위젯을 드래그하면 움직이도록 만들어보자. 그리고 _DraggableCardState 클래스에 Alignment field를 적용해볼 것이다.

Alignment _dragAlignment = Alignment.center;

GestureDetector를 추가하여 onPanDown, onPanUpdate , onPanEnd 콜백을 조정하도록 할 것이다. 이 alignment를 조정하기 위해서는 화면의 사이즈를 가져오는 MediaQuery가 위젯의 사이즈를 얻을 수 있어야하고 다음 코드에 나와있는 것처럼 그 사이즈를 2로 나누어야한다. 그러고나서, Align 위젯의 alignment를 _dragAlignment로 세팅한다.

@override
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(
      child: Card(
        child: widget.child,
      ),
    ),
  );
}



3. Animate the widget

위젯이 만들어졌다면 가운데로 정렬이 되어야한다.

Animation와 _runAnimation 메서드를 추가한다. 이 메서드는 위젯이 드래그된 지점과 가운데 지점 사이를 보간하는 Tween(선형 보간법)을 정의합니다. → 말이 좀 어렵다.

왜 가운데 지점과 드래그 지점의 보간을 하는 지 잘모르겠다.

Animation<Alignment> _animation;

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

다음은, AnimationController가 value를 생산할 때 _dragAlignment를 업데이트한다.

@override
void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: Duration(seconds: 1));
  _controller.addListener(() {
    setState(() {
      _dragAlignment = _animation.value;
    });
  });
}

다음은, Align 위젯이 _dragAlignment를 사용하도록 하는 것이다.

child: Align(
  alignment: _dragAlignment,
  child: Card(
    child: widget.child,
  ),
),

마지막으로 GestureDetector를 업데이트하여 animation controller를 관리할 수 있도록 한다.

onPanDown: (details) {
 _controller.stop();
},
onPanUpdate: (details) {
 setState(() {
   _dragAlignment += Alignment(
     details.delta.dx / (size.width / 2),
     details.delta.dy / (size.height / 2),
   );
 });
},
onPanEnd: (details) {
 _runAnimation();
},



4. Calculate the velocity to simulate a springing motion

마지막 단계는 약간의 수학을 사용해서 드래그된 위젯의 속도를 계산하여 현실감있게 움직이도록 하는 것이다.

첫째로 physics package를 import한다.

import 'package:flutter/physics.dart;

onPanEnd 콜백은 DragEndDetails라는 객체를 제공한다. 이 객체는 스크린에 입력이 멈추면 포인터의 속도를 계산하여 그 값을 넘겨준다. 이 속도는 pixel/second 로 계산되며 Align위젯은 픽셀을 사용하지 않는다. 이 좌표값은 [-1.0, -1.0]과 [1.0, 1.0]의 값이며, [0.0, 0.0]값은 가운데를 의미합니다. STEP2에서 계산된 size는 픽셀에서 좌표값으로 변환된다.

마지막으로 AnimationController는 SpringSimulation에 주어지는 animateWith() 메서드를 가지게 된다.

void _runAnimation(Offset pixelsPerSecond, Size size) {
  _animation = _controller.drive(
    AlignmentTween(
      begin: _dragAlignment,
      end: Alignment.center,
    ),
  );
  // Calculate the velocity relative to the unit interval, [0,1],
  // used by the animation controller.
  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. 전체 코드

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

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

class PhysicsCardDragDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: DraggableCard(
        child: FlutterLogo(
          size: 128,
        ),
      ),
    );
  }
}

/// A draggable card that moves back to [Alignment.center] when it's
/// released.
class DraggableCard extends StatefulWidget {
  final Widget child;
  DraggableCard({this.child});

  @override
  _DraggableCardState createState() => _DraggableCardState();
}

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

  /// The alignment of the card as it is dragged or being animated.
  ///
  /// While the card is being dragged, this value is set to the values computed
  /// in the GestureDetector onPanUpdate callback. If the animation is running,
  /// this value is set to the value of the [_animation].
  Alignment _dragAlignment = Alignment.center;

  Animation<Alignment> _animation;

  /// Calculates and runs a [SpringSimulation].
  void _runAnimation(Offset pixelsPerSecond, Size size) {
    _animation = _controller.drive(
      AlignmentTween(
        begin: _dragAlignment,
        end: Alignment.center,
      ),
    );
    // Calculate the velocity relative to the unit interval, [0,1],
    // used by the animation controller.
    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);
  }

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

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

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

  @override
  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,
        ),
      ),
    );
  }
}