이번 글에서는 수평으로 하나의 사이즈가 큰 이미지를 백그라운드 배경으로 사용하면서 페이지 뷰 처럼 수평으로 슬라이딩 할 수 있는 위젯을 소개하려고 한다.
아래 공유한 결과물을 확인하면 이해가 될 것이다.
백프로 완성된 상태는 아니다. 애니메이션이 부자연스러운 부분이 있어, 이 부분의 개선은 필요하다. 실제로 이런 뷰가 필요하여 우선 개발을 해본 상태이고, 나중에 실제 사용될 때에 완성된 상태로 다시 글을 추가할 예정이다.
먼저 해당 슬라이더 뷰를 구현하기 위해 필요한 뷰의 갯수인 itemCount와 items를 필수로 받아와야 한다.
class OverImageCustomSlider extends StatefulWidget {
final int itemCount;
final List<Widget> items;
const OverImageCustomSlider({
super.key,
required this.itemCount,
required this.items,
});
슬라이더를 구현하기 위한 스와이프 기능 사용에 현재 position과 index 값을 선언하여 해당 값들만 사용하여 기능을 개발하였다.
double _position = 0.0;
int _currentIndex = 0;
구조는 간단하다. 백그라운드 이미지로 사용될 이미지를 Stack 구조로 넣고 그 위에 아이템을 수평으로 나열해 주면 된다.
build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return SizedBox(
height: MediaQuery.of(context).size.height -
MediaQueryData.fromWindow(window).padding.top -
kToolbarHeight,
child: Stack(
children: [
Positioned(
left: _position,
...
),
],
)
);
}
Widget
이미지 위젯인데, 해당 가로폭을 디바이스 사이즈의 필수 파라미터로 받아온 itemCount 갯수 만큼 곱해주면 된다.
Container(
width: _width * widget.itemCount,
height: MediaQuery.of(context).size.height -
MediaQueryData.fromWindow(window).padding.top -
kToolbarHeight,
decoration: const BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(
Colors.black, BlendMode.saturation),
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
image: AssetImage(
"assets/images/city/panorama_city.jpeg"))),
child: Row(
children: [
...
],
),
),
Row 위젯에 배치할 위젯 관련 코드이며, 받아온 items를 각 인덱스에 맞게 수평으로 나열시키면 된다.
child: Row(
children: [
...List.generate(
widget.itemCount,
(index) => SizedBox(
width: _width,
child: widget.items[index],
))
],
),
이렇게 배치한 구조의 Positioned를 이동시켜 주면된다.
Positioned(
left : _position,
child: GestureDetector(
onHorizontalDragEnd: (details){},
onHorizontalDragUpdate: (details){},
child : Container(
...
),
),
),
현재 postion에서 DragUpdateDetails 객체의 dx 값을 더해서 포지션을 이동시켜 주자.
onHorizontalDragUpdate: (details) {
setState(() {
_position = _position + details.delta.dx;
});
},
onHorizontalDragEnd 함수는 드래그를 놓았을 때 수신되는 함수로 해당 함수가 호출될 때에 적절한 포지션으로 이동시키면 된다.
여기 부분에서 애니메이션 처리가 되어있지 않아 이 부분의 코드를 개선하여야 한다.
onHorizontalDragEnd: (details) {
double _halfWidth = _width * _currentIndex;
double _nextPoint = _currentIndex == 0
? (_width / 3)
: _halfWidth + (_width / 3);
double _beforePoint = (_width * _currentIndex) - (_width / 3);
if (-_position > _nextPoint) {
setState(() {
_position = -((_width * _currentIndex) + _width);
_currentIndex = _currentIndex + 1;
if (_currentIndex > widget.itemCount - 1) {
_position = -(_width * (_currentIndex - 1));
_currentIndex = widget.itemCount - 1;
}
});
} else if (-_position < _beforePoint) {
setState(() {
_position = -(_width * _currentIndex) + _width;
_currentIndex = _currentIndex - 1;
if (_currentIndex < 0) {
_position = 0.0;
_currentIndex = 0;
}
});
} else {
setState(() {
_position = -(_width * _currentIndex);
});
}
},
https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/ui/slider/over_image
class OverImageCustomSlider extends StatefulWidget {
final int itemCount;
final List<Widget> items;
const OverImageCustomSlider({
super.key,
required this.itemCount,
required this.items,
});
State<OverImageCustomSlider> createState() => _OverImageCustomSliderState();
}
class _OverImageCustomSliderState extends State<OverImageCustomSlider> {
double _position = 0.0;
int _currentIndex = 0;
Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width;
return SizedBox(
height: MediaQuery.of(context).size.height -
MediaQueryData.fromWindow(window).padding.top -
kToolbarHeight,
child: Stack(
children: [
Positioned(
left: _position,
child: GestureDetector(
onHorizontalDragEnd: (details) {
double _halfWidth = _width * _currentIndex;
double _nextPoint = _currentIndex == 0
? (_width / 3)
: _halfWidth + (_width / 3);
double _beforePoint = (_width * _currentIndex) - (_width / 3);
if (-_position > _nextPoint) {
setState(() {
_position = -((_width * _currentIndex) + _width);
_currentIndex = _currentIndex + 1;
if (_currentIndex > widget.itemCount - 1) {
_position = -(_width * (_currentIndex - 1));
_currentIndex = widget.itemCount - 1;
}
});
} else if (-_position < _beforePoint) {
setState(() {
_position = -(_width * _currentIndex) + _width;
_currentIndex = _currentIndex - 1;
if (_currentIndex < 0) {
_position = 0.0;
_currentIndex = 0;
}
});
} else {
setState(() {
_position = -(_width * _currentIndex);
});
}
},
onHorizontalDragUpdate: (details) {
setState(() {
_position = _position + details.delta.dx;
});
},
child: Container(
width: _width * widget.itemCount,
height: MediaQuery.of(context).size.height -
MediaQueryData.fromWindow(window).padding.top -
kToolbarHeight,
decoration: const BoxDecoration(
image: DecorationImage(
colorFilter: ColorFilter.mode(
Colors.black, BlendMode.saturation),
fit: BoxFit.cover,
filterQuality: FilterQuality.high,
image: AssetImage(
"assets/images/city/panorama_city.jpeg"))),
child: Row(
children: [
...List.generate(
widget.itemCount,
(index) => SizedBox(
width: _width,
child: widget.items[index],
))
],
),
),
),
),
],
),
);
}
}
아직 미완성 상태이긴 하지만, Positioned 위젯을 애니메이션으로 처리해주거나, AnimationController를 사용해서 애니메이션을 이동시켜 주면 부드러운 상태로 만들 수 있을 것 같다.
가볍게 한 번씩 사용해보면 좋을 것 같아 글을 작성하였다.
이 방법 외에도 다양한 방법으로 구현할 수 있어 계속 수정을해 볼 예정이다.