|
|
|
|
|
|
using System; |
|
|
|
using System.Linq; |
|
|
|
using uiwidgets; |
|
|
|
using Unity.UIWidgets.foundation; |
|
|
|
using Unity.UIWidgets.gestures; |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
Color _color; |
|
|
|
|
|
|
|
protected void paintInkCircle( |
|
|
|
Canvas canvas, |
|
|
|
Matrix4 transform, |
|
|
|
Paint paint, |
|
|
|
Offset center, |
|
|
|
float radius, |
|
|
|
TextDirection? textDirection = null, |
|
|
|
ShapeBorder customBorder = null, |
|
|
|
BorderRadius borderRadius = null, |
|
|
|
RectCallback clipCallback = null) { |
|
|
|
|
|
|
|
borderRadius = borderRadius ?? BorderRadius.zero; |
|
|
|
D.assert(canvas != null); |
|
|
|
D.assert(transform != null); |
|
|
|
D.assert(paint != null); |
|
|
|
D.assert(center != null); |
|
|
|
D.assert(borderRadius != null); |
|
|
|
|
|
|
|
Offset originOffset = MatrixUtils.getAsTranslation(transform); |
|
|
|
canvas.save(); |
|
|
|
if (originOffset == null) { |
|
|
|
canvas.transform(transform.storage); |
|
|
|
} else { |
|
|
|
canvas.translate(originOffset.dx, originOffset.dy); |
|
|
|
} |
|
|
|
if (clipCallback != null) { |
|
|
|
Rect rect = clipCallback(); |
|
|
|
if (customBorder != null) { |
|
|
|
canvas.clipPath(customBorder.getOuterPath(rect, textDirection: textDirection)); |
|
|
|
} |
|
|
|
else if (borderRadius != BorderRadius.zero) { |
|
|
|
canvas.clipRRect(RRect.fromRectAndCorners( |
|
|
|
rect, |
|
|
|
topLeft: borderRadius.topLeft, topRight: borderRadius.topRight, |
|
|
|
bottomLeft: borderRadius.bottomLeft, bottomRight: borderRadius.bottomRight |
|
|
|
)); |
|
|
|
} |
|
|
|
else { |
|
|
|
canvas.clipRect(rect); |
|
|
|
} |
|
|
|
} |
|
|
|
canvas.drawCircle(center, radius, paint); |
|
|
|
canvas.restore(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public abstract class InteractiveInkFeatureFactory { |
|
|
|
|
|
|
GestureTapCallback onDoubleTap = null, |
|
|
|
GestureLongPressCallback onLongPress = null, |
|
|
|
ValueChanged<bool> onHighlightChanged = null, |
|
|
|
ValueChanged<bool> onHover = null, |
|
|
|
Color focusColor = null, |
|
|
|
Color hoverColor = null, |
|
|
|
InteractiveInkFeatureFactory splashFactory = null) : base(key: key) { |
|
|
|
InteractiveInkFeatureFactory splashFactory = null, |
|
|
|
bool enableFeedback = true, |
|
|
|
FocusNode focusNode = null, |
|
|
|
bool canRequestFocus = true, |
|
|
|
ValueChanged<bool> onFocusChange = null, |
|
|
|
bool autofocus = false) : base(key: key) { |
|
|
|
this.child = child; |
|
|
|
this.onTap = onTap; |
|
|
|
this.onTapDown = onTapDown; |
|
|
|
|
|
|
this.onHighlightChanged = onHighlightChanged; |
|
|
|
this.onHover = onHover; |
|
|
|
this.focusColor = focusColor; |
|
|
|
this.hoverColor = hoverColor; |
|
|
|
this.enableFeedback = enableFeedback; |
|
|
|
this.focusNode = focusNode; |
|
|
|
this.canRequestFocus = canRequestFocus; |
|
|
|
this.onFocusChange = onFocusChange; |
|
|
|
this.autofocus = autofocus; |
|
|
|
} |
|
|
|
|
|
|
|
public readonly Widget child; |
|
|
|
|
|
|
|
|
|
|
public readonly ValueChanged<bool> onHighlightChanged; |
|
|
|
|
|
|
|
public readonly ValueChanged<bool> onHover; |
|
|
|
|
|
|
|
public readonly bool containedInkWell; |
|
|
|
|
|
|
|
public readonly BoxShape highlightShape; |
|
|
|
|
|
|
|
|
|
|
public readonly ShapeBorder customBorder; |
|
|
|
|
|
|
|
public readonly Color focusColor; |
|
|
|
|
|
|
|
public readonly Color hoverColor; |
|
|
|
|
|
|
|
|
|
|
|
public readonly bool enableFeedback; |
|
|
|
|
|
|
|
public readonly ValueChanged<bool> onFocusChange; |
|
|
|
|
|
|
|
public readonly bool autofocus; |
|
|
|
|
|
|
|
public readonly FocusNode focusNode; |
|
|
|
|
|
|
|
public readonly bool canRequestFocus; |
|
|
|
|
|
|
|
public virtual RectCallback getRectCallback(RenderBox referenceBox) { |
|
|
|
return null; |
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public enum _HighlightType { |
|
|
|
pressed, |
|
|
|
hover, |
|
|
|
focus |
|
|
|
} |
|
|
|
InkHighlight _lastHighlight; |
|
|
|
|
|
|
|
bool _hovering = false; |
|
|
|
readonly Dictionary<_HighlightType, InkHighlight> _highlights = new Dictionary<_HighlightType, InkHighlight>(); |
|
|
|
Dictionary<LocalKey, ActionFactory> _actionMap; |
|
|
|
|
|
|
|
bool highlightsExist => _highlights.Values.Count(highlight => highlight != null) != 0; |
|
|
|
|
|
|
|
void _handleAction(FocusNode node, Intent intent) { |
|
|
|
_startSplash(context: node.context); |
|
|
|
_handleTap(node.context); |
|
|
|
} |
|
|
|
|
|
|
|
UiWidgetAction _createAction() { |
|
|
|
return new CallbackAction( |
|
|
|
ActivateAction.key, |
|
|
|
onInvoke: _handleAction |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
public override void initState() { |
|
|
|
base.initState(); |
|
|
|
_actionMap = new Dictionary<LocalKey, ActionFactory>(); |
|
|
|
_actionMap[ActivateAction.key] = _createAction; |
|
|
|
FocusManager.instance.addHighlightModeListener(_handleFocusHighlightModeChange); |
|
|
|
} |
|
|
|
|
|
|
|
public override void didUpdateWidget(StatefulWidget oldWidget) { |
|
|
|
var _oldWidget = (InkResponse) oldWidget; |
|
|
|
base.didUpdateWidget(oldWidget); |
|
|
|
if (_isWidgetEnabled(widget) != _isWidgetEnabled(_oldWidget)) { |
|
|
|
_handleHoverChange(_hovering); |
|
|
|
_updateFocusHighlights(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public override void dispose() { |
|
|
|
FocusManager.instance.removeHighlightModeListener(_handleFocusHighlightModeChange); |
|
|
|
base.dispose(); |
|
|
|
} |
|
|
|
get { return _lastHighlight != null || (_splashes != null && _splashes.isNotEmpty()); } |
|
|
|
get { return highlightsExist || (_splashes != null && _splashes.isNotEmpty()); } |
|
|
|
|
|
|
|
Color getHighlightColorForType(_HighlightType type) { |
|
|
|
switch (type) { |
|
|
|
case _HighlightType.pressed: |
|
|
|
return widget.highlightColor ?? Theme.of(context).highlightColor; |
|
|
|
case _HighlightType.focus: |
|
|
|
return widget.focusColor ?? Theme.of(context).focusColor; |
|
|
|
case _HighlightType.hover: |
|
|
|
return widget.hoverColor ?? Theme.of(context).hoverColor; |
|
|
|
} |
|
|
|
D.assert(false, () => $"Unhandled {typeof(_HighlightType)} {type}"); |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
TimeSpan getFadeDurationForType(_HighlightType type) { |
|
|
|
switch (type) { |
|
|
|
case _HighlightType.pressed: |
|
|
|
return new TimeSpan(0, 0, 0, 0, 200); |
|
|
|
case _HighlightType.hover: |
|
|
|
case _HighlightType.focus: |
|
|
|
return new TimeSpan(0, 0, 0, 0, 50); |
|
|
|
} |
|
|
|
|
|
|
|
D.assert(false, () => $"Unhandled {typeof(_HighlightType)} {type}"); |
|
|
|
return TimeSpan.Zero; |
|
|
|
} |
|
|
|
|
|
|
|
public void updateHighlight(_HighlightType type, bool value) { |
|
|
|
InkHighlight highlight = _highlights[type]; |
|
|
|
public void updateHighlight(bool value) { |
|
|
|
if (value == (_lastHighlight != null && _lastHighlight.active)) { |
|
|
|
void handleInkRemoval() { |
|
|
|
D.assert(_highlights[type] != null); |
|
|
|
_highlights[type] = null; |
|
|
|
updateKeepAlive(); |
|
|
|
} |
|
|
|
|
|
|
|
if (value == (highlight != null && highlight.active)) { |
|
|
|
if (_lastHighlight == null) { |
|
|
|
if (highlight == null) { |
|
|
|
_lastHighlight = new InkHighlight( |
|
|
|
_highlights[type] = new InkHighlight( |
|
|
|
color: widget.highlightColor ?? Theme.of(context).highlightColor, |
|
|
|
color: getHighlightColorForType(type), |
|
|
|
onRemoved: _handleInkHighlightRemoval); |
|
|
|
onRemoved: handleInkRemoval, |
|
|
|
textDirection: Directionality.of(context), |
|
|
|
fadeDuration: getFadeDurationForType(type) |
|
|
|
); |
|
|
|
_lastHighlight.activate(); |
|
|
|
highlight.activate(); |
|
|
|
_lastHighlight.deactivate(); |
|
|
|
highlight.deactivate(); |
|
|
|
D.assert(value == (_lastHighlight != null && _lastHighlight.active)); |
|
|
|
if (widget.onHighlightChanged != null) { |
|
|
|
widget.onHighlightChanged(value); |
|
|
|
D.assert(value == (_highlights[type] != null && _highlights[type].active)); |
|
|
|
switch (type) { |
|
|
|
case _HighlightType.pressed: { |
|
|
|
if (widget.onHighlightChanged != null) |
|
|
|
widget.onHighlightChanged(value); |
|
|
|
break; |
|
|
|
} |
|
|
|
case _HighlightType.hover: { |
|
|
|
if (widget.onHover != null) |
|
|
|
widget.onHover(value); |
|
|
|
break; |
|
|
|
} |
|
|
|
case _HighlightType.focus: |
|
|
|
break; |
|
|
|
void _handleInkHighlightRemoval() { |
|
|
|
D.assert(_lastHighlight != null); |
|
|
|
_lastHighlight = null; |
|
|
|
updateKeepAlive(); |
|
|
|
} |
|
|
|
|
|
|
|
InteractiveInkFeature _createInkFeature(TapDownDetails details) { |
|
|
|
InteractiveInkFeature _createInkFeature(Offset globalPosition) { |
|
|
|
RenderBox referenceBox = (RenderBox) context.findRenderObject(); |
|
|
|
Offset position = referenceBox.globalToLocal(details.globalPosition); |
|
|
|
RenderBox referenceBox = context.findRenderObject() as RenderBox; |
|
|
|
Offset position = referenceBox.globalToLocal(globalPosition); |
|
|
|
Color color = widget.splashColor ?? Theme.of(context).splashColor; |
|
|
|
RectCallback rectCallback = widget.containedInkWell ? widget.getRectCallback(referenceBox) : null; |
|
|
|
BorderRadius borderRadius = widget.borderRadius; |
|
|
|
|
|
|
if (_currentSplash == splash) { |
|
|
|
_currentSplash = null; |
|
|
|
} |
|
|
|
|
|
|
|
updateKeepAlive(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
return splash; |
|
|
|
} |
|
|
|
|
|
|
|
void _handleFocusHighlightModeChange(FocusHighlightMode mode) { |
|
|
|
if (!mounted) { |
|
|
|
return; |
|
|
|
} |
|
|
|
setState(() =>{ |
|
|
|
_updateFocusHighlights(); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
void _updateFocusHighlights() { |
|
|
|
bool showFocus; |
|
|
|
switch (FocusManager.instance.highlightMode) { |
|
|
|
case FocusHighlightMode.touch: { |
|
|
|
showFocus = false; |
|
|
|
break; |
|
|
|
} |
|
|
|
case FocusHighlightMode.traditional: { |
|
|
|
showFocus = enabled && _hasFocus; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
updateHighlight(_HighlightType.focus, value: showFocus); |
|
|
|
} |
|
|
|
|
|
|
|
bool _hasFocus = false; |
|
|
|
void _handleFocusUpdate(bool hasFocus) { |
|
|
|
_hasFocus = hasFocus; |
|
|
|
_updateFocusHighlights(); |
|
|
|
if (widget.onFocusChange != null) { |
|
|
|
widget.onFocusChange(hasFocus); |
|
|
|
} |
|
|
|
} |
|
|
|
InteractiveInkFeature splash = _createInkFeature(details); |
|
|
|
_splashes = _splashes ?? new HashSet<InteractiveInkFeature>(); |
|
|
|
_splashes.Add(splash); |
|
|
|
_currentSplash = splash; |
|
|
|
_startSplash(details: details); |
|
|
|
} |
|
|
|
|
|
|
|
void _startSplash(TapDownDetails details = null, BuildContext context = null) { |
|
|
|
D.assert(details != null || context != null); |
|
|
|
Offset globalPosition; |
|
|
|
if (context != null) { |
|
|
|
RenderBox referenceBox = context.findRenderObject() as RenderBox; |
|
|
|
D.assert(referenceBox.hasSize, () => "InkResponse must be done with layout before starting a splash."); |
|
|
|
globalPosition = referenceBox.localToGlobal(referenceBox.paintBounds.center); |
|
|
|
} else { |
|
|
|
globalPosition = details.globalPosition; |
|
|
|
} |
|
|
|
InteractiveInkFeature splash = _createInkFeature(globalPosition); |
|
|
|
_splashes = _splashes ?? new HashSet<InteractiveInkFeature>(); |
|
|
|
_splashes.Add(splash); |
|
|
|
_currentSplash = splash; |
|
|
|
updateHighlight(true); |
|
|
|
updateHighlight(_HighlightType.pressed, value: true); |
|
|
|
updateHighlight(false); |
|
|
|
updateHighlight(_HighlightType.pressed, value: false); |
|
|
|
if (widget.onTap != null) { |
|
|
|
widget.onTap(); |
|
|
|
} |
|
|
|
|
|
|
widget.onTapCancel(); |
|
|
|
} |
|
|
|
|
|
|
|
updateHighlight(false); |
|
|
|
updateHighlight(_HighlightType.pressed, value: false); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleDoubleTap() { |
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
D.assert(_currentSplash == null); |
|
|
|
_lastHighlight?.dispose(); |
|
|
|
_lastHighlight = null; |
|
|
|
foreach ( _HighlightType highlight in _highlights.Keys) { |
|
|
|
_highlights[highlight]?.dispose(); |
|
|
|
_highlights[highlight] = null; |
|
|
|
} |
|
|
|
|
|
|
|
bool _isWidgetEnabled(InkResponse widget) { |
|
|
|
return widget.onTap != null || widget.onDoubleTap != null || widget.onLongPress != null; |
|
|
|
} |
|
|
|
|
|
|
|
bool enabled { |
|
|
|
get { |
|
|
|
return _isWidgetEnabled(widget); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void _handleMouseEnter(PointerEnterEvent Event) { |
|
|
|
_handleHoverChange(true); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleMouseExit(PointerExitEvent Event) { |
|
|
|
_handleHoverChange(false); |
|
|
|
} |
|
|
|
|
|
|
|
void _handleHoverChange(bool hovering) { |
|
|
|
if (_hovering != hovering) { |
|
|
|
_hovering = hovering; |
|
|
|
updateHighlight(_HighlightType.hover, value: enabled && _hovering); |
|
|
|
} |
|
|
|
} |
|
|
|
ThemeData themeData = Theme.of(context); |
|
|
|
if (_lastHighlight != null) { |
|
|
|
_lastHighlight.color = widget.highlightColor ?? themeData.highlightColor; |
|
|
|
foreach ( _HighlightType type in _highlights.Keys) { |
|
|
|
if (_highlights[type] != null) { |
|
|
|
_highlights[type].color = getHighlightColorForType(type); |
|
|
|
} |
|
|
|
_currentSplash.color = widget.splashColor ?? themeData.splashColor; |
|
|
|
_currentSplash.color = widget.splashColor ?? Theme.of(context).splashColor; |
|
|
|
|
|
|
|
bool enabled = widget.onTap != null || widget.onDoubleTap != null || |
|
|
|
widget.onLongPress != null; |
|
|
|
|
|
|
|
return new GestureDetector( |
|
|
|
onTapDown: enabled ? (GestureTapDownCallback) _handleTapDown : null, |
|
|
|
onTap: enabled ? (GestureTapCallback) (() => _handleTap(context)) : null, |
|
|
|
onTapCancel: enabled ? (GestureTapCancelCallback) _handleTapCancel : null, |
|
|
|
onDoubleTap: widget.onDoubleTap != null |
|
|
|
? (GestureDoubleTapCallback) (() => _handleDoubleTap()) |
|
|
|
: null, |
|
|
|
onLongPress: widget.onLongPress != null |
|
|
|
? (GestureLongPressCallback) (() => _handleLongPress(context)) |
|
|
|
: null, |
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
child: widget.child |
|
|
|
bool canRequestFocus = enabled && widget.canRequestFocus; |
|
|
|
|
|
|
|
return new Actions( |
|
|
|
actions: _actionMap, |
|
|
|
child: new Focus( |
|
|
|
focusNode: widget.focusNode, |
|
|
|
canRequestFocus: canRequestFocus, |
|
|
|
onFocusChange: _handleFocusUpdate, |
|
|
|
autofocus: widget.autofocus, |
|
|
|
child: new MouseRegion( |
|
|
|
onEnter: enabled ? _handleMouseEnter : (PointerEnterEventListener)null, |
|
|
|
onExit: enabled ? _handleMouseExit : (PointerExitEventListener)null, |
|
|
|
child: new GestureDetector( |
|
|
|
onTapDown: enabled ? _handleTapDown : (GestureTapDownCallback)null, |
|
|
|
onTap: enabled ? () => _handleTap(context) : (GestureTapCallback) null, |
|
|
|
onTapCancel: enabled ? _handleTapCancel : (GestureTapCancelCallback)null, |
|
|
|
onDoubleTap: widget.onDoubleTap != null ? _handleDoubleTap : (GestureDoubleTapCallback)null, |
|
|
|
onLongPress: widget.onLongPress != null ? () => _handleLongPress(context) : (GestureLongPressCallback)null, |
|
|
|
behavior: HitTestBehavior.opaque, |
|
|
|
child: widget.child |
|
|
|
) |
|
|
|
) |
|
|
|
) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
GestureTapDownCallback onTapDown = null, |
|
|
|
GestureTapCancelCallback onTapCancel = null, |
|
|
|
ValueChanged<bool> onHighlightChanged = null, |
|
|
|
ValueChanged<bool> onHover = null, |
|
|
|
Color focusColor = null, |
|
|
|
Color hoverColor = null, |
|
|
|
ShapeBorder customBorder = null |
|
|
|
ShapeBorder customBorder = null, |
|
|
|
bool enableFeedback = true, |
|
|
|
FocusNode focusNode = null, |
|
|
|
bool canRequestFocus = true, |
|
|
|
ValueChanged<bool> onFocusChange = null, |
|
|
|
bool autofocus = false |
|
|
|
) : base( |
|
|
|
key: key, |
|
|
|
child: child, |
|
|
|
|
|
|
} |
|
|
|
}, |
|
|
|
onHighlightChanged: onHighlightChanged, |
|
|
|
onHover: onHover, |
|
|
|
focusColor: focusColor, |
|
|
|
hoverColor: hoverColor, |
|
|
|
customBorder: customBorder) { |
|
|
|
customBorder: customBorder, |
|
|
|
enableFeedback: enableFeedback, |
|
|
|
focusNode: focusNode, |
|
|
|
canRequestFocus: canRequestFocus, |
|
|
|
onFocusChange: onFocusChange, |
|
|
|
autofocus: autofocus) { |
|
|
|
} |
|
|
|
} |
|
|
|
} |