mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Semantics
This commit is contained in:
parent
b5470df8d7
commit
28a1788371
|
@ -2,49 +2,16 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PageSelectorDemo extends StatelessComponent {
|
||||
Widget _buildTabIndicator(BuildContext context, String iconName) {
|
||||
final Color color = Theme.of(context).primaryColor;
|
||||
final ColorTween _selectedColor = new ColorTween(begin: Colors.transparent, end: color);
|
||||
final ColorTween _previousColor = new ColorTween(begin: color, end: Colors.transparent);
|
||||
final TabBarSelectionState selection = TabBarSelection.of(context);
|
||||
|
||||
CurvedAnimation animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease);
|
||||
return new AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
Color background = selection.value == iconName ? _selectedColor.end : _selectedColor.begin;
|
||||
if (selection.valueIsChanging) {
|
||||
// Then the selection's animation is animating from previousValue to value.
|
||||
if (selection.value == iconName)
|
||||
background = _selectedColor.evaluate(animation);
|
||||
else if (selection.previousValue == iconName)
|
||||
background = _previousColor.evaluate(animation);
|
||||
}
|
||||
return new Container(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
margin: new EdgeDims.all(4.0),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: background,
|
||||
border: new Border.all(color: _selectedColor.end),
|
||||
shape: BoxShape.circle
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTabView(String iconName) {
|
||||
return new Container(
|
||||
key: new ValueKey<String>(iconName),
|
||||
padding: const EdgeDims.all(12.0),
|
||||
child: new Card(
|
||||
child: new Center(
|
||||
child: new Icon(icon: "action/$iconName", size:IconSize.s48)
|
||||
child: new Icon(icon: 'action/$iconName', size:IconSize.s48)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -57,10 +24,10 @@ class PageSelectorDemo extends StatelessComponent {
|
|||
}
|
||||
|
||||
Widget build(BuildContext notUsed) { // Can't find the TabBarSelection from this context.
|
||||
final List<String> iconNames = <String>["event", "home", "android", "alarm", "face", "language"];
|
||||
final List<String> iconNames = <String>['event', 'home', 'android', 'alarm', 'face', 'language'];
|
||||
|
||||
return new Scaffold(
|
||||
toolBar: new ToolBar(center: new Text("Page Selector")),
|
||||
toolBar: new ToolBar(center: new Text('Page Selector')),
|
||||
body: new TabBarSelection(
|
||||
values: iconNames,
|
||||
child: new Builder(
|
||||
|
@ -72,16 +39,13 @@ class PageSelectorDemo extends StatelessComponent {
|
|||
child: new Row(
|
||||
children: <Widget>[
|
||||
new IconButton(
|
||||
icon: "navigation/arrow_back",
|
||||
icon: 'navigation/arrow_back',
|
||||
onPressed: () { _handleArrowButtonPress(context, -1); },
|
||||
tooltip: 'Back'
|
||||
),
|
||||
new Row(
|
||||
children: iconNames.map((String name) => _buildTabIndicator(context, name)).toList(),
|
||||
justifyContent: FlexJustifyContent.collapse
|
||||
),
|
||||
new TabPageSelector<String>(),
|
||||
new IconButton(
|
||||
icon: "navigation/arrow_forward",
|
||||
icon: 'navigation/arrow_forward',
|
||||
onPressed: () { _handleArrowButtonPress(context, 1); },
|
||||
tooltip: 'Forward'
|
||||
)
|
||||
|
|
|
@ -103,6 +103,7 @@ abstract class RenderSector extends RenderObject {
|
|||
}
|
||||
|
||||
Rect get paintBounds => new Rect.fromLTWH(0.0, 0.0, 2.0 * deltaRadius, 2.0 * deltaRadius);
|
||||
Rect get semanticBounds => new Rect.fromLTWH(-deltaRadius, -deltaRadius, 2.0 * deltaRadius, 2.0 * deltaRadius);
|
||||
|
||||
bool hitTest(HitTestResult result, { double radius, double theta }) {
|
||||
if (radius < parentData.radius || radius >= parentData.radius + deltaRadius ||
|
||||
|
|
|
@ -2,6 +2,7 @@ name: stocks
|
|||
version: 0.0.2
|
||||
update-url: http://localhost:9888/
|
||||
material-design-icons:
|
||||
- name: action/accessibility
|
||||
- name: action/account_balance
|
||||
- name: action/assessment
|
||||
- name: action/backup
|
||||
|
|
|
@ -43,7 +43,8 @@ class StocksAppState extends State<StocksApp> {
|
|||
backupMode: BackupMode.enabled,
|
||||
debugShowGrid: false,
|
||||
debugShowSizes: false,
|
||||
showPerformanceOverlay: false
|
||||
showPerformanceOverlay: false,
|
||||
showSemanticsDebugger: false
|
||||
);
|
||||
|
||||
void initState() {
|
||||
|
@ -110,6 +111,7 @@ class StocksAppState extends State<StocksApp> {
|
|||
theme: theme,
|
||||
debugShowMaterialGrid: _configuration.debugShowGrid,
|
||||
showPerformanceOverlay: _configuration.showPerformanceOverlay,
|
||||
showSemanticsDebugger: _configuration.showSemanticsDebugger,
|
||||
routes: <String, RouteBuilder>{
|
||||
'/': (RouteArguments args) => new StockHome(_stocks, _symbols, _configuration, configurationUpdater),
|
||||
'/settings': (RouteArguments args) => new StockSettings(_configuration, configurationUpdater)
|
||||
|
|
|
@ -88,16 +88,16 @@ class StockHomeState extends State<StockHome> {
|
|||
content: new Text('This feature has not yet been implemented.'),
|
||||
actions: <Widget>[
|
||||
new FlatButton(
|
||||
child: new Text('USE IT'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
}
|
||||
},
|
||||
child: new Text('USE IT')
|
||||
),
|
||||
new FlatButton(
|
||||
child: new Text('OH WELL'),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
}
|
||||
},
|
||||
child: new Text('OH WELL')
|
||||
),
|
||||
]
|
||||
)
|
||||
|
@ -107,7 +107,16 @@ class StockHomeState extends State<StockHome> {
|
|||
),
|
||||
new DrawerItem(
|
||||
icon: 'device/dvr',
|
||||
onPressed: () { debugDumpApp(); debugDumpRenderTree(); debugDumpLayerTree(); },
|
||||
onPressed: () {
|
||||
try {
|
||||
debugDumpApp();
|
||||
debugDumpRenderTree();
|
||||
debugDumpLayerTree();
|
||||
debugDumpSemanticsTree();
|
||||
} catch (e, stack) {
|
||||
debugPrint('Exception while dumping app:\n$e\n$stack');
|
||||
}
|
||||
},
|
||||
child: new Text('Dump App to Console')
|
||||
),
|
||||
new DrawerDivider(),
|
||||
|
|
|
@ -35,6 +35,10 @@ class StockSettingsState extends State<StockSettings> {
|
|||
sendUpdates(config.configuration.copyWith(showPerformanceOverlay: value));
|
||||
}
|
||||
|
||||
void _handleShowSemanticsDebuggerChanged(bool value) {
|
||||
sendUpdates(config.configuration.copyWith(showSemanticsDebugger: value));
|
||||
}
|
||||
|
||||
void _confirmOptimismChange() {
|
||||
switch (config.configuration.stockMode) {
|
||||
case StockMode.optimistic:
|
||||
|
@ -118,6 +122,19 @@ class StockSettingsState extends State<StockSettings> {
|
|||
]
|
||||
)
|
||||
),
|
||||
new DrawerItem(
|
||||
icon: 'action/accessibility',
|
||||
onPressed: () { _handleShowSemanticsDebuggerChanged(!config.configuration.showSemanticsDebugger); },
|
||||
child: new Row(
|
||||
children: <Widget>[
|
||||
new Flexible(child: new Text('Show semantics overlay')),
|
||||
new Switch(
|
||||
value: config.configuration.showSemanticsDebugger,
|
||||
onChanged: _handleShowSemanticsDebuggerChanged
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
];
|
||||
assert(() {
|
||||
// material grid and size construction lines are only available in checked mode
|
||||
|
|
|
@ -13,13 +13,15 @@ class StockConfiguration {
|
|||
this.backupMode,
|
||||
this.debugShowGrid,
|
||||
this.debugShowSizes,
|
||||
this.showPerformanceOverlay
|
||||
this.showPerformanceOverlay,
|
||||
this.showSemanticsDebugger
|
||||
}) {
|
||||
assert(stockMode != null);
|
||||
assert(backupMode != null);
|
||||
assert(debugShowGrid != null);
|
||||
assert(debugShowSizes != null);
|
||||
assert(showPerformanceOverlay != null);
|
||||
assert(showSemanticsDebugger != null);
|
||||
}
|
||||
|
||||
final StockMode stockMode;
|
||||
|
@ -27,20 +29,23 @@ class StockConfiguration {
|
|||
final bool debugShowGrid;
|
||||
final bool debugShowSizes;
|
||||
final bool showPerformanceOverlay;
|
||||
final bool showSemanticsDebugger;
|
||||
|
||||
StockConfiguration copyWith({
|
||||
StockMode stockMode,
|
||||
BackupMode backupMode,
|
||||
bool debugShowGrid,
|
||||
bool debugShowSizes,
|
||||
bool showPerformanceOverlay
|
||||
bool showPerformanceOverlay,
|
||||
bool showSemanticsDebugger
|
||||
}) {
|
||||
return new StockConfiguration(
|
||||
stockMode: stockMode ?? this.stockMode,
|
||||
backupMode: backupMode ?? this.backupMode,
|
||||
debugShowGrid: debugShowGrid ?? this.debugShowGrid,
|
||||
debugShowSizes: debugShowSizes ?? this.debugShowSizes,
|
||||
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay
|
||||
showPerformanceOverlay: showPerformanceOverlay ?? this.showPerformanceOverlay,
|
||||
showSemanticsDebugger: showSemanticsDebugger ?? this.showSemanticsDebugger
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ export 'src/rendering/overflow.dart';
|
|||
export 'src/rendering/paragraph.dart';
|
||||
export 'src/rendering/performance_overlay.dart';
|
||||
export 'src/rendering/proxy_box.dart';
|
||||
export 'src/rendering/semantics.dart';
|
||||
export 'src/rendering/shifted_box.dart';
|
||||
export 'src/rendering/stack.dart';
|
||||
export 'src/rendering/view.dart';
|
||||
|
|
|
@ -31,7 +31,7 @@ class TapGestureRecognizer extends PrimaryPointerGestureRecognizer {
|
|||
);
|
||||
|
||||
GestureTapDownCallback onTapDown;
|
||||
GestureTapDownCallback onTapUp;
|
||||
GestureTapUpCallback onTapUp;
|
||||
GestureTapCallback onTap;
|
||||
GestureTapCancelCallback onTapCancel;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart';
|
|||
import 'colors.dart';
|
||||
import 'debug.dart';
|
||||
import 'icon.dart';
|
||||
import 'tooltip.dart';
|
||||
|
||||
const double _kChipHeight = 32.0;
|
||||
const double _kAvatarDiamater = _kChipHeight;
|
||||
|
@ -41,11 +42,13 @@ class Chip extends StatelessComponent {
|
|||
|
||||
if (avatar != null) {
|
||||
leftPadding = 0.0;
|
||||
children.add(new Container(
|
||||
margin: const EdgeDims.only(right: 8.0),
|
||||
width: _kAvatarDiamater,
|
||||
height: _kAvatarDiamater,
|
||||
child: avatar
|
||||
children.add(new ExcludeSemantics(
|
||||
child: new Container(
|
||||
margin: const EdgeDims.only(right: 8.0),
|
||||
width: _kAvatarDiamater,
|
||||
height: _kAvatarDiamater,
|
||||
child: avatar
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -58,27 +61,33 @@ class Chip extends StatelessComponent {
|
|||
rightPadding = 0.0;
|
||||
children.add(new GestureDetector(
|
||||
onTap: onDeleted,
|
||||
child: new Container(
|
||||
padding: const EdgeDims.symmetric(horizontal: 4.0),
|
||||
child: new Icon(
|
||||
icon: 'navigation/cancel',
|
||||
size: IconSize.s18,
|
||||
color: Colors.black54
|
||||
child: new Tooltip(
|
||||
message: 'Delete "$label"',
|
||||
child: new Container(
|
||||
padding: const EdgeDims.symmetric(horizontal: 4.0),
|
||||
child: new Icon(
|
||||
icon: 'navigation/cancel',
|
||||
size: IconSize.s18,
|
||||
color: Colors.black54
|
||||
)
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
return new Container(
|
||||
height: _kChipHeight,
|
||||
padding: new EdgeDims.only(left: leftPadding, right: rightPadding),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Colors.grey[300],
|
||||
borderRadius: 16.0
|
||||
),
|
||||
child: new Row(
|
||||
children: children,
|
||||
justifyContent: FlexJustifyContent.collapse
|
||||
return new Semantics(
|
||||
container: true,
|
||||
child: new Container(
|
||||
height: _kChipHeight,
|
||||
padding: new EdgeDims.only(left: leftPadding, right: rightPadding),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: Colors.grey[300],
|
||||
borderRadius: 16.0
|
||||
),
|
||||
child: new Row(
|
||||
children: children,
|
||||
justifyContent: FlexJustifyContent.collapse
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ class DrawerControllerState extends State<DrawerController> {
|
|||
});
|
||||
}
|
||||
|
||||
void _handlePointerDown(_) {
|
||||
void _handleTapDown(Point position) {
|
||||
_controller.stop();
|
||||
_ensureHistoryEntry();
|
||||
}
|
||||
|
@ -166,6 +166,7 @@ class DrawerControllerState extends State<DrawerController> {
|
|||
onHorizontalDragUpdate: _move,
|
||||
onHorizontalDragEnd: _settle,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
excludeFromSemantics: true,
|
||||
child: new Container(width: _kEdgeDragWidth)
|
||||
)
|
||||
);
|
||||
|
@ -188,8 +189,8 @@ class DrawerControllerState extends State<DrawerController> {
|
|||
),
|
||||
new Align(
|
||||
alignment: const FractionalOffset(0.0, 0.5),
|
||||
child: new Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
child: new GestureDetector(
|
||||
onTapDown: _handleTapDown,
|
||||
child: new Align(
|
||||
alignment: const FractionalOffset(1.0, 0.5),
|
||||
widthFactor: _controller.value,
|
||||
|
|
|
@ -70,11 +70,13 @@ class DrawerItem extends StatelessComponent {
|
|||
)
|
||||
);
|
||||
|
||||
return new Container(
|
||||
height: 48.0,
|
||||
child: new InkWell(
|
||||
onTap: onPressed,
|
||||
child: new Row(children: children)
|
||||
return new MergeSemantics(
|
||||
child: new Container(
|
||||
height: 48.0,
|
||||
child: new InkWell(
|
||||
onTap: onPressed,
|
||||
child: new Row(children: children)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,15 +26,12 @@ class IconButton extends StatelessComponent {
|
|||
final String tooltip;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
Widget result = new InkResponse(
|
||||
onTap: onPressed,
|
||||
child: new Padding(
|
||||
padding: const EdgeDims.all(8.0),
|
||||
child: new Icon(
|
||||
icon: icon,
|
||||
colorTheme: colorTheme,
|
||||
color: color
|
||||
)
|
||||
Widget result = new Padding(
|
||||
padding: const EdgeDims.all(8.0),
|
||||
child: new Icon(
|
||||
icon: icon,
|
||||
colorTheme: colorTheme,
|
||||
color: color
|
||||
)
|
||||
);
|
||||
if (tooltip != null) {
|
||||
|
@ -43,7 +40,10 @@ class IconButton extends StatelessComponent {
|
|||
child: result
|
||||
);
|
||||
}
|
||||
return result;
|
||||
return new InkResponse(
|
||||
onTap: onPressed,
|
||||
child: result
|
||||
);
|
||||
}
|
||||
|
||||
void debugFillDescription(List<String> description) {
|
||||
|
|
|
@ -49,12 +49,14 @@ class MaterialApp extends StatefulComponent {
|
|||
this.onGenerateRoute,
|
||||
this.onLocaleChanged,
|
||||
this.debugShowMaterialGrid: false,
|
||||
this.showPerformanceOverlay: false
|
||||
this.showPerformanceOverlay: false,
|
||||
this.showSemanticsDebugger: false
|
||||
}) : super(key: key) {
|
||||
assert(routes != null);
|
||||
assert(routes.containsKey(Navigator.defaultRouteName) || onGenerateRoute != null);
|
||||
assert(debugShowMaterialGrid != null);
|
||||
assert(showPerformanceOverlay != null);
|
||||
assert(showSemanticsDebugger != null);
|
||||
}
|
||||
|
||||
final String title;
|
||||
|
@ -64,6 +66,7 @@ class MaterialApp extends StatefulComponent {
|
|||
final LocaleChangedCallback onLocaleChanged;
|
||||
final bool debugShowMaterialGrid;
|
||||
final bool showPerformanceOverlay;
|
||||
final bool showSemanticsDebugger;
|
||||
|
||||
_MaterialAppState createState() => new _MaterialAppState();
|
||||
}
|
||||
|
@ -194,6 +197,11 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
|
|||
]
|
||||
);
|
||||
}
|
||||
if (config.showSemanticsDebugger) {
|
||||
result = new SemanticsDebugger(
|
||||
child: result
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,14 +32,16 @@ class PopupMenuItem<T> extends StatelessComponent {
|
|||
final T value;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Container(
|
||||
height: _kMenuItemHeight,
|
||||
padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
|
||||
child: new DefaultTextStyle(
|
||||
style: Theme.of(context).text.subhead,
|
||||
child: new Baseline(
|
||||
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
|
||||
child: child
|
||||
return new MergeSemantics(
|
||||
child: new Container(
|
||||
height: _kMenuItemHeight,
|
||||
padding: const EdgeDims.symmetric(horizontal: _kMenuHorizontalPadding),
|
||||
child: new DefaultTextStyle(
|
||||
style: Theme.of(context).text.subhead,
|
||||
child: new Baseline(
|
||||
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
|
||||
child: child
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
|
|
@ -44,11 +44,14 @@ class Radio<T> extends StatelessComponent {
|
|||
Widget build(BuildContext context) {
|
||||
assert(debugCheckHasMaterial(context));
|
||||
ThemeData themeData = Theme.of(context);
|
||||
return new _RadioRenderObjectWidget(
|
||||
selected: value == groupValue,
|
||||
activeColor: activeColor ?? themeData.accentColor,
|
||||
inactiveColor: _getInactiveColor(themeData),
|
||||
onChanged: _enabled ? _handleChanged : null
|
||||
return new Semantics(
|
||||
checked: value == groupValue,
|
||||
child: new _RadioRenderObjectWidget(
|
||||
selected: value == groupValue,
|
||||
activeColor: activeColor ?? themeData.accentColor,
|
||||
inactiveColor: _getInactiveColor(themeData),
|
||||
onChanged: _enabled ? _handleChanged : null
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -495,10 +495,13 @@ class _PersistentBottomSheetState extends State<_PersistentBottomSheet> {
|
|||
child: child
|
||||
);
|
||||
},
|
||||
child: new BottomSheet(
|
||||
animationController: config.animationController,
|
||||
onClosing: config.onClosing,
|
||||
builder: config.builder
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
child: new BottomSheet(
|
||||
animationController: config.animationController,
|
||||
onClosing: config.onClosing,
|
||||
builder: config.builder
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -95,23 +95,26 @@ class SnackBar extends StatelessComponent {
|
|||
child: child
|
||||
);
|
||||
},
|
||||
child: new Material(
|
||||
elevation: 6,
|
||||
color: _kSnackBackground,
|
||||
child: new Container(
|
||||
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
|
||||
child: new Theme(
|
||||
data: new ThemeData(
|
||||
brightness: ThemeBrightness.dark,
|
||||
accentColor: theme.accentColor,
|
||||
accentColorBrightness: theme.accentColorBrightness,
|
||||
text: Typography.white
|
||||
),
|
||||
child: new FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: new Row(
|
||||
children: children,
|
||||
alignItems: FlexAlignItems.center
|
||||
child: new Semantics(
|
||||
container: true,
|
||||
child: new Material(
|
||||
elevation: 6,
|
||||
color: _kSnackBackground,
|
||||
child: new Container(
|
||||
margin: const EdgeDims.symmetric(horizontal: _kSideMargins),
|
||||
child: new Theme(
|
||||
data: new ThemeData(
|
||||
brightness: ThemeBrightness.dark,
|
||||
accentColor: theme.accentColor,
|
||||
accentColorBrightness: theme.accentColorBrightness,
|
||||
text: Typography.white
|
||||
),
|
||||
child: new FadeTransition(
|
||||
opacity: fadeAnimation,
|
||||
child: new Row(
|
||||
children: children,
|
||||
alignItems: FlexAlignItems.center
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -405,7 +405,7 @@ class TabBarSelection<T> extends StatefulComponent {
|
|||
final ValueChanged<T> onChanged;
|
||||
final Widget child;
|
||||
|
||||
TabBarSelectionState createState() => new TabBarSelectionState<T>();
|
||||
TabBarSelectionState<T> createState() => new TabBarSelectionState<T>();
|
||||
|
||||
static TabBarSelectionState of(BuildContext context) {
|
||||
return context.ancestorStateOfType(const TypeMatcher<TabBarSelectionState>());
|
||||
|
@ -952,3 +952,52 @@ class _TabBarViewState extends PageableListState<TabBarView> implements TabBarSe
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabPageSelector<T> extends StatelessComponent {
|
||||
const TabPageSelector({ Key key }) : super(key: key);
|
||||
|
||||
Widget _buildTabIndicator(TabBarSelectionState<T> selection, T tab, Animation animation, ColorTween selectedColor, ColorTween previousColor) {
|
||||
Color background;
|
||||
if (selection.valueIsChanging) {
|
||||
// The selection's animation is animating from previousValue to value.
|
||||
if (selection.value == tab)
|
||||
background = selectedColor.evaluate(animation);
|
||||
else if (selection.previousValue == tab)
|
||||
background = previousColor.evaluate(animation);
|
||||
else
|
||||
background = selectedColor.begin;
|
||||
} else {
|
||||
background = selection.value == tab ? selectedColor.end : selectedColor.begin;
|
||||
}
|
||||
return new Container(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
margin: new EdgeDims.all(4.0),
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: background,
|
||||
border: new Border.all(color: selectedColor.end),
|
||||
shape: BoxShape.circle
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
final TabBarSelectionState selection = TabBarSelection.of(context);
|
||||
final Color color = Theme.of(context).primaryColor;
|
||||
final ColorTween selectedColor = new ColorTween(begin: Colors.transparent, end: color);
|
||||
final ColorTween previousColor = new ColorTween(begin: color, end: Colors.transparent);
|
||||
Animation<double> animation = new CurvedAnimation(parent: selection.animation, curve: Curves.ease);
|
||||
return new AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return new Semantics(
|
||||
label: 'Page ${selection.index + 1} of ${selection.values.length}',
|
||||
child: new Row(
|
||||
children: selection.values.map((T tab) => _buildTabIndicator(selection, tab, animation, selectedColor, previousColor)).toList(),
|
||||
justifyContent: FlexJustifyContent.collapse
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,17 +14,18 @@ const Duration _kToggleDuration = const Duration(milliseconds: 200);
|
|||
// toggle animations. It handles storing the current value, dispatching
|
||||
// ValueChanged on a tap gesture and driving a changed animation. Subclasses are
|
||||
// responsible for painting.
|
||||
abstract class RenderToggleable extends RenderConstrainedBox {
|
||||
abstract class RenderToggleable extends RenderConstrainedBox implements SemanticActionHandler {
|
||||
RenderToggleable({
|
||||
bool value,
|
||||
Size size,
|
||||
Color activeColor,
|
||||
Color inactiveColor,
|
||||
this.onChanged,
|
||||
ValueChanged<bool> onChanged,
|
||||
double minRadialReactionRadius: 0.0
|
||||
}) : _value = value,
|
||||
_activeColor = activeColor,
|
||||
_inactiveColor = inactiveColor,
|
||||
_onChanged = onChanged,
|
||||
super(additionalConstraints: new BoxConstraints.tight(size)) {
|
||||
assert(value != null);
|
||||
assert(activeColor != null);
|
||||
|
@ -61,6 +62,7 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
if (value == _value)
|
||||
return;
|
||||
_value = value;
|
||||
markNeedsSemanticsUpdate(onlyChanges: true, noGeometry: true);
|
||||
_position
|
||||
..curve = Curves.easeIn
|
||||
..reverseCurve = Curves.easeOut;
|
||||
|
@ -87,9 +89,20 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
markNeedsPaint();
|
||||
}
|
||||
|
||||
bool get isInteractive => onChanged != null;
|
||||
ValueChanged<bool> get onChanged => _onChanged;
|
||||
ValueChanged<bool> _onChanged;
|
||||
void set onChanged(ValueChanged<bool> value) {
|
||||
if (value == _onChanged)
|
||||
return;
|
||||
final bool wasInteractive = isInteractive;
|
||||
_onChanged = value;
|
||||
if (wasInteractive != isInteractive) {
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate(noGeometry: true);
|
||||
}
|
||||
}
|
||||
|
||||
ValueChanged<bool> onChanged;
|
||||
bool get isInteractive => onChanged != null;
|
||||
|
||||
CurvedAnimation get position => _position;
|
||||
CurvedAnimation _position;
|
||||
|
@ -146,4 +159,20 @@ abstract class RenderToggleable extends RenderConstrainedBox {
|
|||
canvas.drawCircle(offset.toPoint(), _reaction.value, reactionPaint);
|
||||
}
|
||||
}
|
||||
|
||||
bool get hasSemantics => isInteractive;
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
yield (SemanticsNode semantics) {
|
||||
semantics.hasCheckedState = true;
|
||||
semantics.isChecked = _value;
|
||||
semantics.canBeTapped = isInteractive;
|
||||
};
|
||||
}
|
||||
void handleSemanticTap() => _handleTap();
|
||||
|
||||
void handleSemanticLongPress() { }
|
||||
void handleSemanticScrollLeft() { }
|
||||
void handleSemanticScrollRight() { }
|
||||
void handleSemanticScrollUp() { }
|
||||
void handleSemanticScrollDown() { }
|
||||
}
|
||||
|
|
|
@ -67,6 +67,13 @@ class Tooltip extends StatefulComponent {
|
|||
final Widget child;
|
||||
|
||||
_TooltipState createState() => new _TooltipState();
|
||||
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('"$message"');
|
||||
description.add('vertical offset: $verticalOffset');
|
||||
description.add('position: ${preferBelow ? "below" : "above"}');
|
||||
}
|
||||
}
|
||||
|
||||
class _TooltipState extends State<Tooltip> {
|
||||
|
@ -175,7 +182,11 @@ class _TooltipState extends State<Tooltip> {
|
|||
return new GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: showTooltip,
|
||||
child: config.child
|
||||
excludeFromSemantics: true,
|
||||
child: new Semantics(
|
||||
label: config.message,
|
||||
child: config.child
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ import 'text_style.dart';
|
|||
abstract class TextSpan {
|
||||
// This class must be immutable, because we won't notice when it changes.
|
||||
const TextSpan();
|
||||
String toString([String prefix = '']);
|
||||
void build(ui.ParagraphBuilder builder);
|
||||
ui.ParagraphStyle get paragraphStyle => null;
|
||||
String toPlainText(); // for semantics
|
||||
String toString([String prefix = '']); // for debugging
|
||||
}
|
||||
|
||||
/// An immutable span of unstyled text.
|
||||
|
@ -37,6 +38,7 @@ class PlainTextSpan extends TextSpan {
|
|||
|
||||
int get hashCode => text.hashCode;
|
||||
|
||||
String toPlainText() => text;
|
||||
String toString([String prefix = '']) => '$prefix$runtimeType: "$text"';
|
||||
}
|
||||
|
||||
|
@ -81,6 +83,8 @@ class StyledTextSpan extends TextSpan {
|
|||
|
||||
int get hashCode => hashValues(style, hashList(children));
|
||||
|
||||
String toPlainText() => children.map((TextSpan child) => child.toPlainText()).join();
|
||||
|
||||
String toString([String prefix = '']) {
|
||||
List<String> result = <String>[];
|
||||
result.add('$prefix$runtimeType:');
|
||||
|
@ -94,7 +98,7 @@ class StyledTextSpan extends TextSpan {
|
|||
|
||||
/// An object that paints a [TextSpan] into a canvas.
|
||||
class TextPainter {
|
||||
TextPainter(TextSpan text) {
|
||||
TextPainter([ TextSpan text ]) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
@ -16,6 +17,7 @@ class MatrixUtils {
|
|||
///
|
||||
/// Returns null, otherwise.
|
||||
static Offset getAsTranslation(Matrix4 transform) {
|
||||
assert(transform != null);
|
||||
Float64List values = transform.storage;
|
||||
// Values are stored in column-major order.
|
||||
if (values[0] == 1.0 &&
|
||||
|
@ -37,4 +39,84 @@ class MatrixUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Returns true if the given matrices are exactly equal, and false
|
||||
/// otherwise. Null values are assumed to be the identity matrix.
|
||||
static bool matrixEquals(Matrix4 a, Matrix4 b) {
|
||||
if (identical(a, b))
|
||||
return true;
|
||||
assert(a != null || b != null);
|
||||
if (a == null)
|
||||
return isIdentity(b);
|
||||
if (b == null)
|
||||
return isIdentity(a);
|
||||
assert(a != null && b != null);
|
||||
return a.storage[0] == b.storage[0]
|
||||
&& a.storage[1] == b.storage[1]
|
||||
&& a.storage[2] == b.storage[2]
|
||||
&& a.storage[3] == b.storage[3]
|
||||
&& a.storage[4] == b.storage[4]
|
||||
&& a.storage[5] == b.storage[5]
|
||||
&& a.storage[6] == b.storage[6]
|
||||
&& a.storage[7] == b.storage[7]
|
||||
&& a.storage[8] == b.storage[8]
|
||||
&& a.storage[9] == b.storage[9]
|
||||
&& a.storage[10] == b.storage[10]
|
||||
&& a.storage[11] == b.storage[11]
|
||||
&& a.storage[12] == b.storage[12]
|
||||
&& a.storage[13] == b.storage[13]
|
||||
&& a.storage[14] == b.storage[14]
|
||||
&& a.storage[15] == b.storage[15];
|
||||
}
|
||||
|
||||
static bool isIdentity(Matrix4 a) {
|
||||
assert(a != null);
|
||||
return a.storage[0] == 1.0 // col 1
|
||||
&& a.storage[1] == 0.0
|
||||
&& a.storage[2] == 0.0
|
||||
&& a.storage[3] == 0.0
|
||||
&& a.storage[4] == 0.0 // col 2
|
||||
&& a.storage[5] == 1.0
|
||||
&& a.storage[6] == 0.0
|
||||
&& a.storage[7] == 0.0
|
||||
&& a.storage[8] == 0.0 // col 3
|
||||
&& a.storage[9] == 0.0
|
||||
&& a.storage[10] == 1.0
|
||||
&& a.storage[11] == 0.0
|
||||
&& a.storage[12] == 0.0 // col 4
|
||||
&& a.storage[13] == 0.0
|
||||
&& a.storage[14] == 0.0
|
||||
&& a.storage[15] == 1.0;
|
||||
}
|
||||
|
||||
static Point transformPoint(Matrix4 transform, Point point) {
|
||||
Vector3 position3 = new Vector3(point.x, point.y, 0.0);
|
||||
Vector3 transformed3 = transform.transform3(position3);
|
||||
return new Point(transformed3.x, transformed3.y);
|
||||
}
|
||||
|
||||
static double _min4(double a, double b, double c, double d) {
|
||||
return math.min(a, math.min(b, math.min(c, d)));
|
||||
}
|
||||
static double _max4(double a, double b, double c, double d) {
|
||||
return math.max(a, math.max(b, math.max(c, d)));
|
||||
}
|
||||
|
||||
static Rect transformRect(Rect rect, Matrix4 transform) {
|
||||
assert(rect != null);
|
||||
assert(transform.determinant != 0.0);
|
||||
if (isIdentity(transform))
|
||||
return rect;
|
||||
transform = new Matrix4.copy(transform)..invert();
|
||||
Point point1 = transformPoint(transform, rect.topLeft);
|
||||
Point point2 = transformPoint(transform, rect.topRight);
|
||||
Point point3 = transformPoint(transform, rect.bottomLeft);
|
||||
Point point4 = transformPoint(transform, rect.bottomRight);
|
||||
return new Rect.fromLTRB(
|
||||
_min4(point1.x, point2.x, point3.x, point4.x),
|
||||
_min4(point1.y, point2.y, point3.y, point4.y),
|
||||
_max4(point1.x, point2.x, point3.x, point4.x),
|
||||
_max4(point1.y, point2.y, point3.y, point4.y)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
112
packages/flutter/lib/src/rendering/README.md
Normal file
112
packages/flutter/lib/src/rendering/README.md
Normal file
|
@ -0,0 +1,112 @@
|
|||
Flutter Rendering Layer
|
||||
=======================
|
||||
|
||||
This document is intended to describe some of the core designs of the
|
||||
Flutter rendering layer.
|
||||
|
||||
Layout
|
||||
------
|
||||
|
||||
Paint
|
||||
-----
|
||||
|
||||
Compositing
|
||||
-----------
|
||||
|
||||
Semantics
|
||||
---------
|
||||
|
||||
The last phase of a frame is the Semantics phase. This only occurs if
|
||||
a semantics server has been installed, for example if the user is
|
||||
using an accessibility tool.
|
||||
|
||||
Each frame, the semantics phase starts with a call to the static
|
||||
`RenderObject.flushSemantics()` method from the `Renderer` binding's
|
||||
`beginFrame()` method.
|
||||
|
||||
Each node marked as needing semantics (which initially is just the
|
||||
root node, as scheduled by `scheduleInitialSemantics()`), in depth
|
||||
order, has its semantics updated by calling `_updateSemantics()`.
|
||||
|
||||
The `_updateSemantics()` method calls `_getSemantics()` to obtain an
|
||||
`_InterestingSemanticsFragment`, and then calls `compile()` on that
|
||||
fragment to obtain a `SemanticsNode` which becomes the value of the
|
||||
`RenderObject`'s `_semantics` field. **This is essentially a two-pass
|
||||
walk of the render tree. The first pass determines the shape of the
|
||||
output tree, and the second creates the nodes of this tree and hooks
|
||||
them together.** The second walk is a sparse walk; it only walks the
|
||||
nodes that are interesting for the purpose of semantics.
|
||||
|
||||
`_getSemantics()` is the core function that walks the render tree to
|
||||
obtain the semantics. It collects semantic annotators for this
|
||||
`RenderObject`, then walks its children collecting
|
||||
`_SemanticsFragment`s for them, and then returns an appropriate
|
||||
`_SemanticsFragment` object that describes the `RenderObject`'s
|
||||
semantics.
|
||||
|
||||
Semantic annotators are functions that, given a `SemanticsNode`, set
|
||||
some flags or strings on the object. They are obtained from
|
||||
`getSemanticAnnotators()`. For example, here is how `RenderParagraph`
|
||||
annotates the `SemanticsNode` with its text:
|
||||
|
||||
```dart
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
yield (SemanticsNode node) {
|
||||
node.label = text.toPlainText();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
A `_SemanticsFragment` object is a node in a short-lived tree which is
|
||||
used to create the final `SemanticsNode` tree that is sent to the
|
||||
semantics server. These objects have a list of semantic annotators,
|
||||
and a list of `_SemanticsFragment` children.
|
||||
|
||||
There are several `_SemanticsFragment` classes. The `_getSemantics()`
|
||||
method picks its return value as follows:
|
||||
|
||||
* `_CleanSemanticsFragment` is used to represent a `RenderObject` that
|
||||
has a `SemanticsNode` and which is in no way dirty. This class has
|
||||
no children and no annotators, and when compiled, it returns the
|
||||
`SemanticsNode` that the `RenderObject` already has.
|
||||
|
||||
* `_RootSemanticsFragment`* is used to represent the `RenderObject`
|
||||
found at the top of the render tree. This class always compiles to a
|
||||
`SemanticsNode` with ID 0.
|
||||
|
||||
* `_ConcreteSemanticsFragment`* is used to represent a `RenderObject`
|
||||
that has `hasSemantics` set to true. It returns the `SemanticsNode`
|
||||
for that `RenderObject`.
|
||||
|
||||
* `_ImplicitSemanticsFragment`* is used to represent a `RenderObject`
|
||||
that does not have `hasSemantics` set to true, but which does have
|
||||
some semantic annotators. When it is compiled, if the nearest
|
||||
ancestor `_SemanticsFragment` that isn't also an
|
||||
`_ImplicitSemanticsFragment` is a `_RootSemanticsFragment` or a
|
||||
`_ConcreteSemanticsFragment`, then the `SemanticsNode` from that
|
||||
object is reused. Otherwise, a new one is created.
|
||||
|
||||
* `_ForkingSemanticsFragment` is used to represent a `RenderObject`
|
||||
that introduces no semantics of its own, but which has two or more
|
||||
descendants that do introduce semantics (and which are not ancestors
|
||||
or descendants of each other).
|
||||
|
||||
* For `RenderObject` nodes that introduce no semantics but which have
|
||||
a (single) child that does, the `_SemanticsFragment` of the child is
|
||||
returned.
|
||||
|
||||
* For `RenderObject` nodes that introduce no semantics and have no
|
||||
descendants that introduce semantics, `null` is returned.
|
||||
|
||||
The classes marked with an asterisk * above are the
|
||||
`_InterestingSemanticsFragment` classes.
|
||||
|
||||
When the `_SemanticsFragment` tree is then compiled, the
|
||||
`SemanticsNode` objects are created (if necessary), the semantic
|
||||
annotators are run on each `SemanticsNode`, the geometry (matrix,
|
||||
size, and clip) is applied, and the children are updated.
|
||||
|
||||
As part of this, the code clears out the `_semantics` field of any
|
||||
`RenderObject` that previously had a `SemanticsNode` but no longer
|
||||
does. This is done as part of the first walk where possible, and as
|
||||
part of the second otherwise.
|
|
@ -16,3 +16,33 @@ export 'dart:ui' show
|
|||
VoidCallback;
|
||||
|
||||
typedef void ValueChanged<T>(T value);
|
||||
|
||||
/// A BitField over an enum (or other class whose values implement "index").
|
||||
/// Only the first 63 values of the enum can be used as indices.
|
||||
class BitField<T extends dynamic> {
|
||||
static const _kSMIBits = 63; // see https://www.dartlang.org/articles/numeric-computation/#smis-and-mints
|
||||
static const _kAllZeros = 0;
|
||||
static const _kAllOnes = 0x7FFFFFFFFFFFFFFF; // 2^(_kSMIBits+1)-1
|
||||
BitField(this._length) : _bits = _kAllZeros {
|
||||
assert(_length <= _kSMIBits);
|
||||
}
|
||||
BitField.filled(this._length, bool value) : _bits = value ? _kAllOnes : _kAllZeros {
|
||||
assert(_length <= _kSMIBits);
|
||||
}
|
||||
final int _length;
|
||||
int _bits;
|
||||
bool operator [](T index) {
|
||||
assert(index.index < _length);
|
||||
return (_bits & 1 << index.index) > 0;
|
||||
}
|
||||
void operator []=(T index, bool value) {
|
||||
assert(index.index < _length);
|
||||
if (value)
|
||||
_bits = _bits | (1 << index.index);
|
||||
else
|
||||
_bits = _bits & ~(1 << index.index);
|
||||
}
|
||||
void reset([ bool value = false ]) {
|
||||
_bits = value ? _kAllOnes : _kAllZeros;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,11 +7,13 @@ import 'dart:ui' as ui;
|
|||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
|
||||
|
||||
import 'box.dart';
|
||||
import 'debug.dart';
|
||||
import 'object.dart';
|
||||
import 'view.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
export 'package:flutter/gestures.dart' show HitTestResult;
|
||||
|
||||
|
@ -39,6 +41,8 @@ abstract class Renderer extends Scheduler
|
|||
if (renderView == null) {
|
||||
renderView = new RenderView();
|
||||
renderView.scheduleInitialFrame();
|
||||
if (_semanticsClient != null)
|
||||
renderView.scheduleInitialSemantics();
|
||||
}
|
||||
handleMetricsChanged(); // configures renderView's metrics
|
||||
}
|
||||
|
@ -61,6 +65,14 @@ abstract class Renderer extends Scheduler
|
|||
renderView.configuration = new ViewConfiguration(size: ui.window.size);
|
||||
}
|
||||
|
||||
mojom.SemanticsClient _semanticsClient;
|
||||
void setSemanticsClient(mojom.SemanticsClient client) {
|
||||
assert(_semanticsClient == null);
|
||||
_semanticsClient = client;
|
||||
if (renderView != null)
|
||||
renderView.scheduleInitialSemantics();
|
||||
}
|
||||
|
||||
void _handlePersistentFrameCallback(Duration timeStamp) {
|
||||
beginFrame();
|
||||
}
|
||||
|
@ -71,7 +83,11 @@ abstract class Renderer extends Scheduler
|
|||
RenderObject.flushLayout();
|
||||
RenderObject.flushCompositingBits();
|
||||
RenderObject.flushPaint();
|
||||
renderView.compositeFrame();
|
||||
renderView.compositeFrame(); // this sends the bits to the GPU
|
||||
if (_semanticsClient != null) {
|
||||
RenderObject.flushSemantics();
|
||||
SemanticsNode.sendSemanticsTreeTo(_semanticsClient);
|
||||
}
|
||||
}
|
||||
|
||||
void hitTest(HitTestResult result, Point position) {
|
||||
|
@ -91,6 +107,13 @@ void debugDumpLayerTree() {
|
|||
debugPrint(Renderer.instance?.renderView?.layer?.toStringDeep());
|
||||
}
|
||||
|
||||
/// Prints a textual representation of the entire semantics tree.
|
||||
/// This will only work if there is a semantics client attached.
|
||||
/// Otherwise, the tree is empty and this will print "null".
|
||||
void debugDumpSemanticsTree() {
|
||||
debugPrint(Renderer.instance?.renderView?.debugSemantics?.toStringDeep());
|
||||
}
|
||||
|
||||
/// A concrete binding for applications that use the Rendering framework
|
||||
/// directly. This is the glue that binds the framework to the Flutter engine.
|
||||
class RenderingFlutterBinding extends BindingBase with Scheduler, Gesturer, Renderer {
|
||||
|
|
|
@ -341,6 +341,7 @@ class RenderBlockViewport extends RenderBlockBase {
|
|||
if (value != _startOffset) {
|
||||
_startOffset = value;
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -430,6 +431,8 @@ class RenderBlockViewport extends RenderBlockBase {
|
|||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
|
||||
|
||||
bool hitTestChildren(HitTestResult result, { Point position }) {
|
||||
if (isVertical)
|
||||
return defaultHitTestChildren(result, position: position + new Offset(0.0, -startOffset));
|
||||
|
|
|
@ -450,6 +450,8 @@ abstract class RenderBox extends RenderObject {
|
|||
assert(debugDoesMeetConstraints());
|
||||
}
|
||||
|
||||
Rect get semanticBounds => Point.origin & size;
|
||||
|
||||
void debugResetSize() {
|
||||
// updates the value of size._canBeUsedByParent if necessary
|
||||
size = size;
|
||||
|
@ -627,12 +629,6 @@ abstract class RenderBox extends RenderObject {
|
|||
/// visually "on top" (i.e., paints later).
|
||||
bool hitTestChildren(HitTestResult result, { Point position }) => false;
|
||||
|
||||
static Point _transformPoint(Matrix4 transform, Point point) {
|
||||
Vector3 position3 = new Vector3(point.x, point.y, 0.0);
|
||||
Vector3 transformed3 = transform.transform3(position3);
|
||||
return new Point(transformed3.x, transformed3.y);
|
||||
}
|
||||
|
||||
/// Multiply the transform from the parent's coordinate system to this box's
|
||||
/// coordinate system into the given transform.
|
||||
///
|
||||
|
@ -666,7 +662,7 @@ abstract class RenderBox extends RenderObject {
|
|||
double det = transform.invert();
|
||||
if (det == 0.0)
|
||||
return Point.origin;
|
||||
return _transformPoint(transform, point);
|
||||
return MatrixUtils.transformPoint(transform, point);
|
||||
}
|
||||
|
||||
/// Convert the given point from the local coordinate system for this box to
|
||||
|
@ -678,7 +674,7 @@ abstract class RenderBox extends RenderObject {
|
|||
Matrix4 transform = new Matrix4.identity();
|
||||
for (int index = renderers.length - 1; index > 0; index -= 1)
|
||||
renderers[index].applyPaintTransform(renderers[index - 1], transform);
|
||||
return _transformPoint(transform, point);
|
||||
return MatrixUtils.transformPoint(transform, point);
|
||||
}
|
||||
|
||||
/// Returns a rectangle that contains all the pixels painted by this box.
|
||||
|
|
|
@ -48,7 +48,7 @@ bool debugPaintLayerBordersEnabled = false;
|
|||
/// The color to use when painting Layer borders.
|
||||
ui.Color debugPaintLayerBordersColor = const ui.Color(0xFFFF9800);
|
||||
|
||||
/// Causes RenderBox objects to flash while they are being tapped
|
||||
/// Causes RenderBox objects to flash while they are being tapped.
|
||||
bool debugPaintPointersEnabled = false;
|
||||
|
||||
/// The color to use when reporting pointers.
|
||||
|
|
|
@ -169,11 +169,14 @@ class RenderEditableLine extends RenderBox {
|
|||
}
|
||||
}
|
||||
|
||||
bool get _hasVisualOverflow => _contentSize.width > size.width;
|
||||
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
final bool hasVisualOverflow = (_contentSize.width > size.width);
|
||||
if (hasVisualOverflow)
|
||||
if (_hasVisualOverflow)
|
||||
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
|
||||
else
|
||||
_paintContents(context, offset);
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
|
||||
}
|
||||
|
|
|
@ -587,6 +587,8 @@ class RenderFlex extends RenderBox with ContainerRenderObjectMixin<RenderBox, Fl
|
|||
});
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => _overflow > 0.0 ? Point.origin & size : null;
|
||||
|
||||
String toString() {
|
||||
String header = super.toString();
|
||||
if (_overflow is double && _overflow > 0.0)
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'dart:developer';
|
|||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
@ -13,6 +14,8 @@ import 'package:vector_math/vector_math_64.dart';
|
|||
import 'debug.dart';
|
||||
import 'layer.dart';
|
||||
import 'node.dart';
|
||||
import 'semantics.dart';
|
||||
import 'binding.dart';
|
||||
|
||||
export 'layer.dart';
|
||||
export 'package:flutter/gestures.dart' show HitTestEntry, HitTestResult;
|
||||
|
@ -361,6 +364,281 @@ typedef void RenderingExceptionHandler(RenderObject source, String method, dynam
|
|||
/// information will be printed to the console instead.
|
||||
RenderingExceptionHandler debugRenderingExceptionHandler;
|
||||
|
||||
class _SemanticsGeometry {
|
||||
_SemanticsGeometry() : transform = new Matrix4.identity();
|
||||
_SemanticsGeometry.withClipFrom(_SemanticsGeometry other) {
|
||||
clipRect = other?.clipRect;
|
||||
transform = new Matrix4.identity();
|
||||
}
|
||||
_SemanticsGeometry.copy(_SemanticsGeometry other) {
|
||||
if (other != null) {
|
||||
clipRect = other.clipRect;
|
||||
transform = new Matrix4.copy(other.transform);
|
||||
} else {
|
||||
transform = new Matrix4.identity();
|
||||
}
|
||||
}
|
||||
Rect clipRect;
|
||||
Rect _intersectClipRect(Rect other) {
|
||||
if (clipRect == null)
|
||||
return other;
|
||||
if (other == null)
|
||||
return clipRect;
|
||||
return clipRect.intersect(other);
|
||||
}
|
||||
Matrix4 transform;
|
||||
void applyAncestorChain(List<RenderObject> ancestorChain) {
|
||||
for (int index = ancestorChain.length-1; index > 0; index -= 1) {
|
||||
RenderObject parent = ancestorChain[index];
|
||||
RenderObject child = ancestorChain[index-1];
|
||||
clipRect = _intersectClipRect(parent.describeApproximatePaintClip(child));
|
||||
if (clipRect != null) {
|
||||
if (clipRect.isEmpty) {
|
||||
clipRect = Rect.zero;
|
||||
} else {
|
||||
Matrix4 clipTransform = new Matrix4.identity();
|
||||
parent.applyPaintTransform(child, clipTransform);
|
||||
clipRect = MatrixUtils.transformRect(clipRect, clipTransform);
|
||||
}
|
||||
}
|
||||
parent.applyPaintTransform(child, transform);
|
||||
}
|
||||
}
|
||||
void updateSemanticsNode({ RenderObject rendering, SemanticsNode semantics, SemanticsNode parentSemantics }) {
|
||||
assert(rendering != null);
|
||||
assert(semantics != null);
|
||||
assert(parentSemantics.wasAffectedByClip != null);
|
||||
semantics.transform = transform;
|
||||
if (clipRect != null) {
|
||||
semantics.rect = clipRect.intersect(rendering.semanticBounds);
|
||||
semantics.wasAffectedByClip = true;
|
||||
} else {
|
||||
semantics.rect = rendering.semanticBounds;
|
||||
semantics.wasAffectedByClip = parentSemantics?.wasAffectedByClip ?? false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _SemanticsFragment {
|
||||
_SemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<SemanticAnnotator> annotators,
|
||||
List<_SemanticsFragment> children
|
||||
}) {
|
||||
assert(owner != null);
|
||||
_ancestorChain = <RenderObject>[owner];
|
||||
if (annotators != null)
|
||||
addAnnotators(annotators);
|
||||
assert(() {
|
||||
if (children == null)
|
||||
return true;
|
||||
Set<_SemanticsFragment> seenChildren = new Set<_SemanticsFragment>();
|
||||
for (_SemanticsFragment child in children)
|
||||
assert(seenChildren.add(child)); // check for duplicate adds
|
||||
return true;
|
||||
});
|
||||
_children = children ?? const <_SemanticsFragment>[];
|
||||
}
|
||||
|
||||
List<RenderObject> _ancestorChain;
|
||||
void addAncestor(RenderObject ancestor) {
|
||||
_ancestorChain.add(ancestor);
|
||||
}
|
||||
|
||||
RenderObject get owner => _ancestorChain.first;
|
||||
|
||||
List<SemanticAnnotator> _annotators;
|
||||
void addAnnotators(Iterable<SemanticAnnotator> moreAnnotators) {
|
||||
if (_annotators == null)
|
||||
_annotators = moreAnnotators is List<SemanticAnnotator> ? moreAnnotators : moreAnnotators.toList();
|
||||
else
|
||||
_annotators.addAll(moreAnnotators);
|
||||
}
|
||||
|
||||
List<_SemanticsFragment> _children;
|
||||
|
||||
bool _debugCompiled = false;
|
||||
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics });
|
||||
|
||||
String toString() => '$runtimeType($hashCode)';
|
||||
}
|
||||
|
||||
/// Represents a subtree that doesn't need updating, it already has a
|
||||
/// SemanticsNode and isn't dirty. (We still update the matrix, since
|
||||
/// that comes from the (dirty) ancestors.)
|
||||
class _CleanSemanticsFragment extends _SemanticsFragment {
|
||||
_CleanSemanticsFragment({
|
||||
RenderObject owner
|
||||
}) : super(owner: owner) {
|
||||
assert(owner._semantics != null);
|
||||
}
|
||||
|
||||
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
|
||||
assert(!_debugCompiled);
|
||||
assert(() { _debugCompiled = true; return true; });
|
||||
SemanticsNode node = owner._semantics;
|
||||
assert(node != null);
|
||||
if (geometry != null) {
|
||||
geometry.applyAncestorChain(_ancestorChain);
|
||||
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
|
||||
} else {
|
||||
assert(_ancestorChain.length == 1);
|
||||
}
|
||||
yield node;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _InterestingSemanticsFragment extends _SemanticsFragment {
|
||||
_InterestingSemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<SemanticAnnotator> annotators,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(owner: owner, annotators: annotators, children: children);
|
||||
|
||||
bool get haveConcreteNode => true;
|
||||
|
||||
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
|
||||
assert(!_debugCompiled);
|
||||
assert(() { _debugCompiled = true; return true; });
|
||||
SemanticsNode node = establishSemanticsNode(geometry, currentSemantics, parentSemantics);
|
||||
for (SemanticAnnotator annotator in _annotators)
|
||||
annotator(node);
|
||||
for (_SemanticsFragment child in _children) {
|
||||
assert(child._ancestorChain.last == owner);
|
||||
node.addChildren(child.compile(
|
||||
geometry: createSemanticsGeometryForChild(geometry),
|
||||
currentSemantics: _children.length > 1 ? null : node,
|
||||
parentSemantics: node
|
||||
));
|
||||
}
|
||||
if (haveConcreteNode) {
|
||||
node.finalizeChildren();
|
||||
yield node;
|
||||
}
|
||||
}
|
||||
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics);
|
||||
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry);
|
||||
}
|
||||
|
||||
class _RootSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_RootSemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<SemanticAnnotator> annotators,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(owner: owner, annotators: annotators, children: children);
|
||||
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||
assert(_ancestorChain.length == 1);
|
||||
assert(geometry == null);
|
||||
assert(currentSemantics == null);
|
||||
assert(parentSemantics == null);
|
||||
owner._semantics ??= new SemanticsNode.root(
|
||||
handler: owner is SemanticActionHandler ? owner as dynamic : null
|
||||
);
|
||||
SemanticsNode node = owner._semantics;
|
||||
assert(MatrixUtils.matrixEquals(node.transform, new Matrix4.identity()));
|
||||
assert(!node.wasAffectedByClip);
|
||||
node.rect = owner.semanticBounds;
|
||||
return node;
|
||||
}
|
||||
|
||||
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
|
||||
return new _SemanticsGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
class _ConcreteSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_ConcreteSemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<SemanticAnnotator> annotators,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(owner: owner, annotators: annotators, children: children);
|
||||
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||
owner._semantics ??= new SemanticsNode(
|
||||
handler: owner is SemanticActionHandler ? owner as dynamic : null
|
||||
);
|
||||
SemanticsNode node = owner._semantics;
|
||||
if (geometry != null) {
|
||||
geometry.applyAncestorChain(_ancestorChain);
|
||||
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
|
||||
} else {
|
||||
assert(_ancestorChain.length == 1);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
|
||||
return new _SemanticsGeometry.withClipFrom(geometry);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImplicitSemanticsFragment extends _InterestingSemanticsFragment {
|
||||
_ImplicitSemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<SemanticAnnotator> annotators,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(owner: owner, annotators: annotators, children: children);
|
||||
|
||||
bool get haveConcreteNode => _haveConcreteNode;
|
||||
bool _haveConcreteNode;
|
||||
|
||||
SemanticsNode establishSemanticsNode(_SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics) {
|
||||
SemanticsNode node;
|
||||
assert(_haveConcreteNode == null);
|
||||
_haveConcreteNode = currentSemantics == null && _annotators.isNotEmpty;
|
||||
if (haveConcreteNode) {
|
||||
owner._semantics ??= new SemanticsNode(
|
||||
handler: owner is SemanticActionHandler ? owner as dynamic : null
|
||||
);
|
||||
node = owner._semantics;
|
||||
} else {
|
||||
owner._semantics = null;
|
||||
node = currentSemantics;
|
||||
}
|
||||
if (geometry != null) {
|
||||
geometry.applyAncestorChain(_ancestorChain);
|
||||
if (haveConcreteNode)
|
||||
geometry.updateSemanticsNode(rendering: owner, semantics: node, parentSemantics: parentSemantics);
|
||||
} else {
|
||||
assert(_ancestorChain.length == 1);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
_SemanticsGeometry createSemanticsGeometryForChild(_SemanticsGeometry geometry) {
|
||||
if (haveConcreteNode)
|
||||
return new _SemanticsGeometry.withClipFrom(geometry);
|
||||
return new _SemanticsGeometry.copy(geometry);
|
||||
}
|
||||
}
|
||||
|
||||
class _ForkingSemanticsFragment extends _SemanticsFragment {
|
||||
_ForkingSemanticsFragment({
|
||||
RenderObject owner,
|
||||
Iterable<_SemanticsFragment> children
|
||||
}) : super(owner: owner, children: children) {
|
||||
assert(children != null);
|
||||
assert(children.length > 1);
|
||||
}
|
||||
|
||||
Iterable<SemanticsNode> compile({ _SemanticsGeometry geometry, SemanticsNode currentSemantics, SemanticsNode parentSemantics }) sync* {
|
||||
assert(!_debugCompiled);
|
||||
assert(() { _debugCompiled = true; return true; });
|
||||
assert(geometry != null);
|
||||
geometry.applyAncestorChain(_ancestorChain);
|
||||
for (_SemanticsFragment child in _children) {
|
||||
assert(child._ancestorChain.last == owner);
|
||||
yield* child.compile(
|
||||
geometry: new _SemanticsGeometry.copy(geometry),
|
||||
currentSemantics: null,
|
||||
parentSemantics: parentSemantics
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An object in the render tree.
|
||||
///
|
||||
/// Render objects have a reference to their parent but do not commit to a model
|
||||
|
@ -448,16 +726,17 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|||
debugPrint('This RenderObject had the following owner:\n$debugOwner');
|
||||
int depth = 0;
|
||||
List<String> descendants = <String>[];
|
||||
const int maxDepth = 5;
|
||||
void visitor(RenderObject child) {
|
||||
descendants.add('${" " * depth}$child');
|
||||
depth += 1;
|
||||
if (depth < 5)
|
||||
if (depth < maxDepth)
|
||||
child.visitChildren(visitor);
|
||||
depth -= 1;
|
||||
}
|
||||
visitChildren(visitor);
|
||||
if (descendants.length > 1) {
|
||||
debugPrint('This RenderObject had the following descendants:');
|
||||
debugPrint('This RenderObject had the following descendants (showing up to depth $maxDepth):');
|
||||
} else if (descendants.length == 1) {
|
||||
debugPrint('This RenderObject had the following child:');
|
||||
} else {
|
||||
|
@ -651,6 +930,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|||
});
|
||||
try {
|
||||
performLayout();
|
||||
markNeedsSemanticsUpdate();
|
||||
} catch (e, stack) {
|
||||
_debugReportException('performLayout', e, stack);
|
||||
}
|
||||
|
@ -744,6 +1024,7 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|||
});
|
||||
try {
|
||||
performLayout();
|
||||
markNeedsSemanticsUpdate();
|
||||
assert(debugDoesMeetConstraints());
|
||||
} catch (e, stack) {
|
||||
_debugReportException('performLayout', e, stack);
|
||||
|
@ -1120,6 +1401,240 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
|
|||
assert(child.parent == this);
|
||||
}
|
||||
|
||||
/// Returns a rect in this object's coordinate system that describes
|
||||
/// the approximate bounding box of the clip rect that would be
|
||||
/// applied to the given child during the paint phase, if any.
|
||||
///
|
||||
/// Returns null if the child would not be clipped.
|
||||
///
|
||||
/// This is used in the semantics phase to avoid including children
|
||||
/// that are not physically visible.
|
||||
Rect describeApproximatePaintClip(RenderObject child) => null;
|
||||
|
||||
|
||||
// SEMANTICS
|
||||
|
||||
static bool _semanticsEnabled = false;
|
||||
static bool _debugDoingSemantics = false;
|
||||
static List<RenderObject> _nodesNeedingSemantics = <RenderObject>[];
|
||||
|
||||
/// Bootstrap the semantics reporting mechanism by marking this node
|
||||
/// as needing a semantics update.
|
||||
///
|
||||
/// Requires that this render object is attached, and is the root of
|
||||
/// the render tree.
|
||||
///
|
||||
/// See [Renderer] for an example of how this function is used.
|
||||
void scheduleInitialSemantics() {
|
||||
assert(attached);
|
||||
assert(parent is! RenderObject);
|
||||
assert(!_debugDoingSemantics);
|
||||
assert(_semantics == null);
|
||||
assert(_needsSemanticsUpdate);
|
||||
assert(_semanticsEnabled == false);
|
||||
_semanticsEnabled = true;
|
||||
_nodesNeedingSemantics.add(this);
|
||||
Scheduler.instance.ensureVisualUpdate();
|
||||
}
|
||||
|
||||
static void flushSemantics() {
|
||||
Timeline.startSync('Semantics');
|
||||
assert(_semanticsEnabled);
|
||||
assert(() { _debugDoingSemantics = true; return true; });
|
||||
try {
|
||||
_nodesNeedingSemantics.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
|
||||
for (RenderObject node in _nodesNeedingSemantics) {
|
||||
if (node._needsSemanticsUpdate)
|
||||
node._updateSemantics();
|
||||
}
|
||||
} finally {
|
||||
_nodesNeedingSemantics.clear();
|
||||
assert(() { _debugDoingSemantics = false; return true; });
|
||||
Timeline.finishSync();
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this RenderObject introduces a new box for accessibility purposes.
|
||||
bool get hasSemantics => false;
|
||||
|
||||
/// The bounding box, in the local coordinate system, of this
|
||||
/// object, for accessibility purposes.
|
||||
Rect get semanticBounds;
|
||||
|
||||
bool _needsSemanticsUpdate = true;
|
||||
bool _needsSemanticsGeometryUpdate = true;
|
||||
SemanticsNode _semantics;
|
||||
|
||||
SemanticsNode get debugSemantics { // only exposed for testing and debugging
|
||||
SemanticsNode result;
|
||||
assert(() {
|
||||
result = _semantics;
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Mark this node as needing an update to its semantics
|
||||
/// description.
|
||||
///
|
||||
/// If the change did not involve a removal or addition of
|
||||
/// semantics, only the change of semantics (e.g. isChecked changing
|
||||
/// from true to false, as opposed to isChecked changing from being
|
||||
/// true to not being changed at all), then you can pass the
|
||||
/// onlyChanges argument with the value true to reduce the cost. If
|
||||
/// semantics are being added or removed, more work needs to be done
|
||||
/// to update the semantics tree. If you pass 'onlyChanges: true'
|
||||
/// but this node, which previously had a SemanticsNode, no longer
|
||||
/// has one, or previously did not set any semantics, but now does,
|
||||
/// or previously had a child that returned annotators, but no
|
||||
/// longer does, or other such combinations, then you will either
|
||||
/// assert during the subsequent call to [flushSemantics()] or you
|
||||
/// will have out-of-date information in the semantics tree.
|
||||
///
|
||||
/// If the geometry might have changed in any way, then again, more
|
||||
/// work needs to be done to update the semantics tree (to deal with
|
||||
/// clips). You can pass the noGeometry argument to avoid this work
|
||||
/// in the case where only the labels or flags changed. If you pass
|
||||
/// 'noGeometry: true' when the geometry did change, the semantic
|
||||
/// tree will be out of date.
|
||||
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
|
||||
assert(!_debugDoingSemantics);
|
||||
if (!_semanticsEnabled || !attached || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
|
||||
return;
|
||||
if (!noGeometry && (_semantics == null || (_semantics.hasChildren && _semantics.wasAffectedByClip))) {
|
||||
// Since the geometry might have changed, we need to make sure to reapply any clips.
|
||||
_needsSemanticsGeometryUpdate = true;
|
||||
}
|
||||
if (onlyChanges) {
|
||||
// The shape of the tree didn't change, but the details did.
|
||||
// If we have our own SemanticsNode (our _semantics isn't null)
|
||||
// then mark ourselves dirty. If we don't then we are using an
|
||||
// ancestor's; mark all the nodes up to that one dirty.
|
||||
RenderObject node = this;
|
||||
while (node._semantics == null && node.parent is RenderObject) {
|
||||
if (node._needsSemanticsUpdate)
|
||||
return;
|
||||
node._needsSemanticsUpdate = true;
|
||||
node = node.parent;
|
||||
}
|
||||
if (!node._needsSemanticsUpdate) {
|
||||
node._needsSemanticsUpdate = true;
|
||||
_nodesNeedingSemantics.add(node);
|
||||
}
|
||||
} else {
|
||||
// The shape of the semantics tree around us may have changed.
|
||||
// The worst case is that we may have removed a branch of the
|
||||
// semantics tree, because when that happens we have to go up
|
||||
// and dirty the nearest _semantics-laden ancestor of the
|
||||
// affected node to rebuild the tree.
|
||||
RenderObject node = this;
|
||||
do {
|
||||
if (node.parent is! RenderObject)
|
||||
break;
|
||||
node._needsSemanticsUpdate = true;
|
||||
node._semantics?.reset();
|
||||
node = node.parent;
|
||||
} while (node._semantics == null);
|
||||
node._semantics?.reset();
|
||||
if (!node._needsSemanticsUpdate) {
|
||||
node._needsSemanticsUpdate = true;
|
||||
_nodesNeedingSemantics.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSemantics() {
|
||||
try {
|
||||
assert(_needsSemanticsUpdate);
|
||||
assert(_semantics != null || parent is! RenderObject);
|
||||
_SemanticsFragment fragment = _getSemanticsFragment();
|
||||
assert(fragment is _InterestingSemanticsFragment);
|
||||
SemanticsNode node = fragment.compile(parentSemantics: _semantics?.parent).single;
|
||||
assert(node != null);
|
||||
assert(node == _semantics);
|
||||
} catch (e, stack) {
|
||||
_debugReportException('_updateSemantics', e, stack);
|
||||
}
|
||||
}
|
||||
|
||||
_SemanticsFragment _getSemanticsFragment() {
|
||||
// early-exit if we're not dirty and have our own semantics
|
||||
if (!_needsSemanticsUpdate && hasSemantics) {
|
||||
assert(_semantics != null);
|
||||
return new _CleanSemanticsFragment(owner: this);
|
||||
}
|
||||
List<_SemanticsFragment> children;
|
||||
visitChildrenForSemantics((RenderObject child) {
|
||||
if (_needsSemanticsGeometryUpdate) {
|
||||
// If our geometry changed, make sure the child also does a
|
||||
// full update so that any changes to the clip are fully
|
||||
// applied.
|
||||
child._needsSemanticsUpdate = true;
|
||||
child._needsSemanticsGeometryUpdate = true;
|
||||
}
|
||||
_SemanticsFragment fragment = child._getSemanticsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.addAncestor(this);
|
||||
children ??= <_SemanticsFragment>[];
|
||||
assert(!children.contains(fragment));
|
||||
children.add(fragment);
|
||||
}
|
||||
});
|
||||
_needsSemanticsUpdate = false;
|
||||
_needsSemanticsGeometryUpdate = false;
|
||||
Iterable<SemanticAnnotator> annotators = getSemanticAnnotators();
|
||||
if (parent is! RenderObject)
|
||||
return new _RootSemanticsFragment(owner: this, annotators: annotators, children: children);
|
||||
if (hasSemantics)
|
||||
return new _ConcreteSemanticsFragment(owner: this, annotators: annotators, children: children);
|
||||
if (annotators.isNotEmpty)
|
||||
return new _ImplicitSemanticsFragment(owner: this, annotators: annotators, children: children);
|
||||
_semantics = null;
|
||||
if (children == null)
|
||||
return null;
|
||||
if (children.length > 1)
|
||||
return new _ForkingSemanticsFragment(owner: this, children: children);
|
||||
assert(children.length == 1);
|
||||
return children.single;
|
||||
}
|
||||
|
||||
/// Called when collecting the semantics of this node. Subclasses
|
||||
/// that have children that are not semantically relevant (e.g.
|
||||
/// because they are invisible) should skip those children here.
|
||||
///
|
||||
/// The default implementation mirrors the behavior of
|
||||
/// [visitChildren()] (which is supposed to walk all the children).
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||
visitChildren(visitor);
|
||||
}
|
||||
|
||||
/// Returns functions that will annotate a SemanticsNode with the
|
||||
/// semantics of this RenderObject.
|
||||
///
|
||||
/// To annotate a SemanticsNode for this node, return all the
|
||||
/// annotators provided by the superclass, plus an annotator that
|
||||
/// adds the annotations. When the behavior of the annotators would
|
||||
/// change (e.g. the box is now checked rather than unchecked), call
|
||||
/// [markNeedsSemanticsUpdate()] to indicate to the rendering system
|
||||
/// that the semantics tree needs to be rebuilt.
|
||||
///
|
||||
/// To introduce a new SemanticsNode, set hasSemantics to true for
|
||||
/// this object. The functions returned by this function will be used
|
||||
/// to annotate the SemanticsNode for this object.
|
||||
///
|
||||
/// Semantic annotations are persistent. Values set in one pass will
|
||||
/// still be set in the next pass. Therefore it is important to
|
||||
/// explicitly set fields to false once they are no longer true --
|
||||
/// setting them to true when they are to be enabled, and not
|
||||
/// setting them at all when they are not, will mean they remain set
|
||||
/// once enabled once and will never get unset.
|
||||
///
|
||||
/// If the number of annotators you return will change from zero to
|
||||
/// non-zero, and hasSemantics isn't true, then the associated call
|
||||
/// to markNeedsSemanticsUpdate() must not have 'onlyChanges' set, as
|
||||
/// it is possible that the node should be entirely removed.
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* { }
|
||||
|
||||
|
||||
// EVENTS
|
||||
|
||||
|
|
|
@ -236,4 +236,5 @@ class RenderOffStage extends RenderBox with RenderObjectWithChildMixin<RenderBox
|
|||
|
||||
bool hitTest(HitTestResult result, { Point position }) => false;
|
||||
void paint(PaintingContext context, Offset offset) { }
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) { }
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/painting.dart';
|
|||
|
||||
import 'box.dart';
|
||||
import 'object.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
export 'package:flutter/painting.dart' show
|
||||
FontStyle,
|
||||
|
@ -111,7 +112,11 @@ class RenderParagraph extends RenderBox {
|
|||
_textPainter.paint(context.canvas, offset);
|
||||
}
|
||||
|
||||
// we should probably expose a way to do precise (inter-glpyh) hit testing
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
yield (SemanticsNode node) {
|
||||
node.label = text.toPlainText();
|
||||
};
|
||||
}
|
||||
|
||||
String debugDescribeChildren(String prefix) {
|
||||
return '$prefix \u2558\u2550\u2566\u2550\u2550 text \u2550\u2550\u2550\n'
|
||||
|
|
|
@ -11,6 +11,7 @@ import 'package:vector_math/vector_math_64.dart';
|
|||
import 'box.dart';
|
||||
import 'debug.dart';
|
||||
import 'object.dart';
|
||||
import 'semantics.dart';
|
||||
|
||||
export 'package:flutter/gestures.dart' show
|
||||
PointerEvent,
|
||||
|
@ -557,6 +558,7 @@ class RenderOpacity extends RenderProxyBox {
|
|||
_alpha = _getAlphaFromOpacity(_opacity);
|
||||
markNeedsCompositingBitsUpdate();
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
int _alpha;
|
||||
|
@ -574,6 +576,11 @@ class RenderOpacity extends RenderProxyBox {
|
|||
}
|
||||
}
|
||||
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||
if (child != null && _alpha != 0)
|
||||
visitor(child);
|
||||
}
|
||||
|
||||
void debugDescribeSettings(List<String> settings) {
|
||||
super.debugDescribeSettings(settings);
|
||||
settings.add('opacity: ${opacity.toStringAsFixed(1)}');
|
||||
|
@ -622,6 +629,17 @@ abstract class CustomClipper<T> {
|
|||
/// Returns a description of the clip given that the render object being
|
||||
/// clipped is of the given size.
|
||||
T getClip(Size size);
|
||||
/// Returns an approximation of the clip returned by [getClip], as
|
||||
/// an axis-aligned Rect. This is used by the semantics layer to
|
||||
/// determine whether widgets should be excluded.
|
||||
///
|
||||
/// By default, this returns a rectangle that is the same size as
|
||||
/// the RenderObject. If getClip returns a shape that is roughly the
|
||||
/// same size as the RenderObject (e.g. it's a rounded rectangle
|
||||
/// with very small arcs in the corners), then this may be adequate.
|
||||
Rect getApproximateClipRect(Size size) => Point.origin & size;
|
||||
/// Returns true if the new instance will result in a different clip
|
||||
/// than the oldClipper instance.
|
||||
bool shouldRepaint(CustomClipper oldClipper);
|
||||
}
|
||||
|
||||
|
@ -642,15 +660,19 @@ abstract class _RenderCustomClip<T> extends RenderProxyBox {
|
|||
if (newClipper == null) {
|
||||
assert(oldClipper != null);
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate(onlyChanges: true);
|
||||
} else if (oldClipper == null ||
|
||||
oldClipper.runtimeType != oldClipper.runtimeType ||
|
||||
newClipper.shouldRepaint(oldClipper)) {
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate(onlyChanges: true);
|
||||
}
|
||||
}
|
||||
|
||||
T get _defaultClip;
|
||||
T get _clip => _clipper?.getClip(size) ?? _defaultClip;
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => _clipper?.getApproximateClipRect(size) ?? Point.origin & size;
|
||||
}
|
||||
|
||||
/// Clips its child using a rectangle.
|
||||
|
@ -722,6 +744,9 @@ class RenderClipRRect extends RenderProxyBox {
|
|||
markNeedsPaint();
|
||||
}
|
||||
|
||||
// TODO(ianh): either convert this to the CustomClipper world, or
|
||||
// TODO(ianh): implement describeApproximatePaintClip for this class
|
||||
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
Rect rect = Point.origin & size;
|
||||
|
@ -1314,18 +1339,59 @@ class RenderRepaintBoundary extends RenderProxyBox {
|
|||
/// to hit testing. It still consumes space during layout and paints its child
|
||||
/// as usual. It just cannot be the target of located events because it returns
|
||||
/// false from [hitTest].
|
||||
///
|
||||
/// When [ignoringSemantics] is true, the subtree will be invisible to
|
||||
/// the semantics layer (and thus e.g. accessibility tools). If
|
||||
/// [ignoringSemantics] is null, it uses the value of [ignoring].
|
||||
class RenderIgnorePointer extends RenderProxyBox {
|
||||
RenderIgnorePointer({ RenderBox child, this.ignoring: true }) : super(child);
|
||||
RenderIgnorePointer({
|
||||
RenderBox child,
|
||||
bool ignoring: true,
|
||||
bool ignoringSemantics
|
||||
}) : _ignoring = ignoring, _ignoringSemantics = ignoringSemantics, super(child) {
|
||||
assert(_ignoring != null);
|
||||
}
|
||||
|
||||
bool ignoring;
|
||||
bool get ignoring => _ignoring;
|
||||
bool _ignoring;
|
||||
void set ignoring(bool value) {
|
||||
assert(value != null);
|
||||
if (value == _ignoring)
|
||||
return;
|
||||
_ignoring = value;
|
||||
if (ignoringSemantics == null)
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
bool get ignoringSemantics => _ignoringSemantics;
|
||||
bool _ignoringSemantics;
|
||||
void set ignoringSemantics(bool value) {
|
||||
if (value == _ignoringSemantics)
|
||||
return;
|
||||
bool oldEffectiveValue = _effectiveIgnoringSemantics;
|
||||
_ignoringSemantics = value;
|
||||
if (oldEffectiveValue != _effectiveIgnoringSemantics)
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
bool get _effectiveIgnoringSemantics => ignoringSemantics == null ? ignoring : ignoringSemantics;
|
||||
|
||||
bool hitTest(HitTestResult result, { Point position }) {
|
||||
return ignoring ? false : super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
// TODO(ianh): figure out a way to still include labels and flags in
|
||||
// descendants, just make them non-interactive, even when
|
||||
// _effectiveIgnoringSemantics is true
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) {
|
||||
if (child != null && !_effectiveIgnoringSemantics)
|
||||
visitor(child);
|
||||
}
|
||||
|
||||
void debugDescribeSettings(List<String> settings) {
|
||||
super.debugDescribeSettings(settings);
|
||||
settings.add('ignoring: $ignoring');
|
||||
settings.add('ignoringSemantics: ${ ignoringSemantics == null ? "implicitly " : "" }$_effectiveIgnoringSemantics');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1336,3 +1402,220 @@ class RenderMetaData extends RenderProxyBox {
|
|||
/// Opaque meta data ignored by the render tree
|
||||
dynamic metaData;
|
||||
}
|
||||
|
||||
/// Listens for the specified gestures from the semantics server (e.g.
|
||||
/// an accessibility tool).
|
||||
class RenderSemanticsGestureHandler extends RenderProxyBox implements SemanticActionHandler {
|
||||
RenderSemanticsGestureHandler({
|
||||
RenderBox child,
|
||||
GestureTapCallback onTap,
|
||||
GestureLongPressCallback onLongPress,
|
||||
GestureDragUpdateCallback onHorizontalDragUpdate,
|
||||
GestureDragUpdateCallback onVerticalDragUpdate,
|
||||
this.scrollFactor: 0.8
|
||||
}) : _onTap = onTap,
|
||||
_onLongPress = onLongPress,
|
||||
_onHorizontalDragUpdate = onHorizontalDragUpdate,
|
||||
_onVerticalDragUpdate = onVerticalDragUpdate,
|
||||
super(child);
|
||||
|
||||
GestureTapCallback get onTap => _onTap;
|
||||
GestureTapCallback _onTap;
|
||||
void set onTap(GestureTapCallback value) {
|
||||
if (_onTap == value)
|
||||
return;
|
||||
bool didHaveSemantics = hasSemantics;
|
||||
bool hadHandler = _onTap != null;
|
||||
_onTap = value;
|
||||
if ((value != null) != hadHandler)
|
||||
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
|
||||
}
|
||||
|
||||
GestureLongPressCallback get onLongPress => _onLongPress;
|
||||
GestureLongPressCallback _onLongPress;
|
||||
void set onLongPress(GestureLongPressCallback value) {
|
||||
if (_onLongPress == value)
|
||||
return;
|
||||
bool didHaveSemantics = hasSemantics;
|
||||
bool hadHandler = _onLongPress != null;
|
||||
_onLongPress = value;
|
||||
if ((value != null) != hadHandler)
|
||||
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
|
||||
}
|
||||
|
||||
GestureDragUpdateCallback get onHorizontalDragUpdate => _onHorizontalDragUpdate;
|
||||
GestureDragUpdateCallback _onHorizontalDragUpdate;
|
||||
void set onHorizontalDragUpdate(GestureDragUpdateCallback value) {
|
||||
if (_onHorizontalDragUpdate == value)
|
||||
return;
|
||||
bool didHaveSemantics = hasSemantics;
|
||||
bool hadHandler = _onHorizontalDragUpdate != null;
|
||||
_onHorizontalDragUpdate = value;
|
||||
if ((value != null) != hadHandler)
|
||||
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
|
||||
}
|
||||
|
||||
GestureDragUpdateCallback get onVerticalDragUpdate => _onVerticalDragUpdate;
|
||||
GestureDragUpdateCallback _onVerticalDragUpdate;
|
||||
void set onVerticalDragUpdate(GestureDragUpdateCallback value) {
|
||||
if (_onVerticalDragUpdate == value)
|
||||
return;
|
||||
bool didHaveSemantics = hasSemantics;
|
||||
bool hadHandler = _onVerticalDragUpdate != null;
|
||||
_onVerticalDragUpdate = value;
|
||||
if ((value != null) != hadHandler)
|
||||
markNeedsSemanticsUpdate(onlyChanges: hasSemantics == didHaveSemantics);
|
||||
}
|
||||
|
||||
/// The fraction of the dimension of this render box to use when
|
||||
/// scrolling. For example, if this is 0.8 and the box is 200 pixels
|
||||
/// wide, then when a left-scroll action is received from the
|
||||
/// accessibility system, it will translate into a 160 pixel
|
||||
/// leftwards drag.
|
||||
double scrollFactor;
|
||||
|
||||
bool get hasSemantics {
|
||||
return onTap != null
|
||||
|| onLongPress != null
|
||||
|| onHorizontalDragUpdate != null
|
||||
|| onVerticalDragUpdate != null;
|
||||
}
|
||||
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
if (hasSemantics) {
|
||||
yield (SemanticsNode semantics) {
|
||||
semantics.canBeTapped = onTap != null;
|
||||
semantics.canBeLongPressed = onLongPress != null;
|
||||
semantics.canBeScrolledHorizontally = onHorizontalDragUpdate != null;
|
||||
semantics.canBeScrolledVertically = onVerticalDragUpdate != null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
void handleSemanticTap() {
|
||||
if (onTap != null)
|
||||
onTap();
|
||||
}
|
||||
|
||||
void handleSemanticLongPress() {
|
||||
if (onLongPress != null)
|
||||
onLongPress();
|
||||
}
|
||||
|
||||
void handleSemanticScrollLeft() {
|
||||
if (onHorizontalDragUpdate != null)
|
||||
onHorizontalDragUpdate(size.width * -scrollFactor);
|
||||
}
|
||||
|
||||
void handleSemanticScrollRight() {
|
||||
if (onHorizontalDragUpdate != null)
|
||||
onHorizontalDragUpdate(size.width * scrollFactor);
|
||||
}
|
||||
|
||||
void handleSemanticScrollUp() {
|
||||
if (onVerticalDragUpdate != null)
|
||||
onVerticalDragUpdate(size.height * -scrollFactor);
|
||||
}
|
||||
|
||||
void handleSemanticScrollDown() {
|
||||
if (onVerticalDragUpdate != null)
|
||||
onVerticalDragUpdate(size.height * scrollFactor);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add annotations to the SemanticsNode for this subtree.
|
||||
class RenderSemanticAnnotations extends RenderProxyBox {
|
||||
RenderSemanticAnnotations({
|
||||
RenderBox child,
|
||||
bool container: false,
|
||||
bool checked,
|
||||
String label
|
||||
}) : _container = container,
|
||||
_checked = checked,
|
||||
_label = label,
|
||||
super(child) {
|
||||
assert(container != null);
|
||||
}
|
||||
|
||||
/// If 'container' is true, this RenderObject will introduce a new
|
||||
/// node in the semantics tree. Otherwise, the semantics will be
|
||||
/// merged with the semantics of any ancestors.
|
||||
///
|
||||
/// The 'container' flag is implicitly set to true on the immediate
|
||||
/// semantics-providing descendants of a node where multiple
|
||||
/// children have semantics or have descendants providing semantics.
|
||||
/// In other words, the semantics of siblings are not merged. To
|
||||
/// merge the semantics of an entire subtree, including siblings,
|
||||
/// you can use a [RenderMergeSemantics].
|
||||
bool get container => _container;
|
||||
bool _container;
|
||||
void set container(bool value) {
|
||||
assert(value != null);
|
||||
if (container == value)
|
||||
return;
|
||||
_container = value;
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// If non-null, sets the "hasCheckedState" semantic to true and the
|
||||
/// "isChecked" semantic to the given value.
|
||||
bool get checked => _checked;
|
||||
bool _checked;
|
||||
void set checked(bool value) {
|
||||
if (checked == value)
|
||||
return;
|
||||
bool hadValue = checked != null;
|
||||
_checked = value;
|
||||
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
|
||||
}
|
||||
|
||||
/// If non-null, sets the "label" semantic to the given value.
|
||||
String get label => _label;
|
||||
String _label;
|
||||
void set label(String value) {
|
||||
if (label == value)
|
||||
return;
|
||||
bool hadValue = label != null;
|
||||
_label = value;
|
||||
markNeedsSemanticsUpdate(onlyChanges: (value != null) == hadValue);
|
||||
}
|
||||
|
||||
bool get hasSemantics => container;
|
||||
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
if (checked != null) {
|
||||
yield (SemanticsNode semantics) {
|
||||
semantics.hasCheckedState = true;
|
||||
semantics.isChecked = checked;
|
||||
};
|
||||
}
|
||||
if (label != null) {
|
||||
yield (SemanticsNode semantics) {
|
||||
semantics.label = label;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes the semantics of all descendants to be merged into this
|
||||
/// node such that the entire subtree becomes a single leaf in the
|
||||
/// semantics tree.
|
||||
///
|
||||
/// Useful for combining the semantics of multiple render objects that
|
||||
/// form part of a single conceptual widget, e.g. a checkbox, a label,
|
||||
/// and the gesture detector that goes with them.
|
||||
class RenderMergeSemantics extends RenderProxyBox {
|
||||
RenderMergeSemantics({ RenderBox child }) : super(child);
|
||||
Iterable<SemanticAnnotator> getSemanticAnnotators() sync* {
|
||||
yield (SemanticsNode node) { node.mergeAllDescendantsIntoThisNode = true; };
|
||||
}
|
||||
}
|
||||
|
||||
/// Excludes this subtree from the semantic tree.
|
||||
///
|
||||
/// Useful e.g. for hiding text that is redundant with other text next
|
||||
/// to it (e.g. text included only for the visual effect).
|
||||
class RenderExcludeSemantics extends RenderProxyBox {
|
||||
RenderExcludeSemantics({ RenderBox child }) : super(child);
|
||||
void visitChildrenForSemantics(RenderObjectVisitor visitor) { }
|
||||
}
|
||||
|
|
448
packages/flutter/lib/src/rendering/semantics.dart
Normal file
448
packages/flutter/lib/src/rendering/semantics.dart
Normal file
|
@ -0,0 +1,448 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show Rect;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:sky_services/semantics/semantics.mojom.dart' as mojom;
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'node.dart';
|
||||
|
||||
/// The type of function returned by [RenderObject.getSemanticAnnotators()].
|
||||
///
|
||||
/// These callbacks are invoked with the [SemanticsNode] object that
|
||||
/// corresponds to the [RenderObject]. (One [SemanticsNode] can
|
||||
/// correspond to multiple [RenderObject] objects.)
|
||||
///
|
||||
/// See [RenderObject.getSemanticAnnotators()] for details on the
|
||||
/// contract that semantic annotators must follow.
|
||||
typedef void SemanticAnnotator(SemanticsNode semantics);
|
||||
|
||||
/// Interface for RenderObjects to implement when they want to support
|
||||
/// being tapped, etc.
|
||||
///
|
||||
/// These handlers will only be called if the relevant flag is set
|
||||
/// (e.g. handleSemanticTap() will only be called if canBeTapped is
|
||||
/// true, handleSemanticScrollDown() will only be called if
|
||||
/// canBeScrolledVertically is true, etc).
|
||||
abstract class SemanticActionHandler {
|
||||
void handleSemanticTap() { }
|
||||
void handleSemanticLongPress() { }
|
||||
void handleSemanticScrollLeft() { }
|
||||
void handleSemanticScrollRight() { }
|
||||
void handleSemanticScrollUp() { }
|
||||
void handleSemanticScrollDown() { }
|
||||
}
|
||||
|
||||
enum _SemanticFlags {
|
||||
mergeAllDescendantsIntoThisNode,
|
||||
canBeTapped,
|
||||
canBeLongPressed,
|
||||
canBeScrolledHorizontally,
|
||||
canBeScrolledVertically,
|
||||
hasCheckedState,
|
||||
isChecked,
|
||||
}
|
||||
|
||||
typedef bool SemanticsNodeVisitor(SemanticsNode node);
|
||||
|
||||
class SemanticsNode extends AbstractNode {
|
||||
SemanticsNode({
|
||||
SemanticActionHandler handler
|
||||
}) : _id = _generateNewId(),
|
||||
_actionHandler = handler;
|
||||
|
||||
SemanticsNode.root({
|
||||
SemanticActionHandler handler
|
||||
}) : _id = 0,
|
||||
_actionHandler = handler {
|
||||
attach();
|
||||
}
|
||||
|
||||
static int _lastIdentifier = 0;
|
||||
static int _generateNewId() {
|
||||
_lastIdentifier += 1;
|
||||
return _lastIdentifier;
|
||||
}
|
||||
|
||||
final int _id;
|
||||
final SemanticActionHandler _actionHandler;
|
||||
|
||||
|
||||
// GEOMETRY
|
||||
// These are automatically handled by RenderObject's own logic
|
||||
|
||||
Matrix4 get transform => _transform;
|
||||
Matrix4 _transform; // defaults to null, which we say means the identity matrix
|
||||
void set transform (Matrix4 value) {
|
||||
if (!MatrixUtils.matrixEquals(_transform, value)) {
|
||||
_transform = value;
|
||||
_markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
Rect get rect => _rect;
|
||||
Rect _rect = Rect.zero;
|
||||
void set rect (Rect value) {
|
||||
assert(value != null);
|
||||
if (_rect != value) {
|
||||
_rect = value;
|
||||
_markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
bool wasAffectedByClip = false;
|
||||
|
||||
|
||||
// FLAGS AND LABELS
|
||||
// These are supposed to be set by SemanticAnnotator obtained from getSemanticAnnotators
|
||||
|
||||
BitField<_SemanticFlags> _flags = new BitField<_SemanticFlags>.filled(_SemanticFlags.values.length, false);
|
||||
|
||||
void _setFlag(_SemanticFlags flag, bool value, { bool needsHandler: false }) {
|
||||
assert(value != null);
|
||||
assert((!needsHandler) || (_actionHandler != null) || (value == false));
|
||||
if (_flags[flag] != value) {
|
||||
_flags[flag] = value;
|
||||
_markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
bool _canHandle(_SemanticFlags flag) {
|
||||
return _actionHandler != null && _flags[flag];
|
||||
}
|
||||
|
||||
bool get mergeAllDescendantsIntoThisNode => _flags[_SemanticFlags.mergeAllDescendantsIntoThisNode];
|
||||
void set mergeAllDescendantsIntoThisNode(bool value) => _setFlag(_SemanticFlags.mergeAllDescendantsIntoThisNode, value);
|
||||
|
||||
bool get canBeTapped => _flags[_SemanticFlags.canBeTapped];
|
||||
void set canBeTapped(bool value) => _setFlag(_SemanticFlags.canBeTapped, value, needsHandler: true);
|
||||
|
||||
bool get canBeLongPressed => _flags[_SemanticFlags.canBeLongPressed];
|
||||
void set canBeLongPressed(bool value) => _setFlag(_SemanticFlags.canBeLongPressed, value, needsHandler: true);
|
||||
|
||||
bool get canBeScrolledHorizontally => _flags[_SemanticFlags.canBeScrolledHorizontally];
|
||||
void set canBeScrolledHorizontally(bool value) => _setFlag(_SemanticFlags.canBeScrolledHorizontally, value, needsHandler: true);
|
||||
|
||||
bool get canBeScrolledVertically => _flags[_SemanticFlags.canBeScrolledVertically];
|
||||
void set canBeScrolledVertically(bool value) => _setFlag(_SemanticFlags.canBeScrolledVertically, value, needsHandler: true);
|
||||
|
||||
bool get hasCheckedState => _flags[_SemanticFlags.hasCheckedState];
|
||||
void set hasCheckedState(bool value) => _setFlag(_SemanticFlags.hasCheckedState, value);
|
||||
|
||||
bool get isChecked => _flags[_SemanticFlags.isChecked];
|
||||
void set isChecked(bool value) => _setFlag(_SemanticFlags.isChecked, value);
|
||||
|
||||
String get label => _label;
|
||||
String _label = '';
|
||||
void set label(String value) {
|
||||
assert(value != null);
|
||||
if (_label != value) {
|
||||
_label = value;
|
||||
_markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_flags.reset();
|
||||
_label = '';
|
||||
_markDirty();
|
||||
}
|
||||
|
||||
List<SemanticsNode> _newChildren;
|
||||
void addChildren(Iterable<SemanticsNode> children) {
|
||||
_newChildren ??= <SemanticsNode>[];
|
||||
_newChildren.addAll(children);
|
||||
// we do the asserts afterwards because children is an Iterable
|
||||
// and doing the asserts before would mean the behaviour is
|
||||
// different in checked mode vs release mode (if you walk an
|
||||
// iterator after having reached the end, it'll just start over;
|
||||
// the values are not cached).
|
||||
assert(!_newChildren.any((SemanticsNode child) => child == this));
|
||||
assert(() {
|
||||
SemanticsNode ancestor = this;
|
||||
while (ancestor.parent is SemanticsNode)
|
||||
ancestor = ancestor.parent;
|
||||
assert(!_newChildren.any((SemanticsNode child) => child == ancestor));
|
||||
return true;
|
||||
});
|
||||
assert(() {
|
||||
Set<SemanticsNode> seenChildren = new Set<SemanticsNode>();
|
||||
for (SemanticsNode child in _newChildren)
|
||||
assert(seenChildren.add(child)); // check for duplicate adds
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
List<SemanticsNode> _children;
|
||||
bool get hasChildren => _children?.isNotEmpty ?? false;
|
||||
bool _dead = false;
|
||||
void finalizeChildren() {
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children)
|
||||
child._dead = true;
|
||||
}
|
||||
if (_newChildren != null) {
|
||||
for (SemanticsNode child in _newChildren)
|
||||
child._dead = false;
|
||||
}
|
||||
bool sawChange = false;
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children) {
|
||||
if (child._dead) {
|
||||
if (child.parent == this) {
|
||||
// we might have already had our child stolen from us by
|
||||
// another node that is deeper in the tree.
|
||||
dropChild(child);
|
||||
}
|
||||
sawChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_newChildren != null) {
|
||||
for (SemanticsNode child in _newChildren) {
|
||||
if (child.parent != this) {
|
||||
if (child.parent != null) {
|
||||
// we're rebuilding the tree from the bottom up, so it's possible
|
||||
// that our child was, in the last pass, a child of one of our
|
||||
// ancestors. In that case, we drop the child eagerly here.
|
||||
// TODO(ianh): Find a way to assert that the same node didn't
|
||||
// actually appear in the tree in two places.
|
||||
child.parent?.dropChild(child);
|
||||
}
|
||||
assert(!child.attached);
|
||||
adoptChild(child);
|
||||
sawChange = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
List<SemanticsNode> oldChildren = _children;
|
||||
_children = _newChildren;
|
||||
oldChildren?.clear();
|
||||
_newChildren = oldChildren;
|
||||
if (sawChange)
|
||||
_markDirty();
|
||||
}
|
||||
|
||||
SemanticsNode get parent => super.parent;
|
||||
void redepthChildren() {
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children)
|
||||
redepthChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
// Visits all the descendants of this node, calling visitor for each one, until
|
||||
// visitor returns false. Returns true if all the visitor calls returned true,
|
||||
// otherwise returns false.
|
||||
bool _visitDescendants(SemanticsNodeVisitor visitor) {
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children) {
|
||||
if (!visitor(child) || !child._visitDescendants(visitor))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
|
||||
static Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
|
||||
|
||||
void attach() {
|
||||
super.attach();
|
||||
assert(!_nodes.containsKey(_id));
|
||||
_nodes[_id] = this;
|
||||
_detachedNodes.remove(this);
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children)
|
||||
child.attach();
|
||||
}
|
||||
}
|
||||
void detach() {
|
||||
super.detach();
|
||||
assert(_nodes.containsKey(_id));
|
||||
assert(!_detachedNodes.contains(this));
|
||||
_nodes.remove(_id);
|
||||
_detachedNodes.add(this);
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children)
|
||||
child.detach();
|
||||
}
|
||||
}
|
||||
|
||||
static List<SemanticsNode> _dirtyNodes = <SemanticsNode>[];
|
||||
bool _dirty = false;
|
||||
void _markDirty() {
|
||||
if (_dirty)
|
||||
return;
|
||||
_dirty = true;
|
||||
assert(!_dirtyNodes.contains(this));
|
||||
assert(!_detachedNodes.contains(this));
|
||||
_dirtyNodes.add(this);
|
||||
}
|
||||
|
||||
mojom.SemanticsNode _serialize() {
|
||||
mojom.SemanticsNode result = new mojom.SemanticsNode();
|
||||
result.id = _id;
|
||||
if (_dirty) {
|
||||
// We could be even more efficient about not sending data here, by only
|
||||
// sending the bits that are dirty (tracking the geometry, flags, strings,
|
||||
// and children separately). For now, we send all or nothing.
|
||||
result.geometry = new mojom.SemanticGeometry();
|
||||
result.geometry.transform = transform?.storage;
|
||||
result.geometry.top = rect.top;
|
||||
result.geometry.left = rect.left;
|
||||
result.geometry.width = math.max(rect.width, 0.0);
|
||||
result.geometry.height = math.max(rect.height, 0.0);
|
||||
result.flags = new mojom.SemanticFlags();
|
||||
result.flags.canBeTapped = canBeTapped;
|
||||
result.flags.canBeLongPressed = canBeLongPressed;
|
||||
result.flags.canBeScrolledHorizontally = canBeScrolledHorizontally;
|
||||
result.flags.canBeScrolledVertically = canBeScrolledVertically;
|
||||
result.flags.hasCheckedState = hasCheckedState;
|
||||
result.flags.isChecked = isChecked;
|
||||
result.strings = new mojom.SemanticStrings();
|
||||
result.strings.label = label;
|
||||
List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
|
||||
if (mergeAllDescendantsIntoThisNode) {
|
||||
_visitDescendants((SemanticsNode node) {
|
||||
result.flags.canBeTapped = result.flags.canBeTapped || node.canBeTapped;
|
||||
result.flags.canBeLongPressed = result.flags.canBeLongPressed || node.canBeLongPressed;
|
||||
result.flags.canBeScrolledHorizontally = result.flags.canBeScrolledHorizontally || node.canBeScrolledHorizontally;
|
||||
result.flags.canBeScrolledVertically = result.flags.canBeScrolledVertically || node.canBeScrolledVertically;
|
||||
result.flags.hasCheckedState = result.flags.hasCheckedState || node.hasCheckedState;
|
||||
result.flags.isChecked = result.flags.isChecked || node.isChecked;
|
||||
if (node.label != '')
|
||||
result.strings.label = result.strings.label.isNotEmpty ? '${result.strings.label}\n${node.label}' : node.label;
|
||||
return true; // continue walk
|
||||
});
|
||||
// and we pretend to have no children
|
||||
} else {
|
||||
if (_children != null) {
|
||||
for (SemanticsNode child in _children)
|
||||
children.add(child._serialize());
|
||||
}
|
||||
}
|
||||
result.children = children;
|
||||
_dirty = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void sendSemanticsTreeTo(mojom.SemanticsClient client) {
|
||||
for (SemanticsNode oldNode in _detachedNodes) {
|
||||
// The other side will have forgotten this node if we even send
|
||||
// it again, so make sure to mark it dirty so that it'll get
|
||||
// sent if it is resurrected.
|
||||
oldNode._dirty = true;
|
||||
}
|
||||
_detachedNodes.clear();
|
||||
if (_dirtyNodes.isEmpty)
|
||||
return;
|
||||
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
|
||||
for (int index = 0; index < _dirtyNodes.length; index += 1) {
|
||||
// we mutate the list as we walk it here, which is why we use an index instead of an iterator
|
||||
SemanticsNode node = _dirtyNodes[index];
|
||||
assert(node._dirty);
|
||||
assert(node.parent == null || !node.parent.mergeAllDescendantsIntoThisNode || node.mergeAllDescendantsIntoThisNode);
|
||||
if (node.mergeAllDescendantsIntoThisNode) {
|
||||
// if we're merged into our parent, make sure our parent is added to the list
|
||||
if (node.parent != null && node.parent.mergeAllDescendantsIntoThisNode)
|
||||
node.parent._markDirty(); // this can add the node to the dirty list
|
||||
// make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too
|
||||
if (node._children != null)
|
||||
for (SemanticsNode child in node._children)
|
||||
child.mergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list
|
||||
}
|
||||
assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
|
||||
}
|
||||
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
|
||||
List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
|
||||
for (SemanticsNode node in _dirtyNodes) {
|
||||
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
|
||||
// The _serialize() method marks the node as not dirty, and
|
||||
// recurses through the tree to do a deep serialization of all
|
||||
// contiguous dirty nodes. This means that when we return here,
|
||||
// it's quite possible that subsequent nodes are no longer
|
||||
// dirty. We skip these here.
|
||||
// We also skip any nodes that were reset and subsequently
|
||||
// dropped entirely (RenderObject.markNeedsSemanticsUpdate()
|
||||
// calls reset() on its SemanticsNode if onlyChanges isn't set,
|
||||
// which happens e.g. when the node is no longer contributing
|
||||
// semantics).
|
||||
if (node._dirty && node.attached)
|
||||
updatedNodes.add(node._serialize());
|
||||
}
|
||||
client.updateSemanticsTree(updatedNodes);
|
||||
_dirtyNodes.clear();
|
||||
}
|
||||
|
||||
static SemanticActionHandler getSemanticActionHandlerForId(int id, { _SemanticFlags neededFlag }) {
|
||||
assert(neededFlag != null);
|
||||
SemanticsNode result = _nodes[id];
|
||||
if (result != null && result.mergeAllDescendantsIntoThisNode && !result._canHandle(neededFlag)) {
|
||||
result._visitDescendants((SemanticsNode node) {
|
||||
if (node._actionHandler != null && node._flags[neededFlag]) {
|
||||
result = node;
|
||||
return false; // found node, abort walk
|
||||
}
|
||||
return true; // continue walk
|
||||
});
|
||||
}
|
||||
if (result == null || !result._canHandle(neededFlag))
|
||||
return null;
|
||||
return result._actionHandler;
|
||||
}
|
||||
|
||||
String toString() {
|
||||
return '$runtimeType($_id'
|
||||
'${_dirty ? " (dirty)" : ""}'
|
||||
'${mergeAllDescendantsIntoThisNode ? " (leaf merge)" : ""}'
|
||||
'; $rect'
|
||||
'${wasAffectedByClip ? " (clipped)" : ""}'
|
||||
'${canBeTapped ? "; canBeTapped" : ""}'
|
||||
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
|
||||
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
|
||||
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
|
||||
'${hasCheckedState ? (isChecked ? "; checked" : "; unchecked") : ""}'
|
||||
'${label != "" ? "; \"$label\"" : ""}'
|
||||
')';
|
||||
}
|
||||
|
||||
String toStringDeep([String prefixLineOne = '', String prefixOtherLines = '']) {
|
||||
String result = '$prefixLineOne$this\n';
|
||||
if (_children != null && _children.isNotEmpty) {
|
||||
for (int index = 0; index < _children.length - 1; index += 1) {
|
||||
SemanticsNode child = _children[index];
|
||||
result += '${child.toStringDeep("$prefixOtherLines \u251C", "$prefixOtherLines \u2502")}';
|
||||
}
|
||||
result += '${_children.last.toStringDeep("$prefixOtherLines \u2514", "$prefixOtherLines ")}';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class SemanticsServer extends mojom.SemanticsServer {
|
||||
void tap(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeTapped)?.handleSemanticTap();
|
||||
}
|
||||
void longPress(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeLongPressed)?.handleSemanticLongPress();
|
||||
}
|
||||
void scrollLeft(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollLeft();
|
||||
}
|
||||
void scrollRight(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledHorizontally)?.handleSemanticScrollRight();
|
||||
}
|
||||
void scrollUp(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollUp();
|
||||
}
|
||||
void scrollDown(int nodeID) {
|
||||
SemanticsNode.getSemanticActionHandlerForId(nodeID, neededFlag: _SemanticFlags.canBeScrolledVertically)?.handleSemanticScrollDown();
|
||||
}
|
||||
}
|
|
@ -404,6 +404,8 @@ abstract class RenderStackBase extends RenderBox
|
|||
paintStack(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => _hasVisualOverflow ? Point.origin & size : null;
|
||||
}
|
||||
|
||||
/// Implements the stack layout algorithm
|
||||
|
|
|
@ -141,6 +141,7 @@ class RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>
|
|||
}
|
||||
|
||||
Rect get paintBounds => Point.origin & size;
|
||||
Rect get semanticBounds => Point.origin & size;
|
||||
|
||||
void debugDescribeSettings(List<String> settings) {
|
||||
// call to ${super.debugDescribeSettings(prefix)} is omitted because the root superclasses don't include any interesting information for this class
|
||||
|
|
|
@ -55,6 +55,7 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
|
|||
assert(_offsetIsSane(value, scrollDirection));
|
||||
_scrollOffset = value;
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// The direction in which the child is permitted to be larger than the viewport
|
||||
|
@ -137,11 +138,15 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
|
|||
dyInDevicePixels / devicePixelRatio);
|
||||
}
|
||||
|
||||
bool _wouldNeedClipAtOffset(Offset offset) {
|
||||
assert(child != null);
|
||||
return offset < Offset.zero || !(Offset.zero & size).contains(((Offset.zero - offset) & child.size).bottomRight);
|
||||
}
|
||||
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
Offset roundedScrollOffset = _scrollOffsetRoundedToIntegerDevicePixels;
|
||||
bool _needsClip = offset < Offset.zero
|
||||
|| !(offset & size).contains(((offset - roundedScrollOffset) & child.size).bottomRight);
|
||||
bool _needsClip = _wouldNeedClipAtOffset(roundedScrollOffset);
|
||||
if (_needsClip) {
|
||||
context.pushClipRect(needsCompositing, offset, Point.origin & size, (PaintingContext context, Offset offset) {
|
||||
context.paintChild(child, offset - roundedScrollOffset);
|
||||
|
@ -157,6 +162,13 @@ class RenderViewport extends RenderBox with RenderObjectWithChildMixin<RenderBox
|
|||
super.applyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) {
|
||||
if (child != null &&
|
||||
_wouldNeedClipAtOffset(_scrollOffsetRoundedToIntegerDevicePixels))
|
||||
return Point.origin & size;
|
||||
return null;
|
||||
}
|
||||
|
||||
bool hitTestChildren(HitTestResult result, { Point position }) {
|
||||
if (child != null) {
|
||||
assert(child.parentData is BoxParentData);
|
||||
|
@ -200,6 +212,7 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
|
|||
return;
|
||||
_paintOffset = value;
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
|
||||
/// Called during [layout] to determine the grid's children.
|
||||
|
@ -254,4 +267,6 @@ abstract class RenderVirtualViewport<T extends ContainerBoxParentDataMixin<Rende
|
|||
void paint(PaintingContext context, Offset offset) {
|
||||
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
|
||||
}
|
||||
|
||||
Rect describeApproximatePaintClip(RenderObject child) => Point.origin & size;
|
||||
}
|
||||
|
|
|
@ -2009,21 +2009,116 @@ class RepaintBoundary extends OneChildRenderObjectWidget {
|
|||
}
|
||||
|
||||
class IgnorePointer extends OneChildRenderObjectWidget {
|
||||
IgnorePointer({ Key key, Widget child, this.ignoring: true })
|
||||
IgnorePointer({ Key key, Widget child, this.ignoring: true, this.ignoringSemantics })
|
||||
: super(key: key, child: child);
|
||||
|
||||
final bool ignoring;
|
||||
final bool ignoringSemantics; // if null, defaults to value of ignoring
|
||||
|
||||
RenderIgnorePointer createRenderObject() => new RenderIgnorePointer(ignoring: ignoring);
|
||||
RenderIgnorePointer createRenderObject() => new RenderIgnorePointer(
|
||||
ignoring: ignoring,
|
||||
ignoringSemantics: ignoringSemantics
|
||||
);
|
||||
|
||||
void updateRenderObject(RenderIgnorePointer renderObject, IgnorePointer oldWidget) {
|
||||
renderObject.ignoring = ignoring;
|
||||
renderObject.ignoringSemantics = ignoringSemantics;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// UTILITY NODES
|
||||
|
||||
/// The Semantics widget annotates the widget tree with a description
|
||||
/// of the meaning of the widgets, so that accessibility tools, search
|
||||
/// engines, and other semantic analysis software can determine the
|
||||
/// meaning of the application.
|
||||
class Semantics extends OneChildRenderObjectWidget {
|
||||
Semantics({
|
||||
Key key,
|
||||
Widget child,
|
||||
this.container: false,
|
||||
this.checked,
|
||||
this.label
|
||||
}) : super(key: key, child: child) {
|
||||
assert(container != null);
|
||||
}
|
||||
|
||||
/// If 'container' is true, this Widget will introduce a new node in
|
||||
/// the semantics tree. Otherwise, the semantics will be merged with
|
||||
/// the semantics of any ancestors.
|
||||
///
|
||||
/// The 'container' flag is implicitly set to true on the immediate
|
||||
/// semantics-providing descendants of a node where multiple
|
||||
/// children have semantics or have descendants providing semantics.
|
||||
/// In other words, the semantics of siblings are not merged. To
|
||||
/// merge the semantics of an entire subtree, including siblings,
|
||||
/// you can use a [MergeSemantics] widget.
|
||||
final bool container;
|
||||
|
||||
/// If non-null, indicates that this subtree represents a checkbox
|
||||
/// or similar widget with a "checked" state, and what its current
|
||||
/// state is.
|
||||
final bool checked;
|
||||
|
||||
/// Provides a textual description of the widget.
|
||||
final String label;
|
||||
|
||||
RenderSemanticAnnotations createRenderObject() => new RenderSemanticAnnotations(
|
||||
container: container,
|
||||
checked: checked,
|
||||
label: label
|
||||
);
|
||||
|
||||
void updateRenderObject(RenderSemanticAnnotations renderObject, Semantics oldWidget) {
|
||||
renderObject.container = container;
|
||||
renderObject.checked = checked;
|
||||
renderObject.label = label;
|
||||
}
|
||||
|
||||
void debugFillDescription(List<String> description) {
|
||||
super.debugFillDescription(description);
|
||||
description.add('container: $container');
|
||||
if (checked != null);
|
||||
description.add('checked: $checked');
|
||||
if (label != null);
|
||||
description.add('label: "$label"');
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes all the semantics of the subtree rooted at this node to be
|
||||
/// merged into one node in the semantics tree. For example, if you
|
||||
/// have a component with a Text node next to a checkbox widget, this
|
||||
/// could be used to merge the label from the Text node with the
|
||||
/// "checked" semantic state of the checkbox into a single node that
|
||||
/// had both the label and the checked state. Otherwise, the label
|
||||
/// would be presented as a separate feature than the checkbox, and
|
||||
/// the user would not be able to be sure that they were related.
|
||||
///
|
||||
/// Be aware that if two nodes in the subtree have conflicting
|
||||
/// semantics, the result may be nonsensical. For example, a subtree
|
||||
/// with a checked checkbox and an unchecked checkbox will be
|
||||
/// presented as checked. All the labels will be merged into a single
|
||||
/// string (with newlines separating each label from the other). If
|
||||
/// multiple nodes in the merged subtree can handle semantic gestures,
|
||||
/// the first one in tree order will be the one to receive the
|
||||
/// callbacks.
|
||||
class MergeSemantics extends OneChildRenderObjectWidget {
|
||||
MergeSemantics({ Key key, Widget child }) : super(key: key, child: child);
|
||||
RenderMergeSemantics createRenderObject() => new RenderMergeSemantics();
|
||||
}
|
||||
|
||||
/// Drops all semantics in this subtree.
|
||||
///
|
||||
/// This can be used to hide subwidgets that would otherwise be
|
||||
/// reported but that would only be confusing. For example, the
|
||||
/// material library's [Chip] widget hides the avatar since it is
|
||||
/// redundant with the chip label.
|
||||
class ExcludeSemantics extends OneChildRenderObjectWidget {
|
||||
ExcludeSemantics({ Key key, Widget child }) : super(key: key, child: child);
|
||||
RenderExcludeSemantics createRenderObject() => new RenderExcludeSemantics();
|
||||
}
|
||||
|
||||
class MetaData extends OneChildRenderObjectWidget {
|
||||
MetaData({ Key key, Widget child, this.metaData })
|
||||
: super(key: key, child: child);
|
||||
|
|
|
@ -271,13 +271,15 @@ class _FocusState extends State<Focus> {
|
|||
scheduleMicrotask(_ensureVisibleIfFocused);
|
||||
}
|
||||
}
|
||||
|
||||
return new _FocusScope(
|
||||
focusState: this,
|
||||
scopeFocused: Focus._atScope(context),
|
||||
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
|
||||
focusedWidget: _focusedWidget,
|
||||
child: config.child
|
||||
return new Semantics(
|
||||
container: true,
|
||||
child: new _FocusScope(
|
||||
focusState: this,
|
||||
scopeFocused: Focus._atScope(context),
|
||||
focusedScope: _focusedScope == _noFocusedScope ? null : _focusedScope,
|
||||
focusedWidget: _focusedWidget,
|
||||
child: config.child
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1466,7 +1466,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
|
|||
}
|
||||
|
||||
void debugUpdateRenderObjectOwner() {
|
||||
_renderObject.debugOwner = debugGetOwnershipChain(4);
|
||||
_renderObject.debugOwner = debugGetOwnershipChain(10);
|
||||
}
|
||||
|
||||
void performRebuild() {
|
||||
|
|
|
@ -31,6 +31,10 @@ export 'package:flutter/gestures.dart' show
|
|||
///
|
||||
/// Attempts to recognize gestures that correspond to its non-null callbacks.
|
||||
///
|
||||
/// GestureDetector also listens for accessibility events and maps
|
||||
/// them to the callbacks. To ignore accessibility events, set
|
||||
/// [excludeFromSemantics] to true.
|
||||
///
|
||||
/// See http://flutter.io/gestures/ for additional information.
|
||||
class GestureDetector extends StatefulComponent {
|
||||
const GestureDetector({
|
||||
|
@ -54,7 +58,8 @@ class GestureDetector extends StatefulComponent {
|
|||
this.onScaleStart,
|
||||
this.onScaleUpdate,
|
||||
this.onScaleEnd,
|
||||
this.behavior
|
||||
this.behavior,
|
||||
this.excludeFromSemantics: false
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
|
@ -65,7 +70,7 @@ class GestureDetector extends StatefulComponent {
|
|||
|
||||
/// A pointer that will trigger a tap has stopped contacting the screen at a
|
||||
/// particular location.
|
||||
final GestureTapDownCallback onTapUp;
|
||||
final GestureTapUpCallback onTapUp;
|
||||
|
||||
/// A tap has occurred.
|
||||
final GestureTapCallback onTap;
|
||||
|
@ -117,6 +122,13 @@ class GestureDetector extends StatefulComponent {
|
|||
/// How this gesture detector should behave during hit testing.
|
||||
final HitTestBehavior behavior;
|
||||
|
||||
/// Whether to exclude these gestures from the semantics tree. For
|
||||
/// example, the long-press gesture for showing a tooltip is
|
||||
/// excluded because the tooltip itself is included in the semantics
|
||||
/// tree directly and so having a gesture to show it would result in
|
||||
/// duplication of information.
|
||||
final bool excludeFromSemantics;
|
||||
|
||||
_GestureDetectorState createState() => new _GestureDetectorState();
|
||||
}
|
||||
|
||||
|
@ -269,11 +281,30 @@ class _GestureDetectorState extends State<GestureDetector> {
|
|||
}
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Listener(
|
||||
Widget result = new Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: config.behavior ?? _defaultBehavior,
|
||||
child: config.child
|
||||
);
|
||||
if (!config.excludeFromSemantics) {
|
||||
result = new _GestureSemantics(
|
||||
onTapDown: config.onTapDown,
|
||||
onTapUp: config.onTapUp,
|
||||
onTap: config.onTap,
|
||||
onLongPress: config.onLongPress,
|
||||
onVerticalDragStart: config.onVerticalDragStart,
|
||||
onVerticalDragUpdate: config.onVerticalDragUpdate,
|
||||
onVerticalDragEnd: config.onVerticalDragEnd,
|
||||
onHorizontalDragStart: config.onHorizontalDragStart,
|
||||
onHorizontalDragUpdate: config.onHorizontalDragUpdate,
|
||||
onHorizontalDragEnd: config.onHorizontalDragEnd,
|
||||
onPanStart: config.onPanStart,
|
||||
onPanUpdate: config.onPanUpdate,
|
||||
onPanEnd: config.onPanEnd,
|
||||
child: result
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void debugFillDescription(List<String> description) {
|
||||
|
@ -309,3 +340,122 @@ class _GestureDetectorState extends State<GestureDetector> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _GestureSemantics extends OneChildRenderObjectWidget {
|
||||
_GestureSemantics({
|
||||
Key key,
|
||||
this.onTapDown,
|
||||
this.onTapUp,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onVerticalDragStart,
|
||||
this.onVerticalDragUpdate,
|
||||
this.onVerticalDragEnd,
|
||||
this.onHorizontalDragStart,
|
||||
this.onHorizontalDragUpdate,
|
||||
this.onHorizontalDragEnd,
|
||||
this.onPanStart,
|
||||
this.onPanUpdate,
|
||||
this.onPanEnd,
|
||||
Widget child
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
final GestureTapDownCallback onTapDown;
|
||||
final GestureTapUpCallback onTapUp;
|
||||
final GestureTapCallback onTap;
|
||||
final GestureLongPressCallback onLongPress;
|
||||
final GestureDragStartCallback onVerticalDragStart;
|
||||
final GestureDragUpdateCallback onVerticalDragUpdate;
|
||||
final GestureDragEndCallback onVerticalDragEnd;
|
||||
final GestureDragStartCallback onHorizontalDragStart;
|
||||
final GestureDragUpdateCallback onHorizontalDragUpdate;
|
||||
final GestureDragEndCallback onHorizontalDragEnd;
|
||||
final GesturePanStartCallback onPanStart;
|
||||
final GesturePanUpdateCallback onPanUpdate;
|
||||
final GesturePanEndCallback onPanEnd;
|
||||
|
||||
bool get _watchingTaps {
|
||||
return onTapDown != null
|
||||
|| onTapUp != null
|
||||
|| onTap != null;
|
||||
}
|
||||
|
||||
bool get _watchingHorizontalDrags {
|
||||
return onHorizontalDragStart != null
|
||||
|| onHorizontalDragUpdate != null
|
||||
|| onHorizontalDragEnd != null;
|
||||
}
|
||||
|
||||
bool get _watchingVerticalDrags {
|
||||
return onVerticalDragStart != null
|
||||
|| onVerticalDragUpdate != null
|
||||
|| onVerticalDragEnd != null;
|
||||
}
|
||||
|
||||
bool get _watchingPans {
|
||||
return onPanStart != null
|
||||
|| onPanUpdate != null
|
||||
|| onPanEnd != null;
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
if (onTapDown != null)
|
||||
onTapDown(Point.origin);
|
||||
if (onTapUp != null)
|
||||
onTapUp(Point.origin);
|
||||
if (onTap != null)
|
||||
onTap();
|
||||
}
|
||||
|
||||
void _handleHorizontalDragUpdate(double delta) {
|
||||
if (_watchingHorizontalDrags) {
|
||||
if (onHorizontalDragStart != null)
|
||||
onHorizontalDragStart(Point.origin);
|
||||
if (onHorizontalDragUpdate != null)
|
||||
onHorizontalDragUpdate(delta);
|
||||
if (onHorizontalDragEnd != null)
|
||||
onHorizontalDragEnd(Offset.zero);
|
||||
} else {
|
||||
assert(_watchingPans);
|
||||
if (onPanStart != null)
|
||||
onPanStart(Point.origin);
|
||||
if (onPanUpdate != null)
|
||||
onPanUpdate(new Offset(delta, 0.0));
|
||||
if (onPanEnd != null)
|
||||
onPanEnd(Offset.zero);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleVerticalDragUpdate(double delta) {
|
||||
if (_watchingVerticalDrags) {
|
||||
if (onVerticalDragStart != null)
|
||||
onVerticalDragStart(Point.origin);
|
||||
if (onVerticalDragUpdate != null)
|
||||
onVerticalDragUpdate(delta);
|
||||
if (onVerticalDragEnd != null)
|
||||
onVerticalDragEnd(Offset.zero);
|
||||
} else {
|
||||
assert(_watchingPans);
|
||||
if (onPanStart != null)
|
||||
onPanStart(Point.origin);
|
||||
if (onPanUpdate != null)
|
||||
onPanUpdate(new Offset(0.0, delta));
|
||||
if (onPanEnd != null)
|
||||
onPanEnd(Offset.zero);
|
||||
}
|
||||
}
|
||||
|
||||
RenderSemanticsGestureHandler createRenderObject() => new RenderSemanticsGestureHandler(
|
||||
onTap: _watchingTaps ? _handleTap : null,
|
||||
onLongPress: onLongPress,
|
||||
onHorizontalDragUpdate: _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null,
|
||||
onVerticalDragUpdate: _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null
|
||||
);
|
||||
|
||||
void updateRenderObject(RenderSemanticsGestureHandler renderObject, _GestureSemantics oldWidget) {
|
||||
renderObject.onTap = _watchingTaps ? _handleTap : null;
|
||||
renderObject.onLongPress = onLongPress;
|
||||
renderObject.onHorizontalDragUpdate = _watchingHorizontalDrags || _watchingPans ? _handleHorizontalDragUpdate : null;
|
||||
renderObject.onVerticalDragUpdate = _watchingVerticalDrags || _watchingPans ? _handleVerticalDragUpdate : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:flutter/animation.dart';
|
|||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
import 'navigator.dart';
|
||||
import 'transitions.dart';
|
||||
|
||||
|
@ -24,17 +25,20 @@ class ModalBarrier extends StatelessComponent {
|
|||
final bool dismissable;
|
||||
|
||||
Widget build(BuildContext context) {
|
||||
return new Listener(
|
||||
onPointerDown: (_) {
|
||||
if (dismissable)
|
||||
Navigator.pop(context);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: new ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : new DecoratedBox(
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: color
|
||||
return new Semantics(
|
||||
container: true,
|
||||
child: new GestureDetector(
|
||||
onTapDown: (Point position) {
|
||||
if (dismissable)
|
||||
Navigator.pop(context);
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: new ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: color == null ? null : new DecoratedBox(
|
||||
decoration: new BoxDecoration(
|
||||
backgroundColor: color
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
362
packages/flutter/lib/src/widgets/semantics_debugger.dart
Normal file
362
packages/flutter/lib/src/widgets/semantics_debugger.dart
Normal file
|
@ -0,0 +1,362 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:sky_services/semantics/semantics.mojom.dart' as engine;
|
||||
|
||||
import 'basic.dart';
|
||||
import 'framework.dart';
|
||||
import 'gesture_detector.dart';
|
||||
|
||||
class SemanticsDebugger extends StatefulComponent {
|
||||
const SemanticsDebugger({ Key key, this.child }) : super(key: key);
|
||||
final Widget child;
|
||||
_SemanticsDebuggerState createState() => new _SemanticsDebuggerState();
|
||||
}
|
||||
|
||||
class _SemanticsDebuggerState extends State<SemanticsDebugger> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
_SemanticsDebuggerClient.ensureInstantiated();
|
||||
_SemanticsDebuggerClient.instance.addListener(_update);
|
||||
}
|
||||
void dispose() {
|
||||
_SemanticsDebuggerClient.instance.removeListener(_update);
|
||||
super.dispose();
|
||||
}
|
||||
void _update() {
|
||||
setState(() {
|
||||
// the generation of the _SemanticsDebuggerClient has changed
|
||||
});
|
||||
}
|
||||
Point _lastPointerDownLocation;
|
||||
void _handlePointerDown(PointerDownEvent event) {
|
||||
setState(() {
|
||||
_lastPointerDownLocation = event.position;
|
||||
});
|
||||
}
|
||||
void _handleTap() {
|
||||
assert(_lastPointerDownLocation != null);
|
||||
_SemanticsDebuggerClient.instance.handleTap(_lastPointerDownLocation);
|
||||
setState(() {
|
||||
_lastPointerDownLocation = null;
|
||||
});
|
||||
}
|
||||
void _handleLongPress() {
|
||||
assert(_lastPointerDownLocation != null);
|
||||
_SemanticsDebuggerClient.instance.handleLongPress(_lastPointerDownLocation);
|
||||
setState(() {
|
||||
_lastPointerDownLocation = null;
|
||||
});
|
||||
}
|
||||
void _handlePanEnd(Offset velocity) {
|
||||
assert(_lastPointerDownLocation != null);
|
||||
_SemanticsDebuggerClient.instance.handlePanEnd(_lastPointerDownLocation, velocity);
|
||||
setState(() {
|
||||
_lastPointerDownLocation = null;
|
||||
});
|
||||
}
|
||||
Widget build(BuildContext context) {
|
||||
return new CustomPaint(
|
||||
foregroundPainter: new _SemanticsDebuggerPainter(_SemanticsDebuggerClient.instance.generation, _lastPointerDownLocation),
|
||||
child: new GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: _handleTap,
|
||||
onLongPress: _handleLongPress,
|
||||
onPanEnd: _handlePanEnd,
|
||||
excludeFromSemantics: true, // otherwise if you don't hit anything, we end up receiving it, which causes an infinite loop...
|
||||
child: new Listener(
|
||||
onPointerDown: _handlePointerDown,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: new IgnorePointer(
|
||||
ignoringSemantics: false,
|
||||
child: config.child
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
typedef bool _SemanticsDebuggerEntryFilter(_SemanticsDebuggerEntry entry);
|
||||
|
||||
class _SemanticsDebuggerEntry {
|
||||
_SemanticsDebuggerEntry(this.id);
|
||||
|
||||
final int id;
|
||||
bool canBeTapped = false;
|
||||
bool canBeLongPressed = false;
|
||||
bool canBeScrolledHorizontally = false;
|
||||
bool canBeScrolledVertically = false;
|
||||
bool hasCheckedState = false;
|
||||
bool isChecked = false;
|
||||
String label;
|
||||
Matrix4 transform;
|
||||
Rect rect;
|
||||
List<_SemanticsDebuggerEntry> children;
|
||||
|
||||
String toString() {
|
||||
return '_SemanticsDebuggerEntry($id; $rect; "$label"'
|
||||
'${canBeTapped ? "; canBeTapped" : ""}'
|
||||
'${canBeLongPressed ? "; canBeLongPressed" : ""}'
|
||||
'${canBeScrolledHorizontally ? "; canBeScrolledHorizontally" : ""}'
|
||||
'${canBeScrolledVertically ? "; canBeScrolledVertically" : ""}'
|
||||
'${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}'
|
||||
')';
|
||||
}
|
||||
String toStringDeep([ String prefix = '']) {
|
||||
if (prefix.length > 20)
|
||||
return '$prefix<ABORTED>\n';
|
||||
String result = '$prefix$this\n';
|
||||
for (_SemanticsDebuggerEntry child in children.reversed) {
|
||||
prefix += ' ';
|
||||
result += '${child.toStringDeep(prefix)}';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int findDepth() {
|
||||
if (children == null || children.isEmpty)
|
||||
return 1;
|
||||
return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce(math.max) + 1;
|
||||
}
|
||||
|
||||
static const TextStyle textStyles = const TextStyle(
|
||||
color: const Color(0xFF000000),
|
||||
fontSize: 10.0,
|
||||
height: 0.8,
|
||||
textAlign: TextAlign.center
|
||||
);
|
||||
|
||||
TextPainter textPainter;
|
||||
void updateMessage() {
|
||||
List<String> annotations = <String>[];
|
||||
bool wantsTap = false;
|
||||
if (hasCheckedState) {
|
||||
annotations.add(isChecked ? 'checked' : 'unchecked');
|
||||
wantsTap = true;
|
||||
}
|
||||
if (canBeTapped) {
|
||||
if (!wantsTap)
|
||||
annotations.add('button');
|
||||
} else {
|
||||
if (wantsTap)
|
||||
annotations.add('disabled');
|
||||
}
|
||||
if (canBeLongPressed)
|
||||
annotations.add('long-pressable');
|
||||
if (canBeScrolledHorizontally || canBeScrolledVertically)
|
||||
annotations.add('scrollable');
|
||||
String message;
|
||||
if (annotations.isEmpty) {
|
||||
assert(label != null);
|
||||
message = label;
|
||||
} else {
|
||||
if (label == '') {
|
||||
message = annotations.join('; ');
|
||||
} else {
|
||||
message = '$label (${annotations.join('; ')})';
|
||||
}
|
||||
}
|
||||
message = message.trim();
|
||||
if (message != '') {
|
||||
textPainter ??= new TextPainter();
|
||||
textPainter.text = new StyledTextSpan(textStyles, <TextSpan>[new PlainTextSpan(message)]);
|
||||
textPainter.maxWidth = rect.width;
|
||||
textPainter.maxHeight = rect.height;
|
||||
textPainter.layout();
|
||||
} else {
|
||||
textPainter = null;
|
||||
}
|
||||
}
|
||||
|
||||
void paint(Canvas canvas, int rank) {
|
||||
canvas.save();
|
||||
if (transform != null)
|
||||
canvas.transform(transform.storage);
|
||||
if (!rect.isEmpty) {
|
||||
Color lineColor = new Color(0xFF000000 + new math.Random(id).nextInt(0xFFFFFF));
|
||||
Rect innerRect = rect.deflate(rank * 1.0);
|
||||
if (innerRect.isEmpty) {
|
||||
Paint fill = new Paint()
|
||||
..color = lineColor
|
||||
..style = ui.PaintingStyle.fill;
|
||||
canvas.drawRect(rect, fill);
|
||||
} else {
|
||||
Paint fill = new Paint()
|
||||
..color = const Color(0xFFFFFFFF)
|
||||
..style = ui.PaintingStyle.fill;
|
||||
canvas.drawRect(rect, fill);
|
||||
Paint line = new Paint()
|
||||
..strokeWidth = rank * 2.0
|
||||
..color = lineColor
|
||||
..style = ui.PaintingStyle.stroke;
|
||||
canvas.drawRect(innerRect, line);
|
||||
}
|
||||
if (textPainter != null) {
|
||||
canvas.save();
|
||||
canvas.clipRect(rect);
|
||||
textPainter.paint(canvas, rect.topLeft.toOffset());
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
for (_SemanticsDebuggerEntry child in children)
|
||||
child.paint(canvas, rank - 1);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
_SemanticsDebuggerEntry hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
|
||||
if (transform != null) {
|
||||
if (transform.determinant == 0.0)
|
||||
return null;
|
||||
Matrix4 invertedTransform = new Matrix4.inverted(transform);
|
||||
position = MatrixUtils.transformPoint(invertedTransform, position);
|
||||
}
|
||||
if (!rect.contains(position))
|
||||
return null;
|
||||
_SemanticsDebuggerEntry result;
|
||||
for (_SemanticsDebuggerEntry child in children.reversed) {
|
||||
result = child.hitTest(position, filter);
|
||||
if (result != null)
|
||||
break;
|
||||
}
|
||||
if (result == null || !filter(result))
|
||||
result = this;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class _SemanticsDebuggerClient implements engine.SemanticsClient {
|
||||
_SemanticsDebuggerClient._() {
|
||||
Renderer.instance.setSemanticsClient(this);
|
||||
}
|
||||
|
||||
static _SemanticsDebuggerClient instance;
|
||||
static engine.SemanticsServer _server;
|
||||
static void ensureInstantiated({ engine.SemanticsServer server }) {
|
||||
_server = server ?? new SemanticsServer();
|
||||
instance ??= new _SemanticsDebuggerClient._();
|
||||
}
|
||||
|
||||
Set<VoidCallback> _listeners = new Set<VoidCallback>();
|
||||
void addListener(VoidCallback callback) {
|
||||
assert(!_listeners.contains(callback));
|
||||
_listeners.add(callback);
|
||||
}
|
||||
void removeListener(VoidCallback callback) {
|
||||
_listeners.remove(callback);
|
||||
}
|
||||
|
||||
Map<int, _SemanticsDebuggerEntry> nodes = <int, _SemanticsDebuggerEntry>{};
|
||||
|
||||
_SemanticsDebuggerEntry _updateNode(engine.SemanticsNode node) {
|
||||
_SemanticsDebuggerEntry entry = nodes.putIfAbsent(node.id, () => new _SemanticsDebuggerEntry(node.id));
|
||||
if (node.flags != null) {
|
||||
entry.canBeTapped = node.flags.canBeTapped;
|
||||
entry.canBeLongPressed = node.flags.canBeLongPressed;
|
||||
entry.canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
|
||||
entry.canBeScrolledVertically = node.flags.canBeScrolledVertically;
|
||||
entry.hasCheckedState = node.flags.hasCheckedState;
|
||||
entry.isChecked = node.flags.isChecked;
|
||||
}
|
||||
if (node.strings != null) {
|
||||
assert(node.strings.label != null);
|
||||
entry.label = node.strings.label;
|
||||
} else {
|
||||
assert(entry.label != null);
|
||||
}
|
||||
if (node.geometry != null) {
|
||||
if (node.geometry.transform != null) {
|
||||
assert(node.geometry.transform.length == 16);
|
||||
// TODO(ianh): Replace this with a cleaner call once
|
||||
// https://github.com/google/vector_math.dart/issues/159
|
||||
// is fixed.
|
||||
List<double> array = node.geometry.transform;
|
||||
entry.transform = new Matrix4(
|
||||
array[0], array[1], array[2], array[3],
|
||||
array[4], array[5], array[6], array[7],
|
||||
array[8], array[9], array[10], array[11],
|
||||
array[12], array[13], array[14], array[15]
|
||||
);
|
||||
} else {
|
||||
entry.transform = null;
|
||||
}
|
||||
entry.rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
|
||||
}
|
||||
entry.updateMessage();
|
||||
if (node.children != null) {
|
||||
Set oldChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children ?? const <_SemanticsDebuggerEntry>[]);
|
||||
entry.children?.clear();
|
||||
entry.children ??= new List<_SemanticsDebuggerEntry>();
|
||||
for (engine.SemanticsNode child in node.children)
|
||||
entry.children.add(_updateNode(child));
|
||||
Set newChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children);
|
||||
Set<_SemanticsDebuggerEntry> removedChildren = oldChildren.difference(newChildren);
|
||||
for (_SemanticsDebuggerEntry oldChild in removedChildren)
|
||||
nodes.remove(oldChild.id);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
int generation = 0;
|
||||
|
||||
updateSemanticsTree(List<engine.SemanticsNode> nodes) {
|
||||
generation += 1;
|
||||
for (engine.SemanticsNode node in nodes)
|
||||
_updateNode(node);
|
||||
for (VoidCallback listener in _listeners)
|
||||
listener();
|
||||
}
|
||||
|
||||
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
|
||||
return nodes[0]?.hitTest(position, filter);
|
||||
}
|
||||
|
||||
void handleTap(Point position) {
|
||||
_server.tap(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeTapped)?.id ?? 0);
|
||||
}
|
||||
void handleLongPress(Point position) {
|
||||
_server.longPress(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeLongPressed)?.id ?? 0);
|
||||
}
|
||||
void handlePanEnd(Point position, Offset velocity) {
|
||||
if (velocity.dx.abs() == velocity.dy.abs())
|
||||
return;
|
||||
if (velocity.dx.abs() > velocity.dy.abs()) {
|
||||
if (velocity.dx.sign < 0)
|
||||
_server.scrollLeft(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
|
||||
else
|
||||
_server.scrollRight(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledHorizontally)?.id ?? 0);
|
||||
} else {
|
||||
if (velocity.dy.sign < 0)
|
||||
_server.scrollUp(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
|
||||
else
|
||||
_server.scrollDown(_hitTest(position, (_SemanticsDebuggerEntry entry) => entry.canBeScrolledVertically)?.id ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SemanticsDebuggerPainter extends CustomPainter {
|
||||
const _SemanticsDebuggerPainter(this.generation, this.pointerPosition);
|
||||
final int generation;
|
||||
final Point pointerPosition;
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_SemanticsDebuggerClient.instance.nodes[0]?.paint(
|
||||
canvas,
|
||||
_SemanticsDebuggerClient.instance.nodes[0].findDepth()
|
||||
);
|
||||
if (pointerPosition != null) {
|
||||
Paint paint = new Paint();
|
||||
paint.color = const Color(0x7F0090FF);
|
||||
canvas.drawCircle(pointerPosition, 10.0, paint);
|
||||
}
|
||||
}
|
||||
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
|
||||
return generation != oldDelegate.generation
|
||||
|| pointerPosition != oldDelegate.pointerPosition;
|
||||
}
|
||||
}
|
|
@ -33,10 +33,11 @@ export 'src/widgets/performance_overlay.dart';
|
|||
export 'src/widgets/placeholder.dart';
|
||||
export 'src/widgets/raw_keyboard_listener.dart';
|
||||
export 'src/widgets/routes.dart';
|
||||
export 'src/widgets/scroll_behavior.dart';
|
||||
export 'src/widgets/scrollable.dart';
|
||||
export 'src/widgets/scrollable_grid.dart';
|
||||
export 'src/widgets/scrollable_list.dart';
|
||||
export 'src/widgets/scroll_behavior.dart';
|
||||
export 'src/widgets/semantics_debugger.dart';
|
||||
export 'src/widgets/status_transitions.dart';
|
||||
export 'src/widgets/title.dart';
|
||||
export 'src/widgets/transitions.dart';
|
||||
|
|
|
@ -8,8 +8,8 @@ dependencies:
|
|||
collection: '>=1.1.3 <2.0.0'
|
||||
intl: '>=0.12.4+2 <0.13.0'
|
||||
material_design_icons: '>=0.0.3 <0.1.0'
|
||||
sky_engine: 0.0.87
|
||||
sky_services: 0.0.87
|
||||
sky_engine: 0.0.88
|
||||
sky_services: 0.0.88
|
||||
vector_math: '>=1.4.5 <2.0.0'
|
||||
quiver: '>=0.21.4 <0.22.0'
|
||||
|
||||
|
|
54
packages/flutter/test/widget/buttons_test.dart
Normal file
54
packages/flutter/test/widget/buttons_test.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Does FlatButton contribute semantics', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
tester.pumpWidget(
|
||||
new Material(
|
||||
child: new Center(
|
||||
child: new FlatButton(
|
||||
onPressed: () { },
|
||||
child: new Text('Hello')
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(1));
|
||||
expect(client.updates[0].children[0].id, equals(1));
|
||||
expect(client.updates[0].children[0].flags.canBeTapped, isTrue);
|
||||
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals('Hello'));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
});
|
||||
});
|
||||
}
|
250
packages/flutter/test/widget/semantics_1_test.dart
Normal file
250
packages/flutter/test/widget/semantics_1_test.dart
Normal file
|
@ -0,0 +1,250 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 1', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
|
||||
// smoketest
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Semantics(
|
||||
label: 'test1',
|
||||
child: new Container()
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('test1'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// control for forking
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: true,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// forking semantics
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: false,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(1));
|
||||
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].children[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].children[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[0].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[1].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[1].strings.label, equals('child2'));
|
||||
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
|
||||
expect(client.updates[0].children[1].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[1].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[1].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// toggle a branch off
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: true,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// toggle a branch back on
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: false,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(3));
|
||||
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].children[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].children[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[0].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[1].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[1].strings.label, equals('child2'));
|
||||
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
|
||||
expect(client.updates[0].children[1].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[1].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[1].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
});
|
||||
});
|
||||
}
|
190
packages/flutter/test/widget/semantics_2_test.dart
Normal file
190
packages/flutter/test/widget/semantics_2_test.dart
Normal file
|
@ -0,0 +1,190 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 2', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
|
||||
// this test is the same as the test in Semantics 1, but
|
||||
// starting with the second branch being ignored and then
|
||||
// switching to not ignoring it.
|
||||
|
||||
// forking semantics
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: false,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(1));
|
||||
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].children[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].children[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[0].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[1].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[1].strings.label, equals('child2'));
|
||||
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
|
||||
expect(client.updates[0].children[1].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[1].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[1].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// toggle a branch off
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: true,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// toggle a branch back on
|
||||
tester.pumpWidget(
|
||||
new Column(
|
||||
children: <Widget>[
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new Semantics(label: 'child1')
|
||||
),
|
||||
new Container(
|
||||
height: 10.0,
|
||||
child: new IgnorePointer(
|
||||
ignoring: false,
|
||||
child: new Semantics(label: 'child2')
|
||||
)
|
||||
),
|
||||
],
|
||||
alignItems: FlexAlignItems.stretch
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(3));
|
||||
expect(client.updates[0].children[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals('child1'));
|
||||
expect(client.updates[0].children[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].children[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[0].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].children[1].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[1].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].children[1].strings.label, equals('child2'));
|
||||
expect(client.updates[0].children[1].geometry.transform, equals([1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
|
||||
expect(client.updates[0].children[1].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].children[1].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].children[1].geometry.height, equals(10.0));
|
||||
expect(client.updates[0].children[1].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
});
|
||||
});
|
||||
}
|
152
packages/flutter/test/widget/semantics_3_test.dart
Normal file
152
packages/flutter/test/widget/semantics_3_test.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 3', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
|
||||
// implicit annotators
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Semantics(
|
||||
label: 'test',
|
||||
child: new Container(
|
||||
child: new Semantics(
|
||||
checked: true
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isTrue);
|
||||
expect(client.updates[0].flags.isChecked, isTrue);
|
||||
expect(client.updates[0].strings.label, equals('test'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// remove one
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Container(
|
||||
child: new Semantics(
|
||||
checked: true
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isTrue);
|
||||
expect(client.updates[0].flags.isChecked, isTrue);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// change what it says
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Container(
|
||||
child: new Semantics(
|
||||
label: 'test'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('test'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// add a node
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Semantics(
|
||||
checked: true,
|
||||
child: new Container(
|
||||
child: new Semantics(
|
||||
label: 'test'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isTrue);
|
||||
expect(client.updates[0].flags.isChecked, isTrue);
|
||||
expect(client.updates[0].strings.label, equals('test'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// make no changes
|
||||
tester.pumpWidget(
|
||||
new Container(
|
||||
child: new Semantics(
|
||||
checked: true,
|
||||
child: new Container(
|
||||
child: new Semantics(
|
||||
label: 'test'
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(0));
|
||||
|
||||
});
|
||||
});
|
||||
}
|
120
packages/flutter/test/widget/semantics_4_test.dart
Normal file
120
packages/flutter/test/widget/semantics_4_test.dart
Normal file
|
@ -0,0 +1,120 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 4', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
|
||||
// O
|
||||
// / \ O=root
|
||||
// L L L=node with label
|
||||
// / \ C=node with checked
|
||||
// C C* *=node removed next pass
|
||||
//
|
||||
tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'L1'
|
||||
),
|
||||
new Semantics(
|
||||
label: 'L2',
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
checked: true
|
||||
),
|
||||
new Semantics(
|
||||
checked: false
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(1));
|
||||
expect(client.updates[0].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].children.length, equals(2));
|
||||
expect(client.updates[0].children[1].children[0].id, equals(3));
|
||||
expect(client.updates[0].children[1].children[0].children.length, equals(0));
|
||||
expect(client.updates[0].children[1].children[1].id, equals(4));
|
||||
expect(client.updates[0].children[1].children[1].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// O O=root
|
||||
// / \ L=node with label
|
||||
// L* LC C=node with checked
|
||||
// *=node removed next pass
|
||||
//
|
||||
tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
label: 'L1'
|
||||
),
|
||||
new Semantics(
|
||||
label: 'L2',
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
checked: true
|
||||
),
|
||||
new Semantics()
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(2));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
// O=root
|
||||
// OLC L=node with label
|
||||
// C=node with checked
|
||||
//
|
||||
tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(),
|
||||
new Semantics(
|
||||
label: 'L2',
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
checked: true
|
||||
),
|
||||
new Semantics()
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
});
|
||||
});
|
||||
}
|
49
packages/flutter/test/widget/semantics_5_test.dart
Normal file
49
packages/flutter/test/widget/semantics_5_test.dart
Normal file
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 5', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
|
||||
tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(
|
||||
// this tests that empty nodes disappear
|
||||
),
|
||||
new Semantics(
|
||||
// this tests whether you can have a container with no other semantics
|
||||
container: true
|
||||
),
|
||||
new Semantics(
|
||||
label: 'label' // (force a fork)
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].strings.label, equals(''));
|
||||
expect(client.updates[0].children.length, equals(2));
|
||||
expect(client.updates[0].children[0].id, equals(1));
|
||||
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[0].strings.label, equals(''));
|
||||
expect(client.updates[0].children[1].id, equals(2));
|
||||
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].children[1].strings.label, equals('label'));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
});
|
||||
});
|
||||
}
|
50
packages/flutter/test/widget/semantics_6_test.dart
Normal file
50
packages/flutter/test/widget/semantics_6_test.dart
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('Semantics 6 - SemanticsDebugger smoke test', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
|
||||
// This is a smoketest to verify that adding a debugger doesn't crash.
|
||||
|
||||
tester.pumpWidget(
|
||||
new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(),
|
||||
new Semantics(
|
||||
container: true
|
||||
),
|
||||
new Semantics(
|
||||
label: 'label'
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
tester.pumpWidget(
|
||||
new SemanticsDebugger(
|
||||
child: new Stack(
|
||||
children: <Widget>[
|
||||
new Semantics(),
|
||||
new Semantics(
|
||||
container: true
|
||||
),
|
||||
new Semantics(
|
||||
label: 'label'
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
expect(true, isTrue); // expect that we reach here without crashing
|
||||
|
||||
});
|
||||
});
|
||||
}
|
18
packages/flutter/test/widget/test_semantics.dart
Normal file
18
packages/flutter/test/widget/test_semantics.dart
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:sky_services/semantics/semantics.mojom.dart' as engine;
|
||||
|
||||
class TestSemanticsClient implements engine.SemanticsClient {
|
||||
TestSemanticsClient() {
|
||||
Renderer.instance.setSemanticsClient(this);
|
||||
}
|
||||
final List<engine.SemanticsNode> updates = <engine.SemanticsNode>[];
|
||||
updateSemanticsTree(List<engine.SemanticsNode> nodes) {
|
||||
assert(!nodes.any((engine.SemanticsNode node) => node == null));
|
||||
updates.addAll(nodes);
|
||||
updates.add(null);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'test_semantics.dart';
|
||||
|
||||
void main() {
|
||||
test('Does tooltip end up in the right place - top left', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
|
@ -354,4 +356,74 @@ void main() {
|
|||
expect(tip.localToGlobal(tip.size.bottomRight(Point.origin)).y, equals(320.0));
|
||||
});
|
||||
});
|
||||
|
||||
test('Does tooltip contribute semantics', () {
|
||||
testWidgets((WidgetTester tester) {
|
||||
TestSemanticsClient client = new TestSemanticsClient();
|
||||
GlobalKey key = new GlobalKey();
|
||||
tester.pumpWidget(
|
||||
new Overlay(
|
||||
initialEntries: <OverlayEntry>[
|
||||
new OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return new Stack(
|
||||
children: <Widget>[
|
||||
new Positioned(
|
||||
left: 780.0,
|
||||
top: 300.0,
|
||||
child: new Tooltip(
|
||||
key: key,
|
||||
message: 'TIP',
|
||||
fadeDuration: const Duration(seconds: 1),
|
||||
showDuration: const Duration(seconds: 2),
|
||||
child: new Container(width: 0.0, height: 0.0)
|
||||
)
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('TIP'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
|
||||
key.currentState.showTooltip(); // this triggers a rebuild of the semantics because the tree changes
|
||||
|
||||
tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
|
||||
expect(client.updates.length, equals(2));
|
||||
expect(client.updates[0].id, equals(0));
|
||||
expect(client.updates[0].flags.canBeTapped, isFalse);
|
||||
expect(client.updates[0].flags.canBeLongPressed, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledHorizontally, isFalse);
|
||||
expect(client.updates[0].flags.canBeScrolledVertically, isFalse);
|
||||
expect(client.updates[0].flags.hasCheckedState, isFalse);
|
||||
expect(client.updates[0].flags.isChecked, isFalse);
|
||||
expect(client.updates[0].strings.label, equals('TIP'));
|
||||
expect(client.updates[0].geometry.transform, isNull);
|
||||
expect(client.updates[0].geometry.left, equals(0.0));
|
||||
expect(client.updates[0].geometry.top, equals(0.0));
|
||||
expect(client.updates[0].geometry.width, equals(800.0));
|
||||
expect(client.updates[0].geometry.height, equals(600.0));
|
||||
expect(client.updates[0].children.length, equals(0));
|
||||
expect(client.updates[1], isNull);
|
||||
client.updates.clear();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue