From 552700999d12411968a9ab7e454b465bcc164d3a Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Wed, 26 Jul 2023 10:31:21 -0700 Subject: [PATCH] Document the Flow/Opacity/hit-test issues (#131239) Closes https://github.com/flutter/flutter/issues/6100. --- packages/flutter/lib/src/widgets/basic.dart | 61 +++++++++++++++---- .../lib/src/widgets/implicit_animations.dart | 37 +++++++++++ .../flutter/lib/src/widgets/transitions.dart | 41 +++++++++++++ 3 files changed, 126 insertions(+), 13 deletions(-) diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index eab287bddb..afa0def4db 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -290,6 +290,22 @@ class Directionality extends _UbiquitousInheritedWidget { /// Drawing content into the offscreen buffer may also trigger render target /// switches and such switching is particularly slow in older GPUs. /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being applied +/// to the descendants of the [Opacity] widget. This can be confusing for the +/// user, who may not see anything, and may believe the area of the interface +/// where the [Opacity] is hiding a widget to be non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the [Opacity] +/// widget at all when the [opacity] is zero. +/// +/// To avoid such problems, it is generally a good idea to use an +/// [IgnorePointer] widget when setting the [opacity] to zero. This prevents +/// interactions with any children in the subtree. +/// /// See also: /// /// * [Visibility], which can hide a child more efficiently (albeit less @@ -5574,19 +5590,6 @@ class Wrap extends MultiChildRenderObjectWidget { /// this animation and repaint whenever the animation ticks, avoiding both the /// build and layout phases of the pipeline. /// -/// See also: -/// -/// * [Wrap], which provides the layout model that some other frameworks call -/// "flow", and is otherwise unrelated to [Flow]. -/// * [FlowDelegate], which controls the visual presentation of the children. -/// * [Stack], which arranges children relative to the edges of the container. -/// * [CustomSingleChildLayout], which uses a delegate to control the layout of -/// a single child. -/// * [CustomMultiChildLayout], which uses a delegate to position multiple -/// children. -/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). -/// -/// /// {@tool dartpad} /// This example uses the [Flow] widget to create a menu that opens and closes /// as it is interacted with, shown above. The color of the button in the menu @@ -5595,6 +5598,38 @@ class Wrap extends MultiChildRenderObjectWidget { /// ** See code in examples/api/lib/widgets/basic/flow.0.dart ** /// {@end-tool} /// +/// ## Hit testing and hidden [Flow] widgets +/// +/// The [Flow] widget recomputers its children's positions (as used by hit +/// testing) during the _paint_ phase rather than during the _layout_ phase. +/// +/// Widgets like [Opacity] avoid painting their children when those children +/// would be invisible due to their opacity being zero. +/// +/// Unfortunately, this means that hiding a [Flow] widget using an [Opacity] +/// widget will cause bugs when the user attempts to interact with the hidden +/// region, for example, by tapping it or clicking it. +/// +/// Such bugs will manifest either as out-of-date geometry (taps going to +/// different widgets than might be expected by the currently-specified +/// [FlowDelegate]s), or exceptions (e.g. if the last time the [Flow] was +/// painted, a different set of children was specified). +/// +/// To avoid this, when hiding a [Flow] widget with an [Opacity] widget (or +/// [AnimatedOpacity] or similar), it is wise to also disable hit testing on the +/// widget by using [IgnorePointer]. This is generally good advice anyway as +/// hit-testing invisible widgets is often confusing for the user. +/// +/// See also: +/// +/// * [Wrap], which provides the layout model that some other frameworks call +/// "flow", and is otherwise unrelated to [Flow]. +/// * [Stack], which arranges children relative to the edges of the container. +/// * [CustomSingleChildLayout], which uses a delegate to control the layout of +/// a single child. +/// * [CustomMultiChildLayout], which uses a delegate to position multiple +/// children. +/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). class Flow extends MultiChildRenderObjectWidget { /// Creates a flow layout. /// diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index f9fd981bb4..90ad122c21 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -1674,6 +1674,24 @@ class _AnimatedSlideState extends ImplicitlyAnimatedWidgetState { /// ``` /// {@end-tool} /// +/// ## Hit testing +/// +/// Setting the [opacity] to zero does not prevent hit testing from being +/// applied to the descendants of the [AnimatedOpacity] widget. This can be +/// confusing for the user, who may not see anything, and may believe the area +/// of the interface where the [AnimatedOpacity] is hiding a widget to be +/// non-interactive. +/// +/// With certain widgets, such as [Flow], that compute their positions only when +/// they are painted, this can actually lead to bugs (from unexpected geometry +/// to exceptions), because those widgets are not painted by the [AnimatedOpacity] +/// widget at all when the [opacity] animation reaches zero. +/// +/// To avoid such problems, it is generally a good idea to use an +/// [IgnorePointer] widget when setting the [opacity] to zero. This prevents +/// interactions with any children in the subtree when the [child] is animating +/// away. +/// /// See also: /// /// * [AnimatedCrossFade], for fading between two children. @@ -1771,6 +1789,25 @@ class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState