288
examples/api/lib/widgets/safe_area/safe_area.0.dart
Normal file
288
examples/api/lib/widgets/safe_area/safe_area.0.dart
Normal file
@@ -0,0 +1,288 @@
|
||||
// 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/material.dart';
|
||||
|
||||
/// Flutter code sample for [SafeArea].
|
||||
///
|
||||
/// The app is wrapped with [Insets] (defined below)
|
||||
/// to simulate a mobile device with a notched screen.
|
||||
|
||||
void main() => runApp(const Insets());
|
||||
|
||||
class SafeAreaExampleApp extends StatelessWidget {
|
||||
const SafeAreaExampleApp({super.key});
|
||||
|
||||
static const Color spring = Color(0xFF00FF80);
|
||||
static final ColorScheme colors = ColorScheme.fromSeed(seedColor: spring);
|
||||
static final ThemeData theme = ThemeData(
|
||||
colorScheme: colors,
|
||||
sliderTheme: SliderThemeData(
|
||||
trackHeight: 8,
|
||||
activeTrackColor: colors.primary.withValues(alpha: 0.5),
|
||||
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 12),
|
||||
),
|
||||
scaffoldBackgroundColor: const Color(0xFFD0FFE8),
|
||||
listTileTheme: const ListTileThemeData(
|
||||
tileColor: Colors.white70,
|
||||
contentPadding: EdgeInsets.all(6.0),
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: colors.secondary,
|
||||
foregroundColor: colors.onSecondary,
|
||||
),
|
||||
);
|
||||
|
||||
static final AppBar appBar = AppBar(title: const Text('SafeArea Demo'));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: theme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Builder(
|
||||
builder: (BuildContext context) => Scaffold(
|
||||
appBar: Toggle.appBar.of(context) ? appBar : null,
|
||||
body: const DefaultTextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black,
|
||||
),
|
||||
child: Center(
|
||||
child: SafeAreaExample(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SafeAreaExample extends StatelessWidget {
|
||||
const SafeAreaExample({super.key});
|
||||
|
||||
static final Widget controls = Column(
|
||||
children: <Widget>[
|
||||
const SizedBox(height: 14),
|
||||
Builder(
|
||||
builder: (BuildContext context) => Text(
|
||||
Toggle.safeArea.of(context) ? 'safe area!' : 'no safe area',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 2),
|
||||
for (final Value data in Value.allValues) ...data.controls,
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool hasSafeArea = Toggle.safeArea.of(context);
|
||||
|
||||
return SafeArea(
|
||||
top: hasSafeArea,
|
||||
bottom: hasSafeArea,
|
||||
left: hasSafeArea,
|
||||
right: hasSafeArea,
|
||||
child: controls,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Value implements Enum {
|
||||
Object _getValue(covariant Model<Value> model);
|
||||
|
||||
List<Widget> get controls;
|
||||
|
||||
static const List<Value> allValues = <Value>[...Inset.values, ...Toggle.values];
|
||||
}
|
||||
|
||||
enum Inset implements Value {
|
||||
top('top notch'),
|
||||
sides('side padding'),
|
||||
bottom('bottom indicator');
|
||||
|
||||
const Inset(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
double _getValue(_InsetModel model) => switch (this) {
|
||||
top => model.insets.top,
|
||||
sides => model.insets.left,
|
||||
bottom => model.insets.bottom,
|
||||
};
|
||||
|
||||
double of(BuildContext context) => _getValue(Model.of<_InsetModel>(context, this));
|
||||
|
||||
@override
|
||||
List<Widget> get controls => <Widget>[
|
||||
Text(label),
|
||||
Builder(
|
||||
builder: (BuildContext context) => Slider(
|
||||
max: 50,
|
||||
value: of(context),
|
||||
onChanged: (double newValue) {
|
||||
InsetsState.instance.changeInset(this, newValue);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
];
|
||||
}
|
||||
|
||||
enum Toggle implements Value {
|
||||
appBar('Build an AppBar?'),
|
||||
safeArea("Wrap Scaffold's body with SafeArea?");
|
||||
|
||||
const Toggle(this.label);
|
||||
|
||||
final String label;
|
||||
|
||||
@override
|
||||
bool _getValue(_ToggleModel model) => switch (this) {
|
||||
appBar => model.buildAppBar,
|
||||
safeArea => model.buildSafeArea,
|
||||
};
|
||||
|
||||
bool of(BuildContext context) => _getValue(Model.of<_ToggleModel>(context, this));
|
||||
|
||||
@override
|
||||
List<Widget> get controls => <Widget>[
|
||||
Builder(
|
||||
builder: (BuildContext context) => SwitchListTile(
|
||||
title: Text(label),
|
||||
value: of(context),
|
||||
onChanged: (bool value) {
|
||||
InsetsState.instance.toggle(this, value);
|
||||
},
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
abstract class Model<E extends Value> extends InheritedModel<E> {
|
||||
const Model({super.key, required super.child});
|
||||
|
||||
static M of<M extends Model<Value>>(BuildContext context, Value value) {
|
||||
return context.dependOnInheritedWidgetOfExactType<M>(aspect: value)!;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(Model<E> oldWidget) => true;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(Model<E> oldWidget, Set<E> dependencies) {
|
||||
return dependencies.any((E data) => data._getValue(this) != data._getValue(oldWidget));
|
||||
}
|
||||
}
|
||||
|
||||
class _InsetModel extends Model<Inset> {
|
||||
const _InsetModel({required this.insets, required super.child});
|
||||
|
||||
final EdgeInsets insets;
|
||||
}
|
||||
|
||||
class _ToggleModel extends Model<Toggle> {
|
||||
_ToggleModel({required Set<Toggle> togglers, required super.child})
|
||||
: buildAppBar = togglers.contains(Toggle.appBar),
|
||||
buildSafeArea = togglers.contains(Toggle.safeArea);
|
||||
|
||||
final bool buildAppBar;
|
||||
final bool buildSafeArea;
|
||||
}
|
||||
|
||||
class Insets extends UniqueWidget<InsetsState> {
|
||||
const Insets() : super(key: const GlobalObjectKey<InsetsState>('insets'));
|
||||
|
||||
@override
|
||||
InsetsState createState() => InsetsState();
|
||||
}
|
||||
|
||||
class InsetsState extends State<Insets> {
|
||||
static InsetsState get instance => const Insets().currentState!;
|
||||
|
||||
EdgeInsets insets = const EdgeInsets.fromLTRB(8, 25, 8, 12);
|
||||
void changeInset(Inset inset, double value) {
|
||||
setState(() {
|
||||
insets = switch (inset) {
|
||||
Inset.top => insets.copyWith(top: value),
|
||||
Inset.sides => insets.copyWith(left: value, right: value),
|
||||
Inset.bottom => insets.copyWith(bottom: value),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
final Set<Toggle> _togglers = <Toggle>{};
|
||||
void toggle(Toggle toggler, bool value) {
|
||||
setState(() {
|
||||
value ? _togglers.add(toggler) : _togglers.remove(toggler);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget topNotch = ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.circular(insets.top),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: insets.top,
|
||||
child: const FractionallySizedBox(
|
||||
widthFactor: 1 / 2,
|
||||
child: ColoredBox(color: Colors.black),
|
||||
),
|
||||
),
|
||||
);
|
||||
final Widget bottomIndicator = SizedBox(
|
||||
width: double.infinity,
|
||||
height: insets.bottom,
|
||||
child: const FractionallySizedBox(
|
||||
heightFactor: 0.5,
|
||||
widthFactor: 0.5,
|
||||
child: PhysicalShape(
|
||||
clipper: ShapeBorderClipper(shape: StadiumBorder()),
|
||||
color: Color(0xC0000000),
|
||||
child: SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
);
|
||||
final Widget sideBar = SizedBox(
|
||||
width: insets.left,
|
||||
height: double.infinity,
|
||||
child: const IgnorePointer(child: ColoredBox(color: Colors.black12)),
|
||||
);
|
||||
|
||||
final Widget app = _ToggleModel(
|
||||
togglers: _togglers,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) => MediaQuery(
|
||||
data: MediaQuery.of(context).copyWith(
|
||||
viewInsets: EdgeInsets.only(top: insets.top),
|
||||
viewPadding: insets,
|
||||
padding: insets,
|
||||
),
|
||||
child: const SafeAreaExampleApp(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return _InsetModel(
|
||||
insets: insets,
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
app,
|
||||
Align(alignment: Alignment.topCenter, child: topNotch),
|
||||
Align(alignment: Alignment.bottomCenter, child: bottomIndicator),
|
||||
Align(alignment: Alignment.centerLeft, child: sideBar),
|
||||
Align(alignment: Alignment.centerRight, child: sideBar),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
84
examples/api/test/widgets/safe_area/safe_area.0_test.dart
Normal file
84
examples/api/test/widgets/safe_area/safe_area.0_test.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter_api_samples/widgets/safe_area/safe_area.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
example.InsetsState get insetsState => example.InsetsState.instance;
|
||||
|
||||
void main() {
|
||||
testWidgets('SafeArea widget count', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.Insets());
|
||||
|
||||
// 1 SafeArea and 2 ListTile widgets.
|
||||
expect(find.byType(SafeArea), findsNWidgets(3));
|
||||
|
||||
insetsState.toggle(example.Toggle.appBar, true);
|
||||
await tester.pump();
|
||||
expect(find.byType(SafeArea), findsNWidgets(4));
|
||||
});
|
||||
|
||||
testWidgets('ListTile removes side padding from its content', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.Insets());
|
||||
|
||||
late BuildContext context;
|
||||
late EdgeInsets padding;
|
||||
|
||||
final Finder findBody = find.byType(example.SafeAreaExample);
|
||||
final Finder findListTile = find.text(example.Toggle.appBar.label).first;
|
||||
|
||||
context = tester.element(findBody);
|
||||
padding = MediaQuery.paddingOf(context);
|
||||
expect(padding.top, greaterThan(0));
|
||||
expect(padding.bottom, greaterThan(0));
|
||||
expect(padding.left, greaterThan(0));
|
||||
expect(padding.right, greaterThan(0));
|
||||
|
||||
context = tester.element(findListTile);
|
||||
padding = MediaQuery.paddingOf(context);
|
||||
expect(padding.top, greaterThan(0));
|
||||
expect(padding.bottom, greaterThan(0));
|
||||
expect(padding.left, 0);
|
||||
expect(padding.right, 0);
|
||||
});
|
||||
|
||||
testWidgets('AppBar removes top padding of Scaffold body', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.Insets());
|
||||
final BuildContext context = tester.element(find.text('no safe area'));
|
||||
|
||||
// Double-check that side & bottom padding are unchanged.
|
||||
void verifySidesAndBottom() {
|
||||
final EdgeInsets padding = MediaQuery.paddingOf(context);
|
||||
expect(padding.left, example.Inset.sides.of(context));
|
||||
expect(padding.right, example.Inset.sides.of(context));
|
||||
expect(padding.bottom, example.Inset.bottom.of(context));
|
||||
}
|
||||
|
||||
final double topInset = example.Inset.top.of(context);
|
||||
expect(topInset, greaterThan(0));
|
||||
expect(MediaQuery.paddingOf(context).top, topInset);
|
||||
verifySidesAndBottom();
|
||||
|
||||
insetsState.toggle(example.Toggle.appBar, true);
|
||||
await tester.pump();
|
||||
|
||||
expect(example.Inset.top.of(context), greaterThan(0));
|
||||
expect(MediaQuery.paddingOf(context).top, 0);
|
||||
verifySidesAndBottom();
|
||||
});
|
||||
|
||||
testWidgets('SafeArea removes all padding', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const example.Insets());
|
||||
|
||||
BuildContext context = tester.element(find.text('no safe area'));
|
||||
expect(MediaQuery.paddingOf(context), insetsState.insets);
|
||||
|
||||
insetsState.toggle(example.Toggle.safeArea, true);
|
||||
await tester.pump();
|
||||
|
||||
context = tester.element(find.text('safe area!'));
|
||||
expect(MediaQuery.paddingOf(context), EdgeInsets.zero);
|
||||
});
|
||||
}
|
||||
@@ -16,15 +16,16 @@ import 'media_query.dart';
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=lkF0TQJO0bA}
|
||||
///
|
||||
/// For example, this will indent the child by enough to avoid the status bar at
|
||||
/// the top of the screen.
|
||||
///
|
||||
/// It will also indent the child by the amount necessary to avoid The Notch on
|
||||
/// the iPhone X, or other similar creative physical features of the display.
|
||||
///
|
||||
/// When a [minimum] padding is specified, the greater of the minimum padding
|
||||
/// or the safe area padding will be applied.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how `SafeArea` can apply padding within a mobile device's
|
||||
/// screen to make the relevant content completely visible.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/safe_area/safe_area.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool snippet}
|
||||
///
|
||||
/// This example creates a blue box containing text that is sufficiently padded
|
||||
|
||||
Reference in New Issue
Block a user