mirror of
https://github.com/flutter/flutter
synced 2024-08-27 12:00:58 +00:00
parent
8afd600e79
commit
84078c856f
75
examples/api/lib/cupertino/radio/cupertino_radio.0.dart
Normal file
75
examples/api/lib/cupertino/radio/cupertino_radio.0.dart
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2014 The Flutter 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/cupertino.dart';
|
||||
|
||||
/// Flutter code sample for [CupertinoRadio].
|
||||
|
||||
void main() => runApp(const CupertinoRadioApp());
|
||||
|
||||
class CupertinoRadioApp extends StatelessWidget {
|
||||
const CupertinoRadioApp({super.key});
|
||||
|
||||
static const String _title = 'CuptertinoRadio Example';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CupertinoApp(
|
||||
theme: CupertinoThemeData(brightness: Brightness.light),
|
||||
home: CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(_title),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: CupertinoRadioExample(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SingingCharacter { lafayette, jefferson }
|
||||
|
||||
class CupertinoRadioExample extends StatefulWidget {
|
||||
const CupertinoRadioExample({super.key});
|
||||
|
||||
@override
|
||||
State<CupertinoRadioExample> createState() => _CupertinoRadioExampleState();
|
||||
}
|
||||
|
||||
class _CupertinoRadioExampleState extends State<CupertinoRadioExample> {
|
||||
SingingCharacter? _character = SingingCharacter.lafayette;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('Lafayette'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.lafayette,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('Thomas Jefferson'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.jefferson,
|
||||
groupValue: _character,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright 2014 The Flutter 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/cupertino.dart';
|
||||
|
||||
/// Flutter code sample for [CupertinoRadio.toggleable].
|
||||
|
||||
void main() => runApp(const CupertinoRadioApp());
|
||||
|
||||
class CupertinoRadioApp extends StatelessWidget {
|
||||
const CupertinoRadioApp({super.key});
|
||||
|
||||
static const String _title = 'CuptertinoRadio Toggleable Example';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(_title),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: CupertinoRadioExample(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum SingingCharacter { mulligan, hamilton }
|
||||
|
||||
class CupertinoRadioExample extends StatefulWidget {
|
||||
const CupertinoRadioExample({super.key});
|
||||
|
||||
@override
|
||||
State<CupertinoRadioExample> createState() => _CupertinoRadioExampleState();
|
||||
}
|
||||
|
||||
class _CupertinoRadioExampleState extends State<CupertinoRadioExample> {
|
||||
SingingCharacter? _character = SingingCharacter.mulligan;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoListSection(
|
||||
children: <Widget>[
|
||||
CupertinoListTile(
|
||||
title: const Text('Hercules Mulligan'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.mulligan,
|
||||
groupValue: _character,
|
||||
// TRY THIS: Try setting the toggleable value to false and
|
||||
// see how that changes the behavior of the widget.
|
||||
toggleable: true,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: const Text('Eliza Hamilton'),
|
||||
leading: CupertinoRadio<SingingCharacter>(
|
||||
value: SingingCharacter.hamilton,
|
||||
groupValue: _character,
|
||||
toggleable: true,
|
||||
onChanged: (SingingCharacter? value) {
|
||||
setState(() {
|
||||
_character = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2014 The Flutter 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/cupertino.dart';
|
||||
import 'package:flutter_api_samples/cupertino/radio/cupertino_radio.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Has 2 CupertinoRadio widgets', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoRadioApp(),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoRadio<example.SingingCharacter>), findsNWidgets(2));
|
||||
|
||||
CupertinoRadio<example.SingingCharacter> radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.lafayette);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.jefferson);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.jefferson);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright 2014 The Flutter 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/cupertino.dart';
|
||||
import 'package:flutter_api_samples/cupertino/radio/cupertino_radio.toggleable.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Has 2 CupertinoRadio widgets that can be toggled off', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.CupertinoRadioApp(),
|
||||
);
|
||||
|
||||
expect(find.byType(CupertinoRadio<example.SingingCharacter>), findsNWidgets(2));
|
||||
|
||||
CupertinoRadio<example.SingingCharacter> radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.mulligan);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.mulligan);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, example.SingingCharacter.hamilton);
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).first);
|
||||
expect(radio.groupValue, example.SingingCharacter.hamilton);
|
||||
|
||||
await tester.tap(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
radio = tester.widget(find.byType(CupertinoRadio<example.SingingCharacter>).last);
|
||||
expect(radio.groupValue, null);
|
||||
});
|
||||
}
|
|
@ -50,6 +50,7 @@ export 'src/cupertino/magnifier.dart';
|
|||
export 'src/cupertino/nav_bar.dart';
|
||||
export 'src/cupertino/page_scaffold.dart';
|
||||
export 'src/cupertino/picker.dart';
|
||||
export 'src/cupertino/radio.dart';
|
||||
export 'src/cupertino/refresh.dart';
|
||||
export 'src/cupertino/route.dart';
|
||||
export 'src/cupertino/scrollbar.dart';
|
||||
|
|
308
packages/flutter/lib/src/cupertino/radio.dart
Normal file
308
packages/flutter/lib/src/cupertino/radio.dart
Normal file
|
@ -0,0 +1,308 @@
|
|||
// Copyright 2014 The Flutter 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/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'toggleable.dart';
|
||||
|
||||
// Examples can assume:
|
||||
// late BuildContext context;
|
||||
// enum SingingCharacter { lafayette }
|
||||
// late SingingCharacter? _character;
|
||||
// late StateSetter setState;
|
||||
|
||||
const Size _size = Size(18.0, 18.0);
|
||||
const double _kOuterRadius = 7.0;
|
||||
const double _kInnerRadius = 2.975;
|
||||
|
||||
// The relative values needed to transform a color to its equivilant focus
|
||||
// outline color.
|
||||
const double _kCupertinoFocusColorOpacity = 0.80;
|
||||
const double _kCupertinoFocusColorBrightness = 0.69;
|
||||
const double _kCupertinoFocusColorSaturation = 0.835;
|
||||
|
||||
/// A macOS-style radio button.
|
||||
///
|
||||
/// Used to select between a number of mutually exclusive values. When one radio
|
||||
/// button in a group is selected, the other radio buttons in the group are
|
||||
/// deselected. The values are of type `T`, the type parameter of the
|
||||
/// [CupertinoRadio] class. Enums are commonly used for this purpose.
|
||||
///
|
||||
/// The radio button itself does not maintain any state. Instead, selecting the
|
||||
/// radio invokes the [onChanged] callback, passing [value] as a parameter. If
|
||||
/// [groupValue] and [value] match, this radio will be selected. Most widgets
|
||||
/// will respond to [onChanged] by calling [State.setState] to update the
|
||||
/// radio button's [groupValue].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// Here is an example of CupertinoRadio widgets wrapped in CupertinoListTiles.
|
||||
///
|
||||
/// The currently selected character is passed into `groupValue`, which is
|
||||
/// maintained by the example's `State`. In this case, the first [CupertinoRadio]
|
||||
/// will start off selected because `_character` is initialized to
|
||||
/// `SingingCharacter.lafayette`.
|
||||
///
|
||||
/// If the second radio button is pressed, the example's state is updated
|
||||
/// with `setState`, updating `_character` to `SingingCharacter.jefferson`.
|
||||
/// This causes the buttons to rebuild with the updated `groupValue`, and
|
||||
/// therefore the selection of the second button.
|
||||
///
|
||||
/// ** See code in examples/api/lib/cupertino/radio/cupertino_radio.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoSlider], for selecting a value in a range.
|
||||
/// * [CupertinoCheckbox] and [CupertinoSwitch], for toggling a particular value on or off.
|
||||
/// * [Radio], the Material Design equivalent.
|
||||
/// * <https://developer.apple.com/design/human-interface-guidelines/components/selection-and-input/toggles/>
|
||||
class CupertinoRadio<T> extends StatefulWidget {
|
||||
/// Creates a macOS-styled radio button.
|
||||
///
|
||||
/// The following arguments are required:
|
||||
///
|
||||
/// * [value] and [groupValue] together determine whether the radio button is
|
||||
/// selected.
|
||||
/// * [onChanged] is called when the user selects this radio button.
|
||||
const CupertinoRadio({
|
||||
super.key,
|
||||
required this.value,
|
||||
required this.groupValue,
|
||||
required this.onChanged,
|
||||
this.toggleable = false,
|
||||
this.activeColor,
|
||||
this.inactiveColor,
|
||||
this.fillColor,
|
||||
this.focusColor,
|
||||
this.focusNode,
|
||||
this.autofocus = false,
|
||||
});
|
||||
|
||||
/// The value represented by this radio button.
|
||||
///
|
||||
/// If this equals the [groupValue], then this radio button will appear
|
||||
/// selected.
|
||||
final T value;
|
||||
|
||||
/// The currently selected value for a group of radio buttons.
|
||||
///
|
||||
/// This radio button is considered selected if its [value] matches the
|
||||
/// [groupValue].
|
||||
final T? groupValue;
|
||||
|
||||
/// Called when the user selects this [CupertinoRadio] button.
|
||||
///
|
||||
/// The radio button passes [value] as a parameter to this callback. It does
|
||||
/// not actually change state until the parent widget rebuilds the radio
|
||||
/// button with a new [groupValue].
|
||||
///
|
||||
/// If null, the radio button will be displayed as disabled.
|
||||
///
|
||||
/// The provided callback will not be invoked if this radio button is already
|
||||
/// selected.
|
||||
///
|
||||
/// The callback provided to [onChanged] should update the state of the parent
|
||||
/// [StatefulWidget] using the [State.setState] method, so that the parent
|
||||
/// gets rebuilt; for example:
|
||||
///
|
||||
/// ```dart
|
||||
/// CupertinoRadio<SingingCharacter>(
|
||||
/// value: SingingCharacter.lafayette,
|
||||
/// groupValue: _character,
|
||||
/// onChanged: (SingingCharacter? newValue) {
|
||||
/// setState(() {
|
||||
/// _character = newValue;
|
||||
/// });
|
||||
/// },
|
||||
/// )
|
||||
/// ```
|
||||
final ValueChanged<T?>? onChanged;
|
||||
|
||||
/// Set to true if this radio button is allowed to be returned to an
|
||||
/// indeterminate state by selecting it again when selected.
|
||||
///
|
||||
/// To indicate returning to an indeterminate state, [onChanged] will be
|
||||
/// called with null.
|
||||
///
|
||||
/// If true, [onChanged] can be called with [value] when selected while
|
||||
/// [groupValue] != [value], or with null when selected again while
|
||||
/// [groupValue] == [value].
|
||||
///
|
||||
/// If false, [onChanged] will be called with [value] when it is selected
|
||||
/// while [groupValue] != [value], and only by selecting another radio button
|
||||
/// in the group (i.e. changing the value of [groupValue]) can this radio
|
||||
/// button be unselected.
|
||||
///
|
||||
/// The default is false.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to enable deselecting a radio button by setting the
|
||||
/// [toggleable] attribute.
|
||||
///
|
||||
/// ** See code in examples/api/lib/cupertino/radio/cupertino_radio.toggleable.0.dart **
|
||||
/// {@end-tool}
|
||||
final bool toggleable;
|
||||
|
||||
/// The color to use when this radio button is selected.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.activeBlue].
|
||||
final Color? activeColor;
|
||||
|
||||
/// The color to use when this radio button is not selected.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.white].
|
||||
final Color? inactiveColor;
|
||||
|
||||
/// The color that fills the inner circle of the radio button when selected.
|
||||
///
|
||||
/// Defaults to [CupertinoColors.white].
|
||||
final Color? fillColor;
|
||||
|
||||
/// The color for the radio's border when it has the input focus.
|
||||
///
|
||||
/// If null, then a paler form of the [activeColor] will be used.
|
||||
final Color? focusColor;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.focusNode}
|
||||
final FocusNode? focusNode;
|
||||
|
||||
/// {@macro flutter.widgets.Focus.autofocus}
|
||||
final bool autofocus;
|
||||
|
||||
bool get _selected => value == groupValue;
|
||||
|
||||
@override
|
||||
State<CupertinoRadio<T>> createState() => _CupertinoRadioState<T>();
|
||||
}
|
||||
|
||||
class _CupertinoRadioState<T> extends State<CupertinoRadio<T>> with TickerProviderStateMixin, ToggleableStateMixin {
|
||||
final _RadioPainter _painter = _RadioPainter();
|
||||
|
||||
bool focused = false;
|
||||
|
||||
void _handleChanged(bool? selected) {
|
||||
if (selected == null) {
|
||||
widget.onChanged!(null);
|
||||
return;
|
||||
}
|
||||
if (selected) {
|
||||
widget.onChanged!(widget.value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_painter.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
ValueChanged<bool?>? get onChanged => widget.onChanged != null ? _handleChanged : null;
|
||||
|
||||
@override
|
||||
bool get tristate => widget.toggleable;
|
||||
|
||||
@override
|
||||
bool? get value => widget._selected;
|
||||
|
||||
void onFocusChange(bool value) {
|
||||
if (focused != value) {
|
||||
focused = value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Color effectiveActiveColor = widget.activeColor
|
||||
?? CupertinoColors.activeBlue;
|
||||
final Color effectiveInactiveColor = widget.inactiveColor
|
||||
?? CupertinoColors.white;
|
||||
|
||||
final Color effectiveFocusOverlayColor = widget.focusColor
|
||||
?? HSLColor
|
||||
.fromColor(effectiveActiveColor.withOpacity(_kCupertinoFocusColorOpacity))
|
||||
.withLightness(_kCupertinoFocusColorBrightness)
|
||||
.withSaturation(_kCupertinoFocusColorSaturation)
|
||||
.toColor();
|
||||
|
||||
final Color effectiveActivePressedOverlayColor =
|
||||
HSLColor.fromColor(effectiveActiveColor).withLightness(0.45).toColor();
|
||||
|
||||
final Color effectiveFillColor = widget.fillColor ?? CupertinoColors.white;
|
||||
|
||||
return Semantics(
|
||||
inMutuallyExclusiveGroup: true,
|
||||
checked: widget._selected,
|
||||
child: buildToggleable(
|
||||
focusNode: widget.focusNode,
|
||||
autofocus: widget.autofocus,
|
||||
onFocusChange: onFocusChange,
|
||||
size: _size,
|
||||
painter: _painter
|
||||
..focusColor = effectiveFocusOverlayColor
|
||||
..downPosition = downPosition
|
||||
..isFocused = focused
|
||||
..activeColor = downPosition != null ? effectiveActivePressedOverlayColor : effectiveActiveColor
|
||||
..inactiveColor = effectiveInactiveColor
|
||||
..fillColor = effectiveFillColor
|
||||
..value = value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioPainter extends ToggleablePainter {
|
||||
bool? get value => _value;
|
||||
bool? _value;
|
||||
set value(bool? value) {
|
||||
if (_value == value) {
|
||||
return;
|
||||
}
|
||||
_value = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Color get fillColor => _fillColor!;
|
||||
Color? _fillColor;
|
||||
set fillColor(Color value) {
|
||||
if (value == _fillColor) {
|
||||
return;
|
||||
}
|
||||
_fillColor = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
|
||||
final Offset center = (Offset.zero & size).center;
|
||||
|
||||
// Outer border
|
||||
final Paint paint = Paint()
|
||||
..color = inactiveColor
|
||||
..style = PaintingStyle.fill
|
||||
..strokeWidth = 0.1;
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.color = CupertinoColors.inactiveGray;
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
|
||||
if (value ?? false) {
|
||||
paint.style = PaintingStyle.fill;
|
||||
paint.color = activeColor;
|
||||
canvas.drawCircle(center, _kOuterRadius, paint);
|
||||
paint.color = fillColor;
|
||||
canvas.drawCircle(center, _kInnerRadius, paint);
|
||||
}
|
||||
|
||||
if (isFocused) {
|
||||
paint.style = PaintingStyle.stroke;
|
||||
paint.color = focusColor;
|
||||
paint.strokeWidth = 3.0;
|
||||
canvas.drawCircle(center, _kOuterRadius + 1.5, paint);
|
||||
}
|
||||
}
|
||||
}
|
340
packages/flutter/test/cupertino/radio_test.dart
Normal file
340
packages/flutter/test/cupertino/radio_test.dart
Normal file
|
@ -0,0 +1,340 @@
|
|||
// Copyright 2014 The Flutter 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/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Radio control test', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
final List<int?> log = <int?>[];
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: log.add,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, equals(<int>[1]));
|
||||
log.clear();
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: log.add,
|
||||
activeColor: CupertinoColors.systemGreen,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, isEmpty);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, isEmpty);
|
||||
});
|
||||
|
||||
testWidgets('Radio can be toggled when toggleable is set', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
final List<int?> log = <int?>[];
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, equals(<int>[1]));
|
||||
log.clear();
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: 1,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, equals(<int?>[null]));
|
||||
log.clear();
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: null,
|
||||
onChanged: log.add,
|
||||
toggleable: true,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
|
||||
expect(log, equals(<int>[1]));
|
||||
});
|
||||
|
||||
testWidgets('Radio semantics', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
isInMutuallyExclusiveGroup: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 2,
|
||||
groupValue: 2,
|
||||
onChanged: (int? i) { },
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isEnabled: true,
|
||||
hasTapAction: true,
|
||||
isFocusable: true,
|
||||
isInMutuallyExclusiveGroup: true,
|
||||
isChecked: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 1,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isFocusable: true,
|
||||
isInMutuallyExclusiveGroup: true,
|
||||
));
|
||||
|
||||
await tester.pump();
|
||||
|
||||
// Now the isFocusable should be gone.
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isInMutuallyExclusiveGroup: true,
|
||||
));
|
||||
|
||||
await tester.pumpWidget(const CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
value: 2,
|
||||
groupValue: 2,
|
||||
onChanged: null,
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
expect(tester.getSemantics(find.byType(Focus).last), matchesSemantics(
|
||||
hasCheckedState: true,
|
||||
hasEnabledState: true,
|
||||
isChecked: true,
|
||||
isInMutuallyExclusiveGroup: true,
|
||||
));
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('has semantic events', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
final Key key = UniqueKey();
|
||||
dynamic semanticEvent;
|
||||
int? radioValue = 2;
|
||||
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, (dynamic message) async {
|
||||
semanticEvent = message;
|
||||
});
|
||||
|
||||
await tester.pumpWidget(CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoRadio<int>(
|
||||
key: key,
|
||||
value: 1,
|
||||
groupValue: radioValue,
|
||||
onChanged: (int? i) {
|
||||
radioValue = i;
|
||||
},
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
await tester.tap(find.byKey(key));
|
||||
final RenderObject object = tester.firstRenderObject(find.byKey(key));
|
||||
|
||||
expect(radioValue, 1);
|
||||
expect(semanticEvent, <String, dynamic>{
|
||||
'type': 'tap',
|
||||
'nodeId': object.debugSemantics!.id,
|
||||
'data': <String, dynamic>{},
|
||||
});
|
||||
expect(object.debugSemantics!.getSemanticsData().hasAction(SemanticsAction.tap), true);
|
||||
|
||||
semantics.dispose();
|
||||
tester.binding.defaultBinaryMessenger.setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null);
|
||||
});
|
||||
|
||||
testWidgets('Radio can be controlled by keyboard shortcuts', (WidgetTester tester) async {
|
||||
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
|
||||
int? groupValue = 1;
|
||||
const Key radioKey0 = Key('radio0');
|
||||
const Key radioKey1 = Key('radio1');
|
||||
const Key radioKey2 = Key('radio2');
|
||||
final FocusNode focusNode2 = FocusNode(debugLabel: 'radio2');
|
||||
Widget buildApp({bool enabled = true}) {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
|
||||
return SizedBox(
|
||||
width: 200,
|
||||
height: 100,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
CupertinoRadio<int>(
|
||||
key: radioKey0,
|
||||
value: 0,
|
||||
onChanged: enabled ? (int? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
groupValue: groupValue,
|
||||
autofocus: true,
|
||||
),
|
||||
CupertinoRadio<int>(
|
||||
key: radioKey1,
|
||||
value: 1,
|
||||
onChanged: enabled ? (int? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
groupValue: groupValue,
|
||||
),
|
||||
CupertinoRadio<int>(
|
||||
key: radioKey2,
|
||||
value: 2,
|
||||
onChanged: enabled ? (int? newValue) {
|
||||
setState(() {
|
||||
groupValue = newValue;
|
||||
});
|
||||
} : null,
|
||||
groupValue: groupValue,
|
||||
focusNode: focusNode2,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle();
|
||||
// On web, radios don't respond to the enter key.
|
||||
expect(groupValue, kIsWeb ? equals(1) : equals(0));
|
||||
|
||||
focusNode2.requestFocus();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.space);
|
||||
await tester.pumpAndSettle();
|
||||
expect(groupValue, equals(2));
|
||||
});
|
||||
|
||||
testWidgets('Do not crash when widget disappears while pointer is down', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
|
||||
Widget buildRadio(bool show) {
|
||||
return CupertinoApp(
|
||||
home: Center(
|
||||
child: show ? CupertinoRadio<bool>(key: key, value: true, groupValue: false, onChanged: (_) { }) : Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildRadio(true));
|
||||
final Offset center = tester.getCenter(find.byKey(key));
|
||||
// Put a pointer down on the screen.
|
||||
final TestGesture gesture = await tester.startGesture(center);
|
||||
await tester.pump();
|
||||
// While the pointer is down, the widget disappears.
|
||||
await tester.pumpWidget(buildRadio(false));
|
||||
expect(find.byKey(key), findsNothing);
|
||||
// Release pointer after widget disappeared.
|
||||
await gesture.up();
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue