[Flutter] 제스쳐 인식

Sooooooah·2023년 4월 28일
0

플러터

목록 보기
6/7

물리 시뮬레이션 사용하기

flutter.dev

물리 시뮬레이션을 사용하면 앱과의 상호작용을 사실적으로 느낄 수 있다.

스프링 시뮬레이션을 사용하여 위젯을 드래그한 지점에서 중앙으로 돌아오는 애니메이션을 넣어보자

1. 애니메이션 컨트롤러 설정

State를 확장하고 컨트롤러를 초기화 시킨다.

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

  
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  
  void dispose() {
    _controller.dispose();

    super.dispose();
  }
}

2. 위젯에 제스쳐 더하기

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

+ Alignment _dragAlignment = Alignment.center;

GestureDetector 으로 움직임을 더하자

  
  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. 위젯에 애니메이션을 넣기

제스처가 끝나면 위젯이 다시 중앙으로 돌아갈 수 있도록 애니메이션을 추가하자

state 안에서 _runAnimation() 를 정의해준다.

class _DraggableCardState extends State<DraggableCard>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
+ late Animation<Alignment> _animation;
  Alignment _dragAlignment = Alignment.center;
  
  /// 위젯이 중앙으로 돌아가는 애니메이션
  void _runAnimation() {
    _animation = _controller.drive(
      AlignmentTween(
        begin: _dragAlignment,
        end: Alignment.center,
      ),
    );
    _controller.reset();
    _controller.forward();
  }

애니메이션 컨트롤러를 업데이트 해준다.

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

화면 구성

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

GestureDetector의 동작에 애니메이션을 넣어주자

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

속도 개선을 위해 physics package를 추가

/// 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);
}

호출된 곳에서 인자값도 업데이트 해주자

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

Cookbook

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

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

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: const Center(
        child: 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>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<Alignment> _animation;
  Alignment _dragAlignment = Alignment.center;

  
  void initState() {
    super.initState();

    _controller =
        AnimationController(vsync: this, duration: const Duration(seconds: 1));
    _controller.addListener(() {
      setState(() {
        _dragAlignment = _animation.value;
      });
    });
  }

  
  void dispose() {
    _controller.dispose();

    super.dispose();
  }

  /// 위젯이 중앙으로 돌아가는 애니메이션
  /// 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);
  }

  
  Widget build(BuildContext context) {
    var 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개의 댓글