You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
7.6 KiB
Dart

2 years ago
// 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';
/// Implements a single iOS application page's layout.
///
/// The scaffold lays out the navigation bar on top and the content between or
/// behind the navigation bar.
///
/// When tapping a status bar at the top of the IsekaiPageScaffold, an
/// animation will complete for the current primary [ScrollView], scrolling to
/// the beginning. This is done using the [PrimaryScrollController] that
/// encloses the [ScrollView]. The [ScrollView.primary] flag is used to connect
/// a [ScrollView] to the enclosing [PrimaryScrollController].
///
/// {@tool dartpad}
/// This example shows a [IsekaiPageScaffold] with a [ListView] as a [child].
/// The [CupertinoButton] is connected to a callback that increments a counter.
/// The [backgroundColor] can be changed.
///
/// ** See code in examples/api/lib/cupertino/page_scaffold/cupertino_page_scaffold.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [CupertinoTabScaffold], a similar widget for tabbed applications.
/// * [CupertinoPageRoute], a modal page route that typically hosts a
/// [IsekaiPageScaffold] with support for iOS-style page transitions.
class IsekaiPageScaffold extends StatefulWidget {
/// Creates a layout for pages with a navigation bar at the top.
const IsekaiPageScaffold({
super.key,
this.navigationBar,
this.bottomNavigationBar,
2 years ago
this.backgroundColor,
this.resizeToAvoidBottomInset = false,
2 years ago
required this.child,
}) : assert(child != null),
assert(resizeToAvoidBottomInset != null);
/// The [navigationBar], typically a [CupertinoNavigationBar], is drawn at the
/// top of the screen.
///
/// If translucent, the main content may slide behind it.
/// Otherwise, the main content's top margin will be offset by its height.
///
/// The scaffold assumes the navigation bar will account for the [MediaQuery]
/// top padding, also consume it if the navigation bar is opaque.
// TODO(xster): document its page transition animation when ready
final ObstructingPreferredSizeWidget? navigationBar;
/// The [bottomNavigationBar], typically a [CupertinoNavigationBar], is drawn at the
/// bottom of the screen.
///
/// If translucent, the main content may slide behind it.
/// Otherwise, the main content's bottom margin will be offset by its height.
///
/// The scaffold assumes the navigation bar will account for the [MediaQuery]
/// bottom padding, also consume it if the navigation bar is opaque.
final ObstructingPreferredSizeWidget? bottomNavigationBar;
2 years ago
/// Widget to show in the main content area.
///
/// Content can slide under the [navigationBar] when they're translucent.
/// In that case, the child's [BuildContext]'s [MediaQuery] will have a
/// top padding indicating the area of obstructing overlap from the
/// [navigationBar].
final Widget child;
/// The color of the widget that underlies the entire scaffold.
///
/// By default uses [CupertinoTheme]'s `scaffoldBackgroundColor` when null.
final Color? backgroundColor;
/// Whether the [child] should size itself to avoid the window's bottom inset.
///
/// For example, if there is an onscreen keyboard displayed above the
/// scaffold, the body can be resized to avoid overlapping the keyboard, which
/// prevents widgets inside the body from being obscured by the keyboard.
///
/// Defaults to true and cannot be null.
final bool resizeToAvoidBottomInset;
@override
State<IsekaiPageScaffold> createState() => _IsekaiPageScaffoldState();
}
class _IsekaiPageScaffoldState extends State<IsekaiPageScaffold> {
void _handleStatusBarTap() {
final ScrollController? primaryScrollController =
PrimaryScrollController.of(context);
2 years ago
// Only act on the scroll controller if it has any attached scroll positions.
if (primaryScrollController != null && primaryScrollController.hasClients) {
primaryScrollController.animateTo(
0.0,
// Eyeballed from iOS.
duration: const Duration(milliseconds: 500),
curve: Curves.linearToEaseOut,
);
}
}
@override
Widget build(BuildContext context) {
Widget paddedContent = widget.child;
final MediaQueryData existingMediaQuery = MediaQuery.of(context);
final double topPadding;
final bool topFullObstruction;
2 years ago
if (widget.navigationBar != null) {
// Propagate top padding and include viewInsets if appropriate
topPadding = widget.navigationBar!.preferredSize.height +
existingMediaQuery.padding.top;
topFullObstruction = widget.navigationBar!.shouldFullyObstruct(context);
} else {
// Propagate bottom padding and include viewInsets if appropriate
topPadding = existingMediaQuery.padding.top;
topFullObstruction = false;
}
2 years ago
final double bottomPadding;
final bool bottomFullObstruction;
if (widget.bottomNavigationBar != null) {
2 years ago
// Propagate bottom padding and include viewInsets if appropriate
bottomPadding = widget.bottomNavigationBar!.preferredSize.height +
existingMediaQuery.padding.bottom;
bottomFullObstruction =
widget.bottomNavigationBar!.shouldFullyObstruct(context);
2 years ago
} else {
// Propagate bottom padding and include viewInsets if appropriate
bottomPadding = existingMediaQuery.padding.bottom;
bottomFullObstruction = false;
2 years ago
}
final EdgeInsets newViewInsets = existingMediaQuery.viewInsets;
var fixedExistingMediaQuery = existingMediaQuery.removePadding(
removeTop: widget.navigationBar != null && topFullObstruction,
removeBottom: widget.bottomNavigationBar != null && bottomFullObstruction,
);
paddedContent = MediaQuery(
data: fixedExistingMediaQuery.copyWith(
padding: fixedExistingMediaQuery.padding.copyWith(
top: topFullObstruction ? null : topPadding,
bottom: bottomFullObstruction ? null : bottomPadding,
),
viewInsets: newViewInsets,
),
child: Padding(
padding: EdgeInsets.only(
top: (widget.navigationBar == null)
? 0
: (topFullObstruction ? topPadding : 0),
bottom: (widget.bottomNavigationBar == null)
? 0
: (bottomFullObstruction ? bottomPadding : 0),
),
child: paddedContent,
),
);
2 years ago
return DecoratedBox(
decoration: BoxDecoration(
color: CupertinoDynamicColor.maybeResolve(
widget.backgroundColor, context) ??
2 years ago
CupertinoTheme.of(context).scaffoldBackgroundColor,
),
child: Stack(
children: <Widget>[
// The main content being at the bottom is added to the stack first.
paddedContent,
if (widget.navigationBar != null)
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
child: widget.navigationBar!,
),
if (widget.bottomNavigationBar != null)
Positioned(
bottom: 0.0,
left: 0.0,
right: 0.0,
child: widget.bottomNavigationBar!,
),
2 years ago
// Add a touch handler the size of the status bar on top of all contents
// to handle scroll to top by status bar taps.
Positioned(
top: 0.0,
left: 0.0,
right: 0.0,
height: existingMediaQuery.padding.top,
child: GestureDetector(
excludeFromSemantics: true,
onTap: _handleStatusBarTap,
),
),
],
),
);
}
}