본문 바로가기

Flutter & Dart

공부하다 배운 Flutter 꿀팁 무작정 적어두기 (5) - 동영상 플레이어 만들기

<노마드 코더님 강의 보면서 중요한 내용들 정리하기>


1. 로컬 이미지나 영상을 사용하려면, pubspec.yaml에서 경로를 명시해줘야 한다. 아래와 같이!

  assets:
    - assets/videos/video1.mp4

2. 비디오를 실행하기 위한 패키지와, visibility에 따른 함수를 콜백할 패키지를 추가해준다.

video_player: 2.7.2
visibility_detector: 0.3.3

3. 먼저 타임라인 페이지에 리스트뷰처럼 여러페이지를 한 화면에서 책 넘기듯 볼 수 있는 PageView 위젯을 생성, 컨트롤러 세팅, 스크롤 방향을 수직으로 세팅. 페이지가 바뀔 때 실행될 함수 세팅, 아이템 개수 세팅, 빌드 함수 세팅

  @override
  Widget build(BuildContext context) {
    return PageView.builder(
      controller: _pageController,
      scrollDirection: Axis.vertical,
      onPageChanged: _onPageChanged,
      itemCount: _itemCount,
      itemBuilder: (context, index) =>
          VideoPost(onVideoFinished: _onVideoFinished, index: index),
    );
  }

 

4. 이때 _onPageChanged 함수와 _itemCount는 다음과 같이 작동함. 먼저 _onPageChanged에서는 페이지가 바뀔 때 적용할 애니메이션을 설정함. curve에서는 애니메이션 종류를, duration은 시간을 세팅하고 itemCount의 초기값보다 한 개 적을 때 itemCount를 늘려준다.

void _onPageChanged(int page) {
    _pageController.animateToPage(
      page,
      duration: _scrollDuration,
      curve: _scrollCurve, // animation 종류
    );
    if (page == _itemCount - 1) {
      _itemCount = _itemCount + 4;
      setState(() {});
    }
  }

5. 아이템 빌더에서 작동하는 VideoPost 함수를 작성. 별도의 파일로 빼서 화면에 그려질 내용을 작성할 건데, Stack 위젯으로 쌓을 것.

** VideoPlayer 설정 **
1) 먼저 Positioned 위젯으로 VideoPlayer를 깔아줌. 초기화가 됐으면 깔아주고, 제대로 안 됐으면 블랙박스 깔게 세팅

Positioned.fill(
            child: _videoPlayerController.value.isInitialized
                ? VideoPlayer(_videoPlayerController)
                : Container(
                    color: Colors.black,
                  ),
          ),


2) 비디오 플레이어 컨트롤러 생성.

  final VideoPlayerController _videoPlayerController =  
  VideoPlayerController.asset("assets/videos/video1.mp4");


3) init 시에 비디오 플레이어를 초기화해줄 함수 세팅. 초기화 후 바로 실행되게끔 설정하고, 페이지가 변할 때 작동할 리스너도 세팅

 void _initVideoPlayer() async {
    await _videoPlayerController.initialize();
    _videoPlayerController.play();
    setState(() {});
  }
_videoPlayerController.addListener(_onVideoChange);


4) 페이지가 변할 때 작동하는 함수는 다음과 같음. 비디오의 총 길이와 비디오 플레이 바의 현재 위치가 같아질 때, 타임라인에서 넘겨 받은 onVideoFinished 함수를 실행.

void _onVideoChange() {
    if (_videoPlayerController.value.isInitialized) {
      if (_videoPlayerController.value.duration ==
          _videoPlayerController.value.position) {
        widget.onVideoFinished();
      }
    }
  }


5) onVideoFinished는 다음 페이지로 넘어가는 함수. 애니메이션 동작 종류와 작동 시간을 설정. (타임라인 페이지에)

  void _onVideoFinished() {
    _pageController.nextPage(
      duration: _scrollDuration,
      curve: _scrollCurve,
    );
  }


6) 화면을 탭했을 때 영상이 멈추고 다시 탭했을 때 재개하도록 위젯 세팅. 스택 위젯 안에 먼저 GestureDetector를 만들고, 

 Positioned.fill(
            child: GestureDetector(
              onTap: _onTogglePause,
            ),
          ),


_onTogglePause 함수를 세팅하는데, 이 때 비디오가 작동 중이면 멈추고, 아니면 재생하게 세팅. 그 다음 멈춰있는지의 여부를 바꿔준다.

void _onTogglePause() {
    if (_videoPlayerController.value.isPlaying) {
      _videoPlayerController.pause();
    } else {
      _videoPlayerController.play();
    }
    setState(() {
      _isPaused = !_isPaused;
    });
  }

** 애니메이션 설정 **
: 화면을 탭했을 때 재생 아이콘이 서서히 등장해서 눌리는 느끼는 느낌이 나게 연출하고 싶을 때 어떻게 해야 할까?

1) Transform.scale 위젯과 AnimatedOpacity 위젯을 활용해 멈춰있으면 플레이 아이콘이 보이게, 재생 중이면 안 보이게 세팅하고 아이콘의 사이즈는 유동적으로 변하게끔 애니메이션 컨트롤러의 값으로 설정한다. 이제 컨트롤러 값의 변화에 맞춰 재생 아이콘의 사이즈가 계속해서 갱신되게끔 설정하면 원하는 애니메이션이 만들어질 것!

Transform.scale(
                      scale: _animationController.value,
                      child: AnimatedOpacity(
                        duration: _animationDuration,
                        opacity: _isPaused ? 1 : 0,
                        child: const FaIcon(
                          FontAwesomeIcons.play,
                          color: Colors.white,
                          size: Sizes.size52,
                        ),
                      ),
                    );


2) Transform.scale 위젯을 AnimatedBuilder 위젯으로 감싸준다. AnimatedBuilder는 animation controller를 감지하고 변화가 있을 때마다 builder 메소드를 실행한다. Transform.scale을 builder 메소드 안의 return 값으로 넣어준다. animation 속성에는 _animationController를 세팅.

child: AnimatedBuilder(
                  // AnimatedBuilder는 animation controller를 감지하고 변할 때마다 builder 메소드를 실행함
                  animation: _animationController,
                  builder: (context, child) {
                    return Transform.scale(~~)

 
3) initState 함수 내에서 animationController를 선언해줌. vsync 속성은 this, lowerBound와 upperBound, value(초기값), duration을 설정해줌. 
- vsync : vsync에 전체 StatefulWidget을 의미하는 this를 세팅해두면 위젯이 안 보일 때 애니메이션이 작동하지 않도록 해서 불필요한 자원의 낭비를 예방함. 
- lowerBound : 애니메이션이 사라질 때의 값
- upperBound : 애니메이션이 완료될 때의 값
- value : 애니메이션 초기값

@override
  void initState() {
    super.initState();
    _initVideoPlayer();

    _animationController = AnimationController(
      vsync: this,
      lowerBound: 1.0,
      upperBound: 1.5,
      value: 1.5,
      duration: _animationDuration,
    );
  }


 4) onTogglePause에 animationController의 내용 추가. 플레이 중에 정지할 때는 reverse를 세팅해 upperBound에서 lowerBound로 애니메이트하도록, 즉 버튼이 나타나는데 커지면서 작아지는 형태로 나타나게끔, 다시 재생할 때는 forward를 세팅해 lowerBound에서 upperBound로 애니메이트하도록, 즉 버튼이 사라지는데 커지면서 사라지도록 세팅하는 것

 void _onTogglePause() {
    if (_videoPlayerController.value.isPlaying) {
      _videoPlayerController.pause();
      _animationController.reverse();
    } else {
      _videoPlayerController.play();
      _animationController.forward();
    }
}


 5) 여기까지 세팅하면 영상 실행 중 화면을 터치하면 애니메이션이 반영된 플레이 아이콘이 나오고 다시 아이콘을 누르면 영상이 실행됨. 문제는 아이콘을 정확히 터치해야 해당 내용들이 작동한다는 것! 화면을 터치해도 작동하게 하려면 아이콘을 감싼 AnimatedBuilder 위젯을 IgnorePointer 위젯으로 감싸주면 됨.



** 페이지를 서서히 바꿀 때, 화면이 완전히 나오기 전에는 재생되지 않도록 세팅하기 **

비디오 플레이어와 관련된 모든 내용을 포함한 Stack 위젯을 VisibilityDetector 위젯으로 감싸줌. VisibilityDetector 위젯은 위젯은 위젯의 visibility에 따라 callback할 함수를 세팅하게 해주는 위젯. 위젯과 콜백 함수는 아래와 같이 세팅.

* 위젯

@override
  Widget build(BuildContext context) {
    return VisibilityDetector(
      key: Key("${widget.index}"),
      onVisibilityChanged: _onVisibilityChanged,
      child: Stack(
        children: []
    )
  );
}

 
* 콜백 : 화면에서 보이는 비율이 1, 즉 전체 위젯이 다 보이면서 비디오가 재생 중이 아닐 때만 비디오를 실행하는 함수

void _onVisibilityChanged(VisibilityInfo info) {
    if (info.visibleFraction == 1 && !_videoPlayerController.value.isPlaying) {
      _videoPlayerController.play();
    }
  }

 


★ SingleTickerProviderStateMixin

class _VideoPostState extends State<VideoPost>
    with SingleTickerProviderStateMixin {
}


: 클래스 선언부에 위와 같이 선언하는데 여기서 SingleTickerProviderStateMixin가 무엇이냐?
SingleTickerProviderStateMixin는 Current tree가 활성화된 동안만 tick 하는 단일 ticker를 제공. 즉, 위젯이 화면에 보일 때만, ticker를 제공해준다는 말.
여기서 Ticker는 시계 같은 개념. 이 시계는 function을 실행할 건데, 분초 단위가 아니라 애니메이션의 프레임 단위마다 callback을 호출하는 것. 만약 화면이 멈춰 있을 때도 ticker가 돌아가면 자원 낭비일 것. 그래서 "Single"Ticker를 사용. 엄청 빠른 ticker이지만, 위젯이 화면에 보일 때만 작동하기 때문!
 vsync가 애니메이션의 재생을 도와준다면 SingleTicker~는 위젯이 위젯 tree에 있을 때만 ticker를 유지해줌 

 

출처 노마드 코더