[프로젝트] 영상플레이어 (4) 동영상 위 버튼 올리기
들어가기 앞서...
동영상위의 버튼 플레이 버튼을 만들고 컨트롤 해보자!
본문으로...
1. 비디오 플레이어를 원래 사이즈로 만들까?
@override
Widget build(BuildContext context) {
if (videoController == null) {
return CircularProgressIndicator();
}
return AspectRatio(
aspectRatio: videoController!.value.aspectRatio,
child: Stack(
children: [
VideoPlayer(videoController!),
_Controls(),
],
));
}
}
class _Controls extends StatelessWidget {
const _Controls({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
IconButton(
onPressed: () {},
iconSize: 30.0,
color: Colors.white,
icon: Icon(Icons.play_arrow))
],
);
}
1) 아이콘 하나를 만들었는데, 공통되는 함수로 만들어서 분리시키자!
class _Controls extends StatelessWidget {
const _Controls({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
renderIconButton(onPressed: () {}, iconData: Icons.rotate_left),
renderIconButton(onPressed: () {}, iconData: Icons.play_arrow),
renderIconButton(onPressed: () {}, iconData: Icons.rotate_right),
],
);
}
// 중복되는 아이콘 함수로 빼기
Widget renderIconButton({
required VoidCallback onPressed,
required IconData iconData,
}) {
return IconButton(
onPressed: onPressed,
iconSize: 30.0,
color: Colors.white,
icon: Icon(iconData));
}
}
2) 상단 갤러리 버튼 만들어주자!
...
child: Stack(
children: [
VideoPlayer(videoController!),
_Controls(),
Positioned(
// 오른쪽에 0 픽셀만큼 위치시킨다.
right: 0,
child: IconButton(
onPressed: () {},
color: Colors.white,
iconSize: 30.0,
icon: Icon(
Icons.photo_camera_back,
)),
)
],
)
...
2. 플레이할때 분기처리!
재생중에는 일시정지 버튼으로, 일시정지중에는 재생버튼으로 분기처리해야한다.
그러기위해선, 현재 비디오가 플레이 중인지 아닌지를 알면된다.
// 클래스 부분
_Controls(
onPlayPressed: onPlayPressed,
onReversePressed: onReversePressed,
onForwardPressed: onForwardPressed,
isPlaying: videoController!.value.isPlaying,
),
...
// 함수부분
void onPlayPressed() {
setState(() {
if (videoController!.value.isPlaying) {
// 실행중이면 중지
videoController!.pause();
} else {
// 실행중 아니면 실행
videoController!.play();
}
});
}
...
// 버튼 부분
renderIconButton(
onPressed: onPlayPressed,
iconData: isPlaying ? Icons.pause : Icons.play_arrow
),
3. 이전 버튼, 이후 버튼 함수 추가!
void onPlayPressed() {
setState(() {
if (videoController!.value.isPlaying) {
// 실행중이면 중지
videoController!.pause();
} else {
// 실행중 아니면 실행
videoController!.play();
}
});
}
void onReversePressed() {
final currentPosition = videoController!.value.position;
Duration position = Duration(); // 기본 0초로 세팅
if (currentPosition.inSeconds > 3) {
// 현재 시간 이 3 초보다 크면은 3초를 빼준다.
position = currentPosition - Duration(seconds: 3);
}
videoController!.seekTo(position);
}
void onForwardPressed() {
final maxPosition = videoController!.value.duration;
final currentPosition = videoController!.value.position;
Duration position = maxPosition; // 전체 영상의 길이
if ((maxPosition - Duration(seconds: 3)).inSeconds >currentPosition.inSeconds ) {
// 전체 시간 - 3 초 > 현재 초
position = currentPosition + Duration(seconds: 3);
}
// 현재 시간부터 재생을 다시 할 수 있게 position을 넘겨준다.
videoController!.seekTo(position);
}
4. 슬라이더 만들기
// 영상이 재생시간을 저장할 변수
Duration currentPosition = Duration();
...
children: [
...
Positioned(
bottom: 0,
right: 0,
left: 0,
child: Padding(
// 패딩 추가
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
// 1번 설명 (currentPosition.inSeconds % 60)
// 2번 설명 (.padLeft(2,"0"))
Text(
'${currentPosition.inMinutes}:${(currentPosition.inSeconds % 60).toString().padLeft(2,"0")}',
style: TextStyle(
color: Colors.white,
),
),
Expanded(
child: Slider(
value: currentPosition.inSeconds.toDouble(),
onChanged: (double val) {
setState(() {
currentPosition = Duration(seconds: val.toInt());
});
},
max: videoController!.value.duration.inSeconds.toDouble(),
min: 0,
),
),
Text(
'${videoController!.value.duration.inMinutes}:${(videoController!.value.duration.inSeconds % 60).toString().padLeft(2,"0")}',
style: TextStyle(
color: Colors.white,
),
),
],
),
),
)
...
]
1번 설명)
currentPostion.inSeconds 는 영상의 총길이를 초로 나타낸 값이다.
그렇다면 영상이 1분이넘어가면 80초 이렇게 나올수 있는데, 앞에 inMinutes 를 사용해서 분을 표현했으니, 초에서는 60초 이상이 출력되면 안된다.
그래서 60 으로 나눠서 나머지값만 출력하면 이상없다.
2번 설명)
.padLeft(2,' 0') 는 최대 길이는 2자리이고, 나머지 자리를 채우는 것은 '0' 으로 설정한 것이다..
출처
https://api.flutter.dev/flutter/dart-core/String/padLeft.html
5. 비디오와 슬라이더가 싱크가 맞게 해주기
현재까지는 슬라이더를 클릭하면 해당 초에서 재생이 되지 않는다.
그 이유는 Sider() 위젯의 onChange 함수가 슬라이더만 클릭해서 실행이되고, 재생을 눌렀을때는 다시 0 초 부터 시작을 하기 때문이다.
그래서 onChange 함수에 비디오컨트롤러의 현재 시간을 보내주고 플레이를 시켜주면 된다.
Slider(
value: currentPosition.inSeconds.toDouble(),
onChanged: (double val) {
// 기존
// setState(() {
// currentPosition = Duration(seconds: val.toInt());
// });
// 밑의 코드처럼 변경
videoController!.seekTo(
Duration(
seconds: val.toInt(),
),
);
},
max: videoController!.value.duration.inSeconds
.toDouble(),
min: 0,
),
그렇다면 플레이가 잘 되는것을 볼 수 있다.
6. 코드 정리
...
_SliderBottom(
currentPosition: currentPosition,
maxPosition: videoController!.value.duration,
onSliderChanged: onSliderChanged)
...
void onSliderChanged(double val) {
videoController!.seekTo(
Duration(
seconds: val.toInt(),
),
);
}
...
class _SliderBottom extends StatelessWidget {
final Duration currentPosition;
final Duration maxPosition;
final ValueChanged<double> onSliderChanged;
const _SliderBottom(
{Key? key,
required this.currentPosition,
required this.maxPosition,
required this.onSliderChanged})
: super(key: key);
@override
Widget build(BuildContext context) {
return Positioned(
bottom: 0,
right: 0,
left: 0,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
Text(
'${currentPosition.inMinutes}:${(currentPosition.inSeconds % 60).toString().padLeft(2, "0")}',
style: TextStyle(
color: Colors.white,
),
),
Expanded(
child: Slider(
value: currentPosition.inSeconds.toDouble(),
onChanged: onSliderChanged,
max: maxPosition.inSeconds.toDouble(),
min: 0,
),
),
Text(
'${maxPosition.inMinutes}:${(maxPosition.inSeconds % 60).toString().padLeft(2, "0")}',
style: TextStyle(
color: Colors.white,
),
),
],
),
),
);
}
}
버그 수정
상황: Row() 위젯에다가 CrossAxisAlignment.stretch 로 해놨기 떄문에 버튼의 위, 아래를 클릭해서 onPressed 가 된다.
7. 비디오 컨트롤하는 UI 를 탭했을때만 보이게 하기!
제스쳐로 감싸주고 boolean 변수로 값 담아서 분기처리 해주면 된다.
// 초기에는 false 로 안보이게 하는 boolean 변수 선언
bool showControls = false;
...
return AspectRatio(
aspectRatio: videoController!.value.aspectRatio,
child: GestureDetector(
onTap: (){
setState(() {
showControls = !showControls;
})
}
),
child: Stack(
children: [
...
// 분기처리 추가
if(showControls){}
]
)
)
8. 새로운 비디오 불러오기 버튼 기능 만들기!
// home_screen.dart
// 비디오 있을 때 뷰
Widget renderVideo() {
return CustomVideoPlayer(
video: video!,
onNewVideoPressed: onNewVideoPressed,
);
}
// 기존에 로고클릭하면 나오는 함수 그대로 가져다 쓴다. 이름은 바꿔주자 onLogoTap -> onNewVideoPressed
void onNewVideoPressed() async {
// ImagePicker 를 통한 갤러리의 비디오 가져오기
final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
if (video != null) {
setState(() {
this.video = video;
});
}
}
// custom_video_player.dart
class CustomVideoPlayer extends StatefulWidget {
final VoidCallback onNewVideoPressed;
...
_NewVideo(onPressed: widget.onNewVideoPressed),
...
}
아이콘을 클릭하면 갤러리의 비디오를 선택할 수 있는 창이 정상적으로 잘 뜬다.
하지만, 새로운 동영상을 불러올수는 없다. 그 이유는!
videoController 가 생성되는 시점은 initState() 함수 안에서 존재한다.
initState 함수는 StatefulWidget 이 생성될때 딱! 한번 만 실행이 되기 때문에, 아무리 새로운 video 가 들어온다고 해도 initState() 가 실행이 되지 않으니, videoController 컨트롤 생성은 물론, 초기화가 되지 않는다.
그러니 해결방법은 StatefulWidget 의 생명 주기 할때 배운 State가 살아있는 상태에서 파라미터만 바뀌었을때(StatefulWidget 이 실행이 된적있는데, 파라미터만 변경이 되었을떄)! didUpdateWidget 이 호출이 된다고 배웠는데, 그 위젯안에 기존의 비디오 경로와 새로운 비디오 경로가 다르면 initializeConroller() 함수를 실행시켜주면 된다.
// StatefulWidget 이 실행이 되었고, 파라미터값만 변경이 되었을때 didUpdateWidget 이 호출된다.
@override
void didUpdateWidget(covariant CustomVideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if(oldWidget.video.path != widget.video.path) {
initializeController();
}
}
코드 추가
마지막으로 비디오를 보다가 새로운 영상을 추가해준다고 한다면, 새로추가한 영상의 postion 을 처음으로(0) 안돌려줘서 그렇다.
// 코드 추가
initializeController() async {
// Duration() 으로 돌려놓는다.
currentPosition = Duration();
...
}
끝으로...
- 비디오 컨트롤러를 통해서 영상의 길이, 영상 플레이 중인지 아닌지, 이전버튼, 이후버튼 함수 컨트롤을 배웠다!
- didUpdateWidget 이 언제 실행이 되는 지 알 수 있다.(StatefulWidget 이 실행되고, 파라미터값이 바뀌었을때)
- Controller 와 State 안의 변수를 서로 연동시키는 방법을 배웠다.
