Нестандартное оформление кнопок, текстовых полей и других элементов управления Flutter

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Intro


Иногда при внедрении интерфейса недостаточно тех возможностей кастомизации, которые предоставляет Flutter. Подтверждением этому является большое количество вопросов на Stackoverflow, типа, как добавить тень или градиент к какому-нибудь элементу управления (кнопке, текстовому полю и т.д.). Как правило, ответы сводятся к тому, что надо либо использовать элементы управления из сторонних библиотек, либо обернуть элемент управления в Container c необходимым декорированием, либо создать собственный элемент управления. Однако, эти подходы имеют ограничения или требуют много кода. Особенно добавляет работы настройка различного декорирования элементов управления для различных их состояний и анимирование переходов между этими состояниями. В статье я расскажу, как расширить возможности кастомизации этих элементов без создания новых виджетов и без сторонних библиотек.


Если мы посмотрим на настройки стандартных элементов управления Flutter, то заметим, что многие из них имеют параметр shape. Он и предоставит нам возможность нестандартной кастомизации. Этот же параметр shape можно изменять и через настройки темы приложения. Различные отображения элемента управления для разных его состояний и анимация перехода между этими состояниями настраиваются стандартным для элементов управления способом.


В качестве примера я покажу как декорировать кнопку. Но этот же способ подойдёт большинству других элементов управления Flutter. Так как параметр shape для многих элементов управления имеет одинаковый тип, почти всегда достаточно просто перенести значение его настройки из одного элемента управления (например, кнопки) в другой (например, Chip), чтобы добиться схожего отображения.


Для примера предлагаю изменить форму кнопки и добавить градиентную заливку. В результате должна выглядеть вот так:


Button example


Весь исходный код приложения доступен на github.


Готовое веб-приложение можно посмотреть в dartpad.


Сначала создадим приложение, которое отображает лишь стандартную кнопку.


Выполним команду:


flutter create fl_decor_example

Заменим содержимое файла main.dart на следующее:


import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
          body: Center(
        child: TextButton(
          child: const Text("Text Button"),
          onPressed: () {},
        ),
      )),
    );
  }
}

Создадим файл shape_decoration.dart, в который поместим класс-заготовку для стилизации элемента.


import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'dart:ui';

class MyShapeDecoration extends OutlinedBorder {
  const MyShapeDecoration({
    required this.borderGradien,
    required this.borderWidth,
  }) : super();

  final Gradient borderGradien;
  final double borderWidth;
  final double bevel = 8.0; // Величина наклона кнопки
}

В свойства только что созданного класса мы добавили параметры, с помощью которых можно влиять на отображение элемента управления.


Поменяем настройки темы в файле main.dart так, чтобы стилизировать кнопку только что созданным классом.


      ...
      theme: ThemeData(
        primarySwatch: Colors.blue,
        textButtonTheme: TextButtonThemeData(
            style: ButtonStyle(
          padding: MaterialStateProperty.all(const EdgeInsets.symmetric(horizontal: 16)),
          animationDuration: const Duration(milliseconds: 1500),
          shape: MaterialStateProperty.resolveWith((states) {
            if (states.contains(MaterialState.pressed)) {
              return const MyShapeDecoration(
                borderGradien: LinearGradient(colors: [Colors.red, Colors.red]),
                borderWidth: 6,
              );
            }

            return const MyShapeDecoration(
              borderGradien: LinearGradient(colors: [Colors.purple, Colors.green, Colors.yellow]),
              borderWidth: 2,
            );
          }),
        )),
      ),
      ...

Далее будем работать только с классом MyShapeDecoration.


В класс добавим функцию demensions. Эта функция должна вернуть отступы от краёв элемента, за которые запрещается выходить внутреннему содержимому элемента.


Button dimensions


  @override
  EdgeInsetsGeometry get dimensions {
    return EdgeInsets.symmetric(vertical: borderWidth, horizontal: bevel / 2 + borderWidth);
  }

Далее добавим функции lerpFrom и lerpTo. Они нужны для того, чтобы формировать копии класса во время переходных состояний. Параметр t принимает значения от 0 до 1 и обозначяет текущее положение анимации перехода между двумя состояниями.


  @override
  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
    if (a is MyShapeDecoration) {
      return MyShapeDecoration(
        borderGradien: Gradient.lerp(a.borderGradien, borderGradien, t)!,
        borderWidth: lerpDouble(a.borderWidth, borderWidth, t)!,
      );
    }
    return super.lerpFrom(a, t);
  }

  @override
  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
    if (b is MyShapeDecoration) {
      return MyShapeDecoration(
        borderGradien: Gradient.lerp(borderGradien, b.borderGradien, t)!,
        borderWidth: lerpDouble(borderWidth, b.borderWidth, t)!,
      );
    }
    return super.lerpFrom(b, t);
  }

Добавим функцию copyWith для копирования объекта класса с модифицированными параметрами.


  @override
  OutlinedBorder copyWith({Gradient? borderGradien, double? borderWidth, BorderSide? side}) {
    return MyShapeDecoration(
      borderGradien: borderGradien ?? this.borderGradien,
      borderWidth: borderWidth ?? this.borderWidth,
    );
  }

Функция scale должна возвращать копию объекта класса с учётом масштабирования элемента управления на множитель t.


  @override
  ShapeBorder scale(double t) {
    return MyShapeDecoration(
      borderGradien: borderGradien.scale(t),
      borderWidth: borderWidth * t,
    );
  }

Добавим функции getInnerPath и getOuterPath. В идеале они должны возвращать пути внешней и внутренней границ элементов.


Button dimensions


Но в нашем случае, для простоты, можно реализовать только одну из этих функций.


  @override
  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
    final path = Path();
    path.moveTo(rect.left + bevel, rect.top);
    path.lineTo(rect.right, rect.top);
    path.lineTo(rect.right - bevel, rect.bottom);
    path.lineTo(rect.left, rect.bottom);
    path.close();
    return path;
  }

  @override
  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
    return getInnerPath(rect, textDirection: textDirection);
  }

В процедуре paint рисуем на canvas необходимое нам декорирование элемента.


  @override
  void paint(Canvas canvas, Rect rect, {double? gapStart, double gapExtent = 0.0, double gapPercentage = 0.0, TextDirection? textDirection}) {
    final shader = borderGradien.createShader(rect);

    final paint = Paint()
      ..shader = shader
      ..strokeWidth = borderWidth
      ..style = PaintingStyle.stroke;

    final innerPath = getInnerPath(rect, textDirection: textDirection);
    canvas.drawPath(innerPath, paint);
  }

Осталось внедрить функции сравнения объектов класса, чтобы Flutter мог определить, что объект изменился и нужно заново его нарисовать.


  @override
  bool operator ==(Object other) {
    if (other.runtimeType != runtimeType) return false;
    return other is MyShapeDecoration && other.side == side && other.borderWidth == borderWidth && other.borderGradien == borderGradien;
  }

  @override
  int get hashCode => hashValues(side, borderWidth, borderGradien);

По желанию можно реализовать функцию преобразования объекта в строку.


  @override
  String toString() {
    return '${objectRuntimeType(this, 'MyShapeDecoration')}($side, $borderWidth, $borderGradien)';
  }

На этом всё! Полный код класса MyShapeDecoration доступен по ссылке.


Теперь можем запускать приложение. Форма кнопки изменилась, и при нажатии у неё меняется граница. Вы можете самостоятельно попробовать применить только что созданный класс для других элементов управления, например, для Chip.


Chip example


Недостатком вышеописанного способа является то, что декорирование кнопки происходит поверх самой кнопки. Поэтому при использовании непрозрачных заливок, текст кнопки будет перекрываться. Такое ограничение Flutter есть для кнопки, но отсутствует, например, для TextField.


Заключение


Мы научились изменять внешний вид стандартных элементов управления Flutter на примере кнопки.


Если вам не нужна сложная кастомизация, а достаточно лишь изменения тени, добавления свечения или изменения фона, вы можете воспользоваться моим плагином control_style. На странице плагина есть ссылка на web-приложение, чтобы "поиграть" с разными вариантами декорирования элементов управления.

Источник: https://habr.com/ru/post/666052/


Интересные статьи

Интересные статьи

Всем добрый денек! Надеюсь после первых трех статей, эта вам покажется не менее полезной.Сегодня я постараюсь простым языком объяснить MVC паттерн.И конечно же покажу все...
В продолжение статьи о классическом PRINCE2 по запросу из комментариев попробовала сравнить ключевые методики управления проектами. Надеюсь, что получилось что-то полезно...
Возможность интеграции с «1С» — это ключевое преимущество «1С-Битрикс» для всех, кто профессионально занимается продажами в интернете, особенно для масштабных интернет-магазинов.
Недавно попалась на глаза новость, что вышел очередной релиз Flutter (1.9), который обещает разные вкусности и, в том числе, раннюю поддержку веб-приложений. На работе я занимаюсь разработкой ...
«Битрикс» — кошмар на костылях. Эта популярная характеристика системы среди разработчиков и продвиженцев ныне утратила свою актуальность.