Создаем параллакс-эффект во Flutter с CustomPaint

от автора

В современном мире мобильной разработки, где внимание к деталям и уникальность интерфейса играют ключевую роль, параллакс-эффект открывает новые горизонты для вовлечения пользователей. Существует много разных способов его реализации, например, через 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; }

Краткое пояснение:

  1. Передаем scrollController в поле repaint родительского super конструктора, чтобы customPainter реагировал на скролл

  2. aspectRatio — соотношения сторон картинки. Оно нужно для того, чтобы картинка не получалась растянутой или сдавленной.

  3. Используем метод canvas.drawImageRect, работу которого описал в статье «Как реализовать обрезку изображений во flutter без сторонних библиотек»

    • Напомню, что src — часть изображения, которую мы хотим отобразить, и здесь следует указывать реальные размеры картинки

    • dst — прямоугольник, в котором мы хотим нарисовать вырезанную из src часть

  4. При данной реализации картинка отрисовывается полностью и выходит за пределы видимой области экрана по высоте.
    *Здесь при желании можно оптимизировать , чтобы не держать в памяти часть, которая не отображается на экране

  5. Когда происходит скролл, картинка смещается по оси 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *