사진에 다양한 필터를 적용해볼 수 있도록 filter carousel을 만들어보겠습니다.
먼저 Filter selector를 만들겠습니다.
class FilterSelector extends StatefulWidget {
const FilterSelector({
Key key,
}) : super(key: key);
_FilterSelectorState createState() => _FilterSelectorState();
}
class _FilterSelectorState extends State<FilterSelector> {
Widget build(BuildContext context) {
return SizedBox();
}
}
Stack 을 이용하여 FilterSelector가 사진의 상위에 오도록 하고, 화면에서 아래쪽에 위치시킵니다.
Stack(
children: [
Positioned.fill(
child: _buildPhotoWithFilter(),
),
Positioned(
left: 0.0,
right: 0.0,
bottom: 0.0,
child: FilterSelector(),
),
],
),
FilterSelector안에 slector ring을 만들어넣습니다.
class _FilterSelectorState extends State<FilterSelector> {
static const _filtersPerScreen = 5;
static const _viewportFractionPerItem = 1.0 / _filtersPerScreen;
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final itemSize = constraints.maxWidth * _viewportFractionPerItem;
return Stack(
alignment: Alignment.bottomCenter,
children: [
_buildShadowGradient(itemSize),
_buildSelectionRing(itemSize),
],
);
},
);
}
Widget _buildShadowGradient(double itemSize) {
return SizedBox(
height: itemSize * 2 + widget.padding.vertical,
child: const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
],
),
),
child: SizedBox.expand(),
),
);
}
Widget _buildSelectionRing(double itemSize) {
return IgnorePointer(
child: Padding(
padding: widget.padding,
child: SizedBox(
width: itemSize,
height: itemSize,
child: const DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(
BorderSide(width: 6.0, color: Colors.white),
),
),
),
),
),
);
}
}
carousel에 들어갈 item widget을 생성합니다.
class FilterItem extends StatelessWidget {
FilterItem({
Key key,
this.color,
this.onFilterSelected,
}) : super(key: key);
final Color color;
final VoidCallback? onFilterSelected;
Widget build(BuildContext context) {
return GestureDetector(
onTap: onFilterSelected,
child: AspectRatio(
aspectRatio: 1.0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ClipOval(
child: Image.network(
'https://flutter.dev/docs/cookbook/img-files'
'/effects/instagram-buttons/millenial-texture.jpg',
color: color.withOpacity(0.5),
colorBlendMode: BlendMode.hardLight,
),
),
),
),
);
}
}
pageView 를 이용해서 좌우로 스크롤 가능한 filter carousel을 생성합니다.
- pageViewController 의 viewportFraction 을 이용해서 각 filterItem widget들의 사이즈와 투명도를 조절합니다.
- AnimatedBuilder 사용하여 controller가 스크롤위치를 변화시킬 때마다 filterItem의 사이즈와 투명도를 알맞게 조절하도록합니다.
class _FilterSelectorState extends State<FilterSelector> {
final PageController _controller;
Color itemColor(int index) => widget.filters[index % widget.filters.length];
void initState() {
super.initState();
_controller = PageController(
viewportFraction: _viewportFractionPerItem,
);
_controller.addListener(_onPageChanged);
}
void _onPageChanged() {
final page = (_controller.page ?? 0).round();
widget.onFilterChanged(widget.filters[page]);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget _buildCarousel(double itemSize) {
return Container(
height: itemSize,
margin: widget.padding,
child: PageView.builder(
controller: _controller,
itemCount: widget.filters.length,
itemBuilder: (context, index) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
if (!_controller.hasClients ||
!_controller.position.hasContentDimensions) {
return SizedBox();
}
final selectedIndex = _controller.page!.roundToDouble();
final pageScrollAmount = _controller.page! - selectedIndex;
final maxScrollDistance = _filtersPerScreen / 2;
final pageDistanceFromSelected =
(selectedIndex - index + pageScrollAmount).abs();
final percentFromCenter =
1.0 - pageDistanceFromSelected / maxScrollDistance;
final itemScale = 0.5 + (percentFromCenter * 0.5);
final opacity = 0.25 + (percentFromCenter * 0.75);
return Transform.scale(
scale: itemScale,
child: Opacity(
opacity: opacity,
child: FilterItem(
color: itemColor(index),
onFilterSelected: () => _onFilterTapped,
),
),
);
},
),
);
},
),
);
}
void _onFilterTapped(int index) {
_controller.animateToPage(
index,
duration: const Duration(milliseconds: 450),
curve: Curves.ease,
);
}
}
mport 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show ViewportOffset;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
Widget build(BuildContext context) {
return MaterialApp(
home: ExampleInstagramFilterSelection(),
debugShowCheckedModeBanner: false,
);
}
}
class ExampleInstagramFilterSelection extends StatefulWidget {
const ExampleInstagramFilterSelection({Key key}) : super(key: key);
_ExampleInstagramFilterSelectionState createState() =>
_ExampleInstagramFilterSelectionState();
}
class _ExampleInstagramFilterSelectionState
extends State<ExampleInstagramFilterSelection> {
final _filters = [
Colors.white,
...List.generate(
Colors.primaries.length,
(index) => Colors.primaries[(index * 4) % Colors.primaries.length],
)
];
final _filterColor = ValueNotifier<Color>(Colors.white);
void _onFilterChanged(Color value) {
_filterColor.value = value;
}
Widget build(BuildContext context) {
return Material(
color: Colors.black,
child: Stack(
children: [
Positioned.fill(
child: _buildPhotoWithFilter(),
),
Positioned(
left: 0.0,
right: 0.0,
bottom: 0.0,
child: _buildFilterSelector(),
),
],
),
);
}
Widget _buildPhotoWithFilter() {
return ValueListenableBuilder(
valueListenable: _filterColor,
builder: (context, value, child) {
final color = value as Color;
return Image.network(
'https://picsum.photos/id/103/2592/1936.jpg',
color: color.withOpacity(0.5),
colorBlendMode: BlendMode.color,
fit: BoxFit.cover,
);
},
);
}
Widget _buildFilterSelector() {
return FilterSelector(
onFilterChanged: _onFilterChanged,
filters: _filters,
);
}
}
class FilterSelector extends StatefulWidget {
const FilterSelector({
Key key,
this.filters,
this.onFilterChanged,
this.padding = const EdgeInsets.symmetric(vertical: 24.0),
}) : super(key: key);
final List<Color> filters;
final void Function(Color selectedColor) onFilterChanged;
final EdgeInsets padding;
_FilterSelectorState createState() => _FilterSelectorState();
}
class _FilterSelectorState extends State<FilterSelector> {
static const _filtersPerScreen = 5;
static const _viewportFractionPerItem = 1.0 / _filtersPerScreen;
PageController _controller;
int _page;
int get filterCount => widget.filters.length;
Color itemColor(int index) => widget.filters[index % filterCount];
void initState() {
super.initState();
_page = 0;
_controller = PageController(
initialPage: _page,
viewportFraction: _viewportFractionPerItem,
);
_controller.addListener(_onPageChanged);
}
void _onPageChanged() {
final page = (_controller.page ?? 0).round();
if (page != _page) {
_page = page;
widget.onFilterChanged(widget.filters[page]);
}
}
void _onFilterTapped(int index) {
_controller.animateToPage(
index,
duration: const Duration(milliseconds: 450),
curve: Curves.ease,
);
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scrollable(
controller: _controller,
axisDirection: AxisDirection.right,
physics: PageScrollPhysics(),
viewportBuilder: (context, viewportOffset) {
return LayoutBuilder(
builder: (context, constraints) {
final itemSize = constraints.maxWidth * _viewportFractionPerItem;
viewportOffset
..applyViewportDimension(constraints.maxWidth)
..applyContentDimensions(0.0, itemSize * (filterCount - 1));
return Stack(
alignment: Alignment.bottomCenter,
children: [
_buildShadowGradient(itemSize),
_buildCarousel(
viewportOffset: viewportOffset,
itemSize: itemSize,
),
_buildSelectionRing(itemSize),
],
);
},
);
},
);
}
Widget _buildShadowGradient(double itemSize) {
return SizedBox(
height: itemSize * 2 + widget.padding.vertical,
child: const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
],
),
),
child: SizedBox.expand(),
),
);
}
Widget _buildCarousel({
ViewportOffset viewportOffset,
double itemSize,
}) {
return Container(
height: itemSize,
margin: widget.padding,
child: Flow(
delegate: CarouselFlowDelegate(
viewportOffset: viewportOffset,
filtersPerScreen: _filtersPerScreen,
),
children: [
for (var i = 0; i < filterCount; i++)
FilterItem(
onFilterSelected: () => _onFilterTapped(i),
color: itemColor(i),
),
],
),
);
}
Widget _buildSelectionRing(double itemSize) {
return IgnorePointer(
child: Padding(
padding: widget.padding,
child: SizedBox(
width: itemSize,
height: itemSize,
child: const DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.fromBorderSide(
BorderSide(width: 6.0, color: Colors.white),
),
),
),
),
),
);
}
}
class CarouselFlowDelegate extends FlowDelegate {
CarouselFlowDelegate({
this.viewportOffset,
this.filtersPerScreen,
}) : super(repaint: viewportOffset);
final ViewportOffset viewportOffset;
final int filtersPerScreen;
void paintChildren(FlowPaintingContext context) {
final count = context.childCount;
final size = context.size.width;
final itemExtent = size / filtersPerScreen;
final active = viewportOffset.pixels / itemExtent;
final min = math.max(0, active.floor() - 3).toInt();
final max = math.min(count - 1, active.ceil() + 3).toInt();
for (var index = min; index <= max; index++) {
final itemXFromCenter = itemExtent * index - viewportOffset.pixels;
final percentFromCenter = 1.0 - (itemXFromCenter / (size / 2)).abs();
final itemScale = 0.5 + (percentFromCenter * 0.5);
final opacity = 0.25 + (percentFromCenter * 0.75);
final itemTransform = Matrix4.identity()
..translate((size - itemExtent) / 2)
..translate(itemXFromCenter)
..translate(itemExtent / 2, itemExtent / 2)
..multiply(Matrix4.diagonal3Values(itemScale, itemScale, 1.0))
..translate(-itemExtent / 2, -itemExtent / 2);
context.paintChild(
index,
transform: itemTransform,
opacity: opacity,
);
}
}
bool shouldRepaint(covariant CarouselFlowDelegate oldDelegate) {
return oldDelegate.viewportOffset != viewportOffset;
}
}
class FilterItem extends StatelessWidget {
FilterItem({
Key key,
this.color,
this.onFilterSelected,
}) : super(key: key);
final Color color;
final VoidCallback onFilterSelected;
Widget build(BuildContext context) {
return GestureDetector(
onTap: onFilterSelected,
child: AspectRatio(
aspectRatio: 1.0,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: ClipOval(
child: Image.network(
'https://flutter.dev/docs/cookbook/img-files/effects/instagram-buttons/millenial-texture.jpg',
color: color.withOpacity(0.5),
colorBlendMode: BlendMode.hardLight,
),
),
),
),
);
}
}
This is interesting for me. I have always been interested in photo editing. True, I still only plan to learn this. But my photos are always beautiful. By clicking here you will understand everything yourself. For a long time I have been referring to this talented team of professionals. Most recently, experienced retouchers processed our wedding photos. I was just delighted with the result. Moreover, I was offered special packages and was able to save money.