|
|
|
// 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,
|
|
|
|
this.backgroundColor,
|
|
|
|
this.resizeToAvoidBottomInset = false,
|
|
|
|
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;
|
|
|
|
|
|
|
|
/// 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);
|
|
|
|
// 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;
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
final double bottomPadding;
|
|
|
|
final bool bottomFullObstruction;
|
|
|
|
if (widget.bottomNavigationBar != null) {
|
|
|
|
// Propagate bottom padding and include viewInsets if appropriate
|
|
|
|
bottomPadding = widget.bottomNavigationBar!.preferredSize.height +
|
|
|
|
existingMediaQuery.padding.bottom;
|
|
|
|
|
|
|
|
bottomFullObstruction =
|
|
|
|
widget.bottomNavigationBar!.shouldFullyObstruct(context);
|
|
|
|
} else {
|
|
|
|
// Propagate bottom padding and include viewInsets if appropriate
|
|
|
|
bottomPadding = existingMediaQuery.padding.bottom;
|
|
|
|
|
|
|
|
bottomFullObstruction = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
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,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
return DecoratedBox(
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: CupertinoDynamicColor.maybeResolve(
|
|
|
|
widget.backgroundColor, context) ??
|
|
|
|
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!,
|
|
|
|
),
|
|
|
|
// 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,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|