mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

This auto-formats all *.dart files in the repository outside of the `engine` subdirectory and enforces that these files stay formatted with a presubmit check. **Reviewers:** Please carefully review all the commits except for the one titled "formatted". The "formatted" commit was auto-generated by running `dev/tools/format.sh -a -f`. The other commits were hand-crafted to prepare the repo for the formatting change. I recommend reviewing the commits one-by-one via the "Commits" tab and avoiding Github's "Files changed" tab as it will likely slow down your browser because of the size of this PR. --------- Co-authored-by: Kate Lovett <katelovett@google.com> Co-authored-by: LongCatIsLooong <31859944+LongCatIsLooong@users.noreply.github.com>
196 lines
6.3 KiB
Dart
196 lines
6.3 KiB
Dart
// 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/rendering.dart';
|
|
|
|
void main() {
|
|
runApp(const SettingsAppBarApp());
|
|
}
|
|
|
|
class SettingsAppBarApp extends StatelessWidget {
|
|
const SettingsAppBarApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const MaterialApp(home: SettingsAppBarExample());
|
|
}
|
|
}
|
|
|
|
class SettingsAppBarExample extends StatefulWidget {
|
|
const SettingsAppBarExample({super.key});
|
|
|
|
@override
|
|
State<SettingsAppBarExample> createState() => _SettingsAppBarExampleState();
|
|
}
|
|
|
|
class _SettingsAppBarExampleState extends State<SettingsAppBarExample> {
|
|
final GlobalKey headerSliverKey = GlobalKey();
|
|
final GlobalKey titleSliverKey = GlobalKey();
|
|
late final ScrollController scrollController;
|
|
double headerOpacity = 0;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
scrollController = ScrollController();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// The key must be for a widget _below_ a RenderSliver so that
|
|
// findAncestorRenderObjectOfType can find the RenderSliver when it searches
|
|
// the key widget's renderer ancestors.
|
|
RenderSliver? keyToSliver(GlobalKey key) =>
|
|
key.currentContext?.findAncestorRenderObjectOfType<RenderSliver>();
|
|
|
|
// Each time the app's list scrolls: if the Title sliver has scrolled completely behind
|
|
// the (pinned) header sliver, then change the header's opacity from 0 to 1.
|
|
//
|
|
// The header RenderSliver's SliverConstraints.scrollOffset is the distance
|
|
// above the top of the viewport where the top of header sliver would appear
|
|
// if it were laid out normally. Since it's a pinned sliver, it's unconditionally
|
|
// painted at the top of the viewport, even though its scrollOffset constraint
|
|
// increases as the user scrolls upwards. The "Settings" title RenderSliver's
|
|
// scrollExtent is the vertical space it wants to occupy. It doesn't change as
|
|
// the user scrolls.
|
|
bool handleScrollNotification(ScrollNotification notification) {
|
|
final RenderSliver? headerSliver = keyToSliver(headerSliverKey);
|
|
final RenderSliver? titleSliver = keyToSliver(titleSliverKey);
|
|
if (headerSliver != null && titleSliver != null && titleSliver.geometry != null) {
|
|
final double opacity =
|
|
headerSliver.constraints.scrollOffset > titleSliver.geometry!.scrollExtent ? 1 : 0;
|
|
if (opacity != headerOpacity) {
|
|
setState(() {
|
|
headerOpacity = opacity;
|
|
});
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
const EdgeInsets horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
|
|
final ThemeData theme = Theme.of(context);
|
|
final TextTheme textTheme = theme.textTheme;
|
|
final ColorScheme colorScheme = theme.colorScheme;
|
|
|
|
return Scaffold(
|
|
backgroundColor: colorScheme.surfaceContainer,
|
|
body: SafeArea(
|
|
child: NotificationListener<ScrollNotification>(
|
|
onNotification: handleScrollNotification,
|
|
child: CustomScrollView(
|
|
controller: scrollController,
|
|
slivers: <Widget>[
|
|
PinnedHeaderSliver(
|
|
child: Header(
|
|
key: headerSliverKey,
|
|
opacity: headerOpacity,
|
|
child: Text('Settings', style: textTheme.titleMedium),
|
|
),
|
|
),
|
|
SliverPadding(
|
|
padding: horizontalPadding,
|
|
sliver: SliverToBoxAdapter(
|
|
child: TitleItem(
|
|
key: titleSliverKey,
|
|
child: Text(
|
|
'Settings',
|
|
style: textTheme.titleLarge!.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 32,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SliverPadding(padding: horizontalPadding, sliver: ItemList()),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The pinned item at the top of the list. This is an implicitly
|
|
// animated widget: when the opacity changes the title and divider
|
|
// fade in or out.
|
|
class Header extends StatelessWidget {
|
|
const Header({super.key, required this.opacity, required this.child});
|
|
|
|
final double opacity;
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ThemeData theme = Theme.of(context);
|
|
final ColorScheme colorScheme = theme.colorScheme;
|
|
|
|
return AnimatedContainer(
|
|
duration: const Duration(milliseconds: 300),
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
decoration: ShapeDecoration(
|
|
color: opacity == 0 ? colorScheme.surfaceContainer : colorScheme.surfaceContainerLowest,
|
|
shape: LinearBorder.bottom(
|
|
side: BorderSide(
|
|
color:
|
|
opacity == 0 ? colorScheme.surfaceContainer : colorScheme.surfaceContainerHighest,
|
|
),
|
|
),
|
|
),
|
|
alignment: Alignment.center,
|
|
child: AnimatedOpacity(
|
|
opacity: opacity,
|
|
duration: const Duration(milliseconds: 300),
|
|
child: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// The second item in the list. It scrolls normally. When it has scrolled
|
|
// completely out of view behind the first, pinned, Header item, the Header
|
|
// fades in.
|
|
class TitleItem extends StatelessWidget {
|
|
const TitleItem({super.key, required this.child});
|
|
|
|
final Widget child;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Container(
|
|
alignment: AlignmentDirectional.bottomStart,
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: child,
|
|
);
|
|
}
|
|
}
|
|
|
|
// A placeholder SliverList of 50 items.
|
|
class ItemList extends StatelessWidget {
|
|
const ItemList({super.key, this.itemCount = 50});
|
|
|
|
final int itemCount;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
|
return SliverList(
|
|
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
|
|
return Card(
|
|
color: colorScheme.onSecondary,
|
|
child: ListTile(textColor: colorScheme.secondary, title: Text('Item $index')),
|
|
);
|
|
}, childCount: itemCount),
|
|
);
|
|
}
|
|
}
|