С выходом официальной версии Flutter 3.3 глобальное управление выбором текста наконец получило официальную поддержку. Эта функция исправляет долгое время существующие проблемы с выбором текста в Flutter, особенно на Flutter Web, где часто наблюдается отсутствие совпадения поведения при выборе текста с ожидаемым.
Использование SelectionArea
также очень простое, как показано ниже, достаточно добавить SelectionArea
там, где вы хотите поддерживать эту функцию. Вы даже можете добавить SelectionArea
в каждом Scaffold
маршруте, чтобы полностью активировать поддержку.
По умолчанию SelectionArea
уже реализует все основные функции, а Flutter адаптировал его под различные платформы, что приводит к различному внешнему виду на Android и iOS.
![]() |
![]() |
![]() |
---|
Конечно, возможно вы заметите, что на iOS нет кнопки "Выбрать всё" в панели инструментов, это связано с тем, что iOS использует значение canSelectAll
по умолчанию в TextSelectionControls
. В этом значении есть условие, которое требует, чтобы start == end
.
Если вам кажется, что это условие неверное, вы всегда можете переопределить свой собственный TextSelectionControls
, например, вернуть true
в методе canSelectAll
.
![]() |
![]() |
---|
Да, мы можем настроить SelectionArea
, расширяя возможности TextSelectionControls
:
buildToolbar
можно настраивать стиль и логику выпадающего меню, а также добавлять дополнительные возможности, такие как "вставка изображения".buildHandle
можно настраивать внешний вид области выбора текста, которую можно перемещать внутри SelectionArea
. Для Handle
и Toolbar
стиль реализуется путём добавления нового Overlay
. Логика этой части находится в объекте SelectionOverlay
:![]() |
![]() |
---|
Если вы ещё не знакомы с
Overlay
, его можно просто представить следующим образом: по умолчанию все маршруты находятся внутри одногоOverlay
, открытие маршрута — это добавлениеOverlayEntry
вOverlay
.
Поэтому Handle
и Toolbar
открываются как специальные "маршрутные" компоненты со своими уровнями, как показано справа нижним изображением, которое представляет собой OverlayEntry
для Toolbar
.| |
|
| -------------------------------------------------------- | -------------------------------------------------------- |
Кроме того, цвет Handle
по умолчанию обычно определяется TextSelectionTheme
и Theme
.
Например, в MaterialTextSelectionControls
, цвет Handle
для начала и конца по умолчанию определяется через selectionHandleColor
в TextSelectionTheme
или через primary
в Theme
.
А где берётся цвет области выбора текста? Это тоже OverlayEntry
?
Ответ отрицательный, этот цвет главным образом определяется при рендере канваса при рисовании текста.
Как показано ниже, при рисовании текста проверяется наличие выбранной части текста, если она существует, то вызывается рисование соответствующего слоя выбора.
Что касается цвета выбранных областей текста по умолчанию, он определяется через selectionColor
в DefaultSelectionStyle
. Конечно, как показано справа нижним изображением, в MaterialApp
этот цвет всё ещё связан с selectionColor
в TextSelectionTheme
или с primary
в Theme
.
![]() |
![]() |
---|
А что если вы хотите запретить выбор некоторых элементов внутри SelectionArea
?
Для этого Flutter предоставляет реализацию SelectionContainer.disabled
, которая позволяет запрещать выбор текста в определённой области.| |
|
| -------------------------------------------------------- | -------------------------------------------------------- |
Почему вложенный SelectionContainer.disabled
может отключить возможность выбора текста? Это связано с тем, как работает SelectionArea
.
SelectionContainer
внутренне используетInheritedWidget
, который делится между потомкамиSelectionRegistrar
. По умолчаниюSelectionArea
используетSelectionContainer
и делится соответствующимRegistrar
.
SelectionArea
SelectionContainer
имеет соответствующий registrar
, который делится между потомками.SelectionContainer.disabled
registrar
является null
.Основное различие заключается в том, что в SelectionContainer.disabled
нет registrar
, как показано слева на следующей картинке, после применения disabled
получаемый registrar
становится null
. Поэтому, как показано справа в следующем коде, логика обновления области выбора будет немедленно завершена.
![]() |
![]() |
---|
На данном этапе вы должны иметь общее представление о том, как использовать и настраивать возможности SelectionArea
. Далее мы рассмотрим два "бага", которые помогут более глубоко понять внутреннюю работу SelectionArea
.
Как показано ниже, после использования WidgetSpan
по умолчанию пользователи не смогут выбрать текст внутри WidgetSpan
, начиная выбор с хэндлера.| |
|
| -------------------------------------------------------- | -------------------------------------------------------- |
PS: На самом деле можно выделить объекты при помощи перетаскивания, просто в данном случае мы временно рассматриваем ситуацию, когда выделение невозможно.
Почему так происходит? В первую очередь стоит понять, что после использования WidgetSpan
, обернувшего Hello World
, фактически создаются два Text
, то есть вышеупомянутый интерфейс состоит из двух RenderParagraph
.
Для внешнего Text
его содержание на самом деле равно "Flutter is the best!"
. Обратите внимание на это содержимое — здесь появились лишние два пробела.
Эти два пробела появились потому, что WidgetSpan
использует заполнитель с кодом 0xFFFC
. Этот заполнитель заменяется на "Hello World"
и картинку с кошачьей головой во время отрисовки.
Поэтому если мы попробуем скопировать этот текст, то получим "Flutter isthe best!", где два заполняющих символа не будут скопированы, поскольку они исключаются при получении доступных для выбора фрагментов.
Кроме того, при нажатии на кнопку "Скопировать", сам Hello World
внутри WidgetSpan
не будет выбран, поэтому вызов getSelectedContent
вернёт null
, то есть пустую строку.
Итак, можно заметить, что при ручной выборке с помощью перетаскивания текст внутри WidgetSpan
не будет выделен, поскольку он находится в разных Text
. Для внешнего Text
он является лишь заполнителем.
Конечно, на самом деле при перетаскивании можно выбрать текст внутри
WidgetSpan
, например, начиная перетаскивать сHello World
. Причины, почему это не работает в этом месте, будут объяснены ниже.
Что произойдет, если мы щелкнем на "Выбрать всё"? Как показано на следующем рисунке, после нажатия на "Выбрать всё" возникают две странные проблемы:
Hello World
внутри WidgetSpan
становится доступным для выделенияStart Handle
) начинается не с самого начала текста, а с начала WidgetSpan
Рассмотрим сначала первый вопрос: почему при выборе всего текста Hello World
внутри WidgetSpan
становится доступным для выделения?
Основное отличие полного выделения от перетаскивания заключается в том, что первое отправляет событие "Выбрать всё" (SelectAllSelectionEvent
), которое активирует все события для всех потомков, включая Hello World
внутри WidgetSpan
.
Последний объект, отвечающий за реакцию на событие SelectAll, — это _SelectableFragment
. Здесь присутствуют два ключевых логических блока:- _handleSelectAll
получает значения _textSelectionStart
и _textSelectionEnd
, что указывает на то, что в данный момент компонент уже выбран;
didChangeSelection
вызывает перерисовку через paragraph.markNeedsPaint()
, а затем добавляет цветовое оформление при выборе.Как видно, поскольку "Hello World"
внутри WidgetSpan
также реагирует на событие SelectAll
, он будет находиться в состоянии выбора, поэтому его содержимое можно получить с помощью метода getSelectedContent
.
Однако, скопированное содержимое будет выглядеть как "Hello World!Flutter isthe best!"
, что кажется некорректным. Это второй вопрос, который мы хотели затронуть: положение начального хэндла слева не совпадает с началом текста.
Сначала рассмотрим, почему скопированное содержимое выглядит как "Hello World!Flutter isthe best!"
.
Как было упомянуто ранее, метод getSelectedContent
используется для копирования. В данном случае, первым элементом списка selectables
является "Hello World"
, поэтому конечный текст будет выглядеть как "Hello World!Flutter isthe best!"
.
А почему "Hello World"
находится на первом месте в списке selectables
? Для этого следует обратиться к логике сортировки selectable в Flutter.Известно, что Text
реализует текстовое представление с помощью RenderParagraph
. При инициализации RenderParagraph
, если существует _registrar
, то есть если существует SelectionArea
, то поддерживаемые для выбора фрагменты добавляются внутрь _additions
через метод add
.
Затем SelectionArea
выполняет сортировку выбранных элементов. Как показывают следующие строки кода, перед сортировкой "Hello World"
находится в конце списка _additions
, так как он является потомком WidgetSpan
.
После выполнения команды
sort
, можно заметить, что "Hello World" переместилось в начало списка. Это объясняет, почему при копировании содержимое отображается сначала как "Hello World", а затем следует "Start Handle".
Логика команды sort
реализуется через метод compareOrder
. Анализируя реализацию метода compareOrder
, можно выделить логику _compareVertically
. По результатам отладки, можно заметить, что "Hello World" находится выше всех остальных текстовых элементов благодаря своему положению в Rect
(top), поэтому он считается более высокого приоритета, что приводит к тому, что его воспринимают как находящийся на следующей строке.
Зная проблему, её легко исправить. Как показано ниже, если скорректировать высоту WidgetSpan
, то "Start Handle" будет работать корректно, но... "End Handle" окажется неправильно позиционирован.
| |
|
| ------------------------------------------------------------ | ------------------------------------------------------------ |Теперь скопированное содержимое будет выглядеть так:
Flutter isthe best!Hello World!
, так как теперь существует некая "тонкая" ошибка, которая приводит к тому, что "Hello World" располагается в конце списка при сортировке, что приводит к неверному положению "End Handle".
Кроме того, вы заметите, что, как показано слева в анимации, при перемещении "Handle" можно выбрать "Hello World" внутри "WidgetSpan". Это возможно даже в предыдущих условиях, хотя требовалось начинать выборку именно с "Hello World", как показано справа в анимации, поскольку в самом начале "Hello World" имел более высокий уровень приоритета среди "selectables", поэтому для выборки также требовалось начинать именно с него.
![]() |
![]() |
---|
В настоящее время эта проблема может быть воспроизведена как в ветке master, так и в ветке stable. Соответствующее сообщение об ошибке я отправил в #111021.
Хотя появление SelectionArea
закрыло одну из долгих слабых сторон Flutter, реализация на основе SelectionArea
имеет определенную сложность, поэтому в настоящее время есть ещё много деталей, требующих оптимизации. Однако всё начинается с первого шага, и внедрение SelectionArea
в версии 3.3 можно считать хорошим началом.Наконец, надеюсь, что после прочтения данной статьи вы получили представление о том, как использовать и реализовать SelectionArea
. Если у вас остались вопросы, приветствуем ваши комментарии и предложения!
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )