Last active
August 26, 2021 00:14
-
-
Save venkatd/d259568dd49d3ea44519281cc0b65d49 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/gestures.dart'; | |
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/widgets.dart'; | |
typedef TapOutsideCallback = void Function(BoxHitTestResult hitTestResult); | |
/// Allows widgets to detect when tapping outside of the bounds of the widget. | |
/// | |
/// A common use case is allowing [TextField]s and other widgets to give up | |
/// their focus when someone taps outside of them. | |
/// | |
/// See also: | |
/// | |
/// * [TapOutsideSurface], the widget you must put close to the root of your | |
/// application in order to detect taps outside. | |
class TapOutsideDetector extends SingleChildRenderObjectWidget { | |
/// Create a new [FocusTrapArea] that expands the area of the provided [focusNode]. | |
TapOutsideDetector({ | |
Key? key, | |
required Widget child, | |
this.onTapOutside, | |
this.enabled = true, | |
}) : super(key: key, child: child); | |
/// Called when [enabled] is true and the nearest [TapOutsideSurface] | |
/// receives a in an area outside the bounds of the [child] widget | |
final TapOutsideCallback? onTapOutside; | |
/// Whether [onTapOutside] should be called | |
final bool enabled; | |
_TapOutsideSurfaceRenderObject? _lastRegisteredSurface; | |
@override | |
_TapOutsideDetectorRenderObject createRenderObject(BuildContext context) { | |
final renderObject = _TapOutsideDetectorRenderObject( | |
onTapOutside: onTapOutside, | |
enabled: enabled, | |
); | |
final surfaceRenderObject = context | |
.findAncestorRenderObjectOfType<_TapOutsideSurfaceRenderObject>(); | |
if (surfaceRenderObject == null) { | |
throw TapOutsideSurfaceNotFoundError._(renderObject); | |
} | |
if (onTapOutside != null && enabled) { | |
surfaceRenderObject.register(renderObject); | |
_lastRegisteredSurface = surfaceRenderObject; | |
} | |
return renderObject; | |
} | |
@override | |
void updateRenderObject( | |
BuildContext context, _TapOutsideDetectorRenderObject renderObject) { | |
bool wasRegistered = renderObject.shouldBeRegistered; | |
renderObject.onTapOutside = onTapOutside; | |
renderObject.enabled = enabled; | |
bool shouldBeRegistered = renderObject.shouldBeRegistered; | |
if (shouldBeRegistered == wasRegistered) { | |
return; | |
} | |
final surfaceRenderObject = context | |
.findAncestorRenderObjectOfType<_TapOutsideSurfaceRenderObject>(); | |
if (surfaceRenderObject == null) { | |
throw TapOutsideSurfaceNotFoundError._(renderObject); | |
} | |
if (shouldBeRegistered && !wasRegistered) { | |
surfaceRenderObject.register(renderObject); | |
_lastRegisteredSurface = surfaceRenderObject; | |
} else if (!shouldBeRegistered && wasRegistered) { | |
surfaceRenderObject.unregister(renderObject); | |
_lastRegisteredSurface = null; | |
} | |
} | |
@override | |
void didUnmountRenderObject(_TapOutsideDetectorRenderObject renderObject) { | |
_lastRegisteredSurface?.unregister(renderObject); | |
_lastRegisteredSurface = null; | |
} | |
} | |
class _TapOutsideDetectorRenderObject extends RenderProxyBox { | |
_TapOutsideDetectorRenderObject({ | |
required this.onTapOutside, | |
required this.enabled, | |
}); | |
TapOutsideCallback? onTapOutside; | |
bool enabled; | |
bool get shouldBeRegistered => onTapOutside != null && enabled; | |
} | |
/// In order to use [TapOutsideDetector], you must wrap the entire application | |
/// in a [TapOutsideSurface] which can detect tap outside events on behalf of | |
/// [TapOutsideDetector]s. | |
/// | |
/// For performance reasons, widgets in Flutter can't respond hit tests | |
/// outside of their bounds. | |
class TapOutsideSurface extends SingleChildRenderObjectWidget { | |
const TapOutsideSurface({ | |
required Widget child, | |
Key? key, | |
}) : super(child: child, key: key); | |
@override | |
RenderObject createRenderObject(BuildContext context) { | |
return _TapOutsideSurfaceRenderObject(); | |
} | |
@override | |
void updateRenderObject( | |
BuildContext context, _TapOutsideSurfaceRenderObject renderObject) {} | |
} | |
class _TapOutsideSurfaceRenderObject extends RenderProxyBoxWithHitTestBehavior { | |
final cachedResults = Expando<BoxHitTestResult>(); | |
final _registeredDetectors = Set<_TapOutsideDetectorRenderObject>(); | |
void register(_TapOutsideDetectorRenderObject detector) { | |
_registeredDetectors.add(detector); | |
} | |
void unregister(_TapOutsideDetectorRenderObject detector) { | |
_registeredDetectors.remove(detector); | |
} | |
@override | |
bool hitTest(BoxHitTestResult result, {required Offset position}) { | |
if (!size.contains(position)) { | |
return false; | |
} | |
final hitTarget = | |
hitTestChildren(result, position: position) || hitTestSelf(position); | |
if (hitTarget) { | |
final entry = BoxHitTestEntry(this, position); | |
cachedResults[entry] = result; | |
result.add(entry); | |
} | |
return hitTarget; | |
} | |
@override | |
void handleEvent(PointerEvent event, HitTestEntry entry) { | |
if (_registeredDetectors.isEmpty) { | |
return; | |
} | |
assert(debugHandleEvent(event, entry)); | |
if (event is! PointerDownEvent || | |
event.buttons != kPrimaryButton || | |
event.kind != PointerDeviceKind.mouse) { | |
return; | |
} | |
final BoxHitTestResult? result = cachedResults[entry]; | |
if (result == null) return; | |
final matchingDetectors = | |
_matchingDetectors(_registeredDetectors, result.path); | |
for (final detector in matchingDetectors) { | |
assert(detector.enabled); | |
detector.onTapOutside!.call(result); | |
} | |
} | |
Iterable<_TapOutsideDetectorRenderObject> _matchingDetectors( | |
Set<_TapOutsideDetectorRenderObject> detectors, | |
Iterable<HitTestEntry> hitTestPath) { | |
final hitDetectors = Set<HitTestTarget>(); | |
for (final entry in hitTestPath) { | |
final target = entry.target; | |
if (_registeredDetectors.contains(target)) { | |
hitDetectors.add(target); | |
} | |
} | |
return detectors.difference(hitDetectors); | |
} | |
} | |
/// The error that will be thrown if [TapOutsideDetector] fails to find a [TapOutsideSurface]. | |
class TapOutsideSurfaceNotFoundError<_TapOutsideSurfaceRenderObject> | |
extends Error { | |
TapOutsideSurfaceNotFoundError._(this._detector); | |
final _TapOutsideSurfaceRenderObject _detector; | |
@override | |
String toString() { | |
return ''' | |
Error: Could not find a TapOutsideSurface ancestor above $_detector | |
'''; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/rendering.dart'; | |
import 'package:flutter/widgets.dart'; | |
import 'tap_outside_detector.dart'; | |
/// Unfocuses the widget with the primary focus when a tap occurs outside the | |
/// bounds of [child]. Unfocus is only triggered if [child] is an ancestor of | |
/// the widget with primary focus. | |
class TapOutsideUnfocuser extends StatefulWidget { | |
const TapOutsideUnfocuser({required this.child}); | |
final Widget child; | |
@override | |
State<TapOutsideUnfocuser> createState() => _TapOutsideUnfocuserState(); | |
} | |
class _TapOutsideUnfocuserState extends State<TapOutsideUnfocuser> { | |
late final FocusNode focusNode; | |
bool focusNodeHasFocus = false; | |
@override | |
initState() { | |
super.initState(); | |
focusNode = FocusNode( | |
debugLabel: 'TapOutsideUnfocuser', | |
canRequestFocus: false, | |
skipTraversal: true, | |
); | |
focusNodeHasFocus = focusNode.hasFocus; | |
focusNode.addListener(_onFocusMaybeChanged); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Focus( | |
focusNode: focusNode, | |
child: TapOutsideDetector( | |
enabled: focusNodeHasFocus, | |
onTapOutside: (BoxHitTestResult result) { | |
if (_shouldUnfocus(result)) { | |
FocusManager.instance.primaryFocus?.unfocus(); | |
} | |
}, | |
child: widget.child, | |
), | |
); | |
} | |
void _onFocusMaybeChanged() { | |
if (focusNodeHasFocus != focusNode.hasFocus) { | |
setState(() { | |
focusNodeHasFocus = focusNode.hasFocus; | |
}); | |
} | |
} | |
@override | |
void dispose() { | |
super.dispose(); | |
focusNode.dispose(); | |
} | |
} | |
bool _shouldUnfocus(BoxHitTestResult result) { | |
for (final e in result.path) { | |
if (e.target is RenderEditable || e.target is _IgnoreUnfocuserRenderBox) { | |
return false; | |
} | |
} | |
return true; | |
} | |
/// Widgets besides TextField that you don't want to trigger an unfocus for | |
/// can be wrapped with [IgnoreUnfocuser] | |
class IgnoreUnfocuser extends SingleChildRenderObjectWidget { | |
const IgnoreUnfocuser({required this.child}) : super(child: child); | |
final Widget child; | |
@override | |
_IgnoreUnfocuserRenderBox createRenderObject(BuildContext context) => | |
_IgnoreUnfocuserRenderBox(); | |
} | |
class _IgnoreUnfocuserRenderBox extends RenderPointerListener {} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment