이번 글에서는 Color Picker를 직접 만들어 보았다.
Flutter 라이브러리 중 Color Picker로 사용할 만한 것을 찾지 못해서 일단 가볍게 만들기로 했다. 원래는 다양한 Color Picker를 개발하려고 했는데, 시간이 너무 오래 소요되서 당장 급하게 필요한 것만 개발하게 되었다.
개발한 파일을 쉽게 가져다 사용할 수 있게 해주고 싶어서 상태 관리는 사용하지 않고 개발하려고 하다가, 우선 Provider를 사용해서 개발을 하였다.
소스 코드의 내용 중 UI 관련 부분은 필요한 부분만 설명할 예정이고, 로직 우선으로 설명을 하도록 하겠다.
공유된 Git 저장소에서 파일 가져다가 직접 사용해보면 이해하기가 더 수월합니다.
Flutter로 색상 선택기를 만들기 위해 필요한 부분이 색상을 선택할 수 있어야 하고, 각 색상마다 스와이프하여 세부 색상을 정할 수 있게 하고 싶었다.
개발한 내용은 간단하다. 각 색상에 세부 색상을 100개의 아이템으로 한정하였고, 100개의 아이템을 각 디바이스의 가로 폭에 맞춰 수평으로 배치한 다음 선택기를 Stack으로 쌓아 Positioned 위젯의 left 포지션을 이동시켜 주면 된다.
해당 포지션을 이동 시키는 방법은 GestureDector의 onHorizontalDragUpdate를 사용하여 원하는 포지션으로 이동시켜 주면 된다.
상태 관리로 Provider를 사용하고 있어서, Provider만 추가해주자.
dependencies:
provider: ^6.0.4
ColorPickerModel을 하나 만들어보자. Color와 인덱스 값을 가지는 모델을 사용해서 리스트로 사용할 예정이다.
class ColorPickerModel {
final int index;
final Color color;
const ColorPickerModel({
required this.index,
required this.color,
});
ColorPickerModel copyWith({
required int? index,
required Color? color,
}) {
return ColorPickerModel(
index: index ?? this.index,
color: color ?? this.color,
);
}
}
추가적으로 ColorType이라는 enum 객체를 생성해서, 각 색상의 기본 RGB 컬러를 리스트로 넣어두자.
enum ColorType {
red([244, 67, 54]),
pink([233, 30, 99]),
deepOrange([255, 87, 34]),
orange([255, 152, 0]),
amber([255, 193, 7]),
yellow([255, 235, 59]),
lightGreen([139, 195, 74]),
green([76, 175, 80]),
teal([0, 150, 136]),
cyan([0, 188, 212]),
lightBlue([3, 169, 244]),
blue([33, 150, 243]),
navy([33, 64, 243]),
purple([156, 39, 176]),
deepPurple([103, 58, 183]),
white([206, 206, 206]),
grey([100, 100, 100]),
black([49, 49, 49]);
final List<int> rgb;
const ColorType(this.rgb);
}
UI는 크게 2가지 부분만 짚고 넘어갈 예정이다.
먼저 아래와 같이 Stack 안에 Wrap을 사용해서 ColorPickerModel 100개를 담고 있는 리스트를 사용해서 수평으로 배치시키고, 그 위로 AnimatedPositioned 위젯을 쌓아 선택기가 수평으로 이동될 수 있도록 해줄거다.
여기서 onHorizontalDragStart, onHorizontalDragUpdate 기능은 Provider에서 작업할 내용이며, 선택기를 움직이는 중요한 부분이기 때문에 잘 기억해두자.
Stack(
children: [
Center(
child: Wrap(
children: [
...List.generate(100, (index) => GestureDetector(
onTap: () {
state.pickerColorTap(
state.pickerColors[index]);
},
child: Container(
width: ((MediaQueryData.fromWindow(window)
.size
.width -
60) /
100),
height: 40,
decoration: BoxDecoration(
color: state.pickerColors[index].color,
),
),
),
),
],
),
),
AnimatedPositioned(
duration: Duration(milliseconds: state.duration),
left: state.currentPosition,
child: GestureDetector(
onHorizontalDragStart: state.dragStart,
onHorizontalDragUpdate: state.dragUpdate,
child: Container(
width: 50,
height: 55,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(55),
color: state.currentColor,
border: Border.all(
color: Colors.white70, width: 4)),
),
),
),
],
)
해당 부분은 위에서 작업한 선택기 아래에 배치되는 부분으로, ColorType을 선택하여 Color 값을 변경해주는 부분이다.
Wrap(
spacing: 10,
runSpacing: 20,
children: [
...List.generate(state.colorType.length, (index) => GestureDetector(
onTap: () {
HapticFeedback.mediumImpact();
state.colorChanged(
state.colorType[index], index);
},
child: Container(
width: MediaQueryData.fromWindow(window)
.size
.width /
6 -
(90 / 5.99999),
height: MediaQueryData.fromWindow(window)
.size
.width /
6 -
(90 / 5.99999),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(50),
border: Border.all(
color: state.currentIndex == index
? Colors.white
: Colors.white30,
width: state.currentIndex == index
? 3
: 2),
color: Color.fromRGBO(
state.colorType[index].rgb[0],
state.colorType[index].rgb[1],
state.colorType[index].rgb[2],
1),
),
),
)),
],
),
로직을 만들어보자. Provider를 사용해서 개발을 하였다. ChangeNotifier를 상속 받아 Provider 기능을 사용해 보자.
아래 변수가 Color Picker를 만드는데, 사용되는 변수이다. 여기서 ColorType 리스트를 보면 우리가 모델에서 생성해준 enum을 전부 담아주는 부분이다.
class ColorPickerProvider extends ChangeNotifier {
List<ColorPickerModel> pickerColors = [];
double currentPosition = 0;
int duration = 0;
late Color currentColor;
int currentIndex = 0;
List<ColorType> colorType = List.generate(
ColorType.values.length, (index) => ColorType.values[index]);
...
}
각 색상의 선택기에 수평으로 배치될 세부 색상 데이터를 세팅해주는 부분이다. 총 100개의 아이템을 리스트에 담아야 하는데, 메인 색상의 좌측으로 메인 색상보다 밝은 색상을 배치하고, 우측 편으로는 점점 어두워지는 색상을 배치시켜야 한다.
void _colorState(ColorType type) {
pickerColors = List.generate(
50,
(index) => ColorPickerModel(
index: (index - 49).abs(),
color: Color.fromRGBO(
(type.rgb[0] + index > 255) ? 255 : type.rgb[0] + index,
(type.rgb[1] + index > 255) ? 255 : type.rgb[1] + index,
(type.rgb[2] + index > 255) ? 255 : type.rgb[2] + index,
1),
));
pickerColors = pickerColors.reversed.toList();
pickerColors.addAll([
...List.generate(
50,
(index) => ColorPickerModel(
index: 50 + index,
color: Color.fromRGBO(
(type.rgb[0] - index < 0) ? 0 : type.rgb[0] - index,
(type.rgb[1] - index < 0) ? 0 : type.rgb[1] - index,
(type.rgb[2] - index < 0) ? 0 : type.rgb[2] - index,
1,
),
))
]);
}
메인 색상이 변경될 때마다 해당 포지션을 이동시키기 위한 로직이다.
void colorChanged(ColorType type, int index) {
_colorState(type);
currentIndex = index;
currentColor = pickerColors[49].color;
currentPosition =
((MediaQueryData.fromWindow(window).size.width - 40) / 100) * 50;
notifyListeners();
}
UI 부분에서 잘 기억하라고 했던 onHorizontalDragStart 부분의 로직이다. 여기서는 단순히 AnimatedPositioned 위젯의 duration 값을 0으로 변경해주는 작업만 진행하면 된다.
duration 값이 있으면, 스와이프로 움직일 때에 에니메이션 효과가 들어가면서 움직임이 조금 어색해진다.
void dragStart(DragStartDetails? details) {
HapticFeedback.mediumImpact();
duration = 0;
notifyListeners();
}
가장 핵심인 onHorizontalDragUpdate 기능의 로직이다. DragUpdateDetails에 delta.dx 값이 x 축으로 이동되는 값을 제공하는데, 해당 값을 사용하여 현재의 currentPosition 변수 값에 이동되는 x 값만큼 더해 주면 된다.
currentPosition 값이 선택기 전체 Wrap 사이즈보다 작거나 크면 위젯을 벗어나기 때문에 해당 포지션의 최소/최대 영역을 벗어나지 못하게 해주었다.
색상 선택기가 움직이는 현재의 포지션에 맞는 pickerColors 리스트를 currentColor로 넣어주면 된다.
void dragUpdate(DragUpdateDetails? details) {
HapticFeedback.lightImpact();
double _dx = details!.delta.dx;
double _itemWidth = (MediaQueryData.fromWindow(window).size.width) - 40;
currentPosition = (currentPosition + _dx < 10) ||
(currentPosition + _dx > _itemWidth - 20)
? currentPosition
: currentPosition + _dx;
currentColor = pickerColors[currentPosition ~/ (_itemWidth / 100)].color;
notifyListeners();
}
추가적으로 색상 선택기의 원하는 영역을 터치하여 색상을 선택할 수 있는 기능도 만들었다. 여기서 보면 onHorizontalDragStart가 시작될 때에 duration을 0으로 변경한 것처럼 Tap 했을 때는 200으로 변경시켜 자연스럽게 포지션이 이동될 수 있도록 해주면 된다.
void pickerColorTap(ColorPickerModel data) {
HapticFeedback.mediumImpact();
duration = 200;
double _width = (MediaQueryData.fromWindow(window).size.width - 40);
currentColor = data.color;
currentPosition = (_width / 100) * data.index;
currentPosition = currentPosition < 20
? 10
: currentPosition > (_width - 20)
? (_width - 20)
: currentPosition;
notifyListeners();
}
https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/app/color_picker
이렇게 Color Picker를 개발해 봤다. 세부 색상은 제가 임의로 그냥 넣은 색상이라 원하는 세부 색상을 리스트에 넣어서 사용하시면 된다.
간단하게 만들어봤기에, 실제로 프로덕션에서 사용하기 위해서는 좀 더 다듬어야 하는데, 그 부분까지 공유하기에는 조금 어려움이 있어 이해하기 바란다.
어렵거나 이해안되시는 부분은 댓글 남겨주시면 답변하도록 하겠습니다.