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

Here's another one of my PRs where I hunt for typos across `flutter` repo. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] All existing and new tests are passing. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/wiki/Tree-hygiene#overview [Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene [test-exempt]: https://github.com/flutter/flutter/wiki/Tree-hygiene#tests [Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo [Features we expect every widget to implement]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/wiki/Chat
205 lines
6.4 KiB
Dart
205 lines
6.4 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,
|
|
),
|
|
);
|
|
}
|
|
}
|