В современном мире мобильной разработки, где внимание к деталям и уникальность интерфейса играют ключевую роль, параллакс-эффект открывает новые горизонты для вовлечения пользователей. Существует много разных способов его реализации, например, через Flow или PageView. В этой статье мы раскроем, как с помощью CustomPaint оживить приложение на Flutter, добавив эффект параллакса.

Параллакс-эффект, как художественный прием в дизайне, создает иллюзию глубины и движения. Это достигается путем различной скорости движения слоев изображения, придавая динамизм и глубину визуальным элементам. Например, фон может двигаться медленнее передних элементов, создавая впечатление многомерности.
Задача
Допустим, поступила задача сверстать экранчик, и креативный дизайнер придумал для этого длинную вертикальную картинку на бэкграунде и какое-то определенное количество элементов на переднем плане. Картинка должна смещаться медленнее относительно основного контента, создавая эффект параллакса.
Для простоты реализации пусть это будут одинаковые элементы представляющие собой полупрозрачные карточки с рандомными изображениями.
Шаги
-
Виджет карточки с изображением. Для получения приятных на глаз изображений воспользуемся сервисом https://picsum.photos/:
import 'package:flutter/material.dart'; class ItemCard extends StatelessWidget { const ItemCard({ super.key, required this.id, }); final int id; @override Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric( horizontal: 20, vertical: 20, ), clipBehavior: Clip.hardEdge, width: double.maxFinite, height: 300, decoration: BoxDecoration( color: Colors.grey.withOpacity(0.4), borderRadius: BorderRadius.circular(20), boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 10, spreadRadius: 5, ), ], ), child: Stack( children: [ Image.network( 'https://picsum.photos/id/$id/500/300', width: double.maxFinite, ), Positioned( left: 20, bottom: 20, child: Text( 'Image $id', style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold, ), ), ), ], ), ); } }
-
Реализация экрана с скроллящимся списком. id картинок подбираем рандомно
import 'dart:math' show Random; import 'package:flutter/material.dart'; class ParallaxScreen extends StatefulWidget { const ParallaxScreen({super.key}); @override State<ParallaxScreen> createState() => _ParallaxScreenState(); } class _ParallaxScreenState extends State<ParallaxScreen> { late final List<int> ids = List.generate(10, (index) => Random().nextInt(500)); @override Widget build(BuildContext context) { return Scaffold( body: ListView.builder( itemCount: ids.length, itemBuilder: (context, index) { final int id = ids[index]; return ItemCard(id: id); }, ), ); } }

-
Бэкграунд реализуем через
CustomPainter, так как с его помощью можно относительно легко проводить различные манипуляции с картинкойui.Image. Для отслеживания скроллинга в списке будем использоватьScrollController
import 'dart:ui' as ui; import 'package:flutter/material.dart'; class _BackgroundImagePainter extends CustomPainter { final ScrollController controller; final ui.Image image; const _BackgroundImagePainter(this.controller, this.image) : super(repaint: controller); @override void paint(Canvas canvas, Size size) { final imageWidth = image.width.toDouble(); final imageHeight = image.height.toDouble(); final aspectRatio = imageWidth / imageHeight; final src = Rect.fromLTWH( 0, 0, imageWidth, imageHeight, ); final deltaY = -controller.offset * 0.6; final dst = Rect.fromLTWH( 0, deltaY, size.width, size.width / aspectRatio, ); canvas.drawImageRect( image, src, dst, Paint()..filterQuality = FilterQuality.high, ); } @override bool shouldRepaint(_BackgroundImagePainter oldDelegate) => controller.offset != oldDelegate.controller.offset; }
Краткое пояснение:
-
Передаем
scrollControllerв полеrepaintродительскогоsuperконструктора, чтобыcustomPainterреагировал на скролл -
aspectRatio— соотношения сторон картинки. Оно нужно для того, чтобы картинка не получалась растянутой или сдавленной. -
Используем метод canvas.drawImageRect, работу которого описал в статье «Как реализовать обрезку изображений во flutter без сторонних библиотек»
-
Напомню, что
src— часть изображения, которую мы хотим отобразить, и здесь следует указывать реальные размеры картинки -
dst— прямоугольник, в котором мы хотим нарисовать вырезанную изsrcчасть
-
-
При данной реализации картинка отрисовывается полностью и выходит за пределы видимой области экрана по высоте.
*Здесь при желании можно оптимизировать , чтобы не держать в памяти часть, которая не отображается на экране -
Когда происходит скролл, картинка смещается по оси Y на значение
deltaY, равное-controller.offset * 0.6, где 0.6, это коэффициент, за счет которого достигается эффект параллакса
-
Сам
_Backgroundвиджет:
import 'dart:ui' as ui; import 'package:flutter/material.dart'; class _Background extends StatefulWidget { const _Background({ required this.child, required this.scrollController, }); final Widget child; final ScrollController scrollController; @override State<_Background> createState() => _BackgroundState(); } class _BackgroundState extends State<_Background> { @override void initState() { super.initState(); _loadImage(); } ui.Image? _image; Future<void> _loadImage() async { const imageProvider = NetworkImage('https://picsum.photos/id/307/600/4000'); final ImageStreamListener listener = ImageStreamListener((info, _) { setState(() { _image = info.image; }); }); final ImageStream stream = imageProvider.resolve(const ImageConfiguration()); stream.addListener(listener); } @override void dispose() { _image?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return CustomPaint( painter: _image != null ? _BackgroundImagePainter(widget.scrollController, _image!) : null, child: widget.child, ); } }
Краткие пояснения:
-
Для загрузки изображения в методе
_loadImageиспользуетсяImageProvider, реализованный черезNetworkImage. Если, например, нужно получить картинку из другого источника, то есть дефолтныеAssetImageиFileImage -
CustomPaintпринимает размерыchild, поэтомуsizeуказывать не нужно
Конечный код реализованного экрана
class ParallaxScreen extends StatefulWidget { const ParallaxScreen({super.key}); @override State<ParallaxScreen> createState() => _ParallaxScreenState(); } class _ParallaxScreenState extends State<ParallaxScreen> { late final List<int> ids = List.generate(10, (index) => Random().nextInt(500)); final ScrollController _scrollController = ScrollController(); @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: _Background( scrollController: _scrollController, child: ListView.builder( controller: _scrollController, itemCount: ids.length, itemBuilder: (context, index) { final int id = ids[index]; return ItemCard(id: id); }, ), ), ); } }
Заключение
Сегодня мы изучили, как с небольшими усилиями можно добавить эффект параллакса в ваше Flutter-приложение.
Напишите в комментариях, приходилось ли вам добавлять такой эффект в своем проекте и какими инструментами вы пользовались.
ссылка на оригинал статьи https://habr.com/ru/articles/794004/
Добавить комментарий