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

Some cause of jank on both Skia and Impeller are these massive images. We should just resize them so that the noise is reduced and we can see other performance issues. Fixes https://github.com/flutter/flutter/issues/147797 Tested: this is a test!
395 lines
13 KiB
Dart
395 lines
13 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/services.dart';
|
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
|
|
import '../../data/gallery_options.dart';
|
|
import '../../gallery_localizations.dart';
|
|
import '../../layout/adaptive.dart';
|
|
import '../../layout/image_placeholder.dart';
|
|
import 'backlayer.dart';
|
|
import 'border_tab_indicator.dart';
|
|
import 'colors.dart';
|
|
import 'header_form.dart';
|
|
import 'item_cards.dart';
|
|
import 'model/data.dart';
|
|
import 'model/destination.dart';
|
|
|
|
class _FrontLayer extends StatefulWidget {
|
|
const _FrontLayer({
|
|
required this.title,
|
|
required this.index,
|
|
required this.mobileTopOffset,
|
|
required this.restorationId,
|
|
});
|
|
|
|
final String title;
|
|
final int index;
|
|
final double mobileTopOffset;
|
|
final String restorationId;
|
|
|
|
@override
|
|
_FrontLayerState createState() => _FrontLayerState();
|
|
}
|
|
|
|
class _FrontLayerState extends State<_FrontLayer> {
|
|
List<Destination>? destinations;
|
|
|
|
static const double frontLayerBorderRadius = 16.0;
|
|
static const EdgeInsets bottomPadding = EdgeInsets.only(bottom: 120);
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
// We use didChangeDependencies because the initialization involves an
|
|
// InheritedWidget (for localization). However, we don't need to get
|
|
// destinations again when, say, resizing the window.
|
|
if (destinations == null) {
|
|
if (widget.index == 0) {
|
|
destinations = getFlyDestinations(context);
|
|
}
|
|
if (widget.index == 1) {
|
|
destinations = getSleepDestinations(context);
|
|
}
|
|
if (widget.index == 2) {
|
|
destinations = getEatDestinations(context);
|
|
}
|
|
}
|
|
}
|
|
|
|
Widget _header() {
|
|
return Align(
|
|
alignment: AlignmentDirectional.centerStart,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(
|
|
top: 20,
|
|
bottom: 22,
|
|
),
|
|
child: SelectableText(
|
|
widget.title,
|
|
style: Theme.of(context).textTheme.titleSmall,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool isDesktop = isDisplayDesktop(context);
|
|
final bool isSmallDesktop = isDisplaySmallDesktop(context);
|
|
final int crossAxisCount = isDesktop ? 4 : 1;
|
|
|
|
return FocusTraversalGroup(
|
|
policy: ReadingOrderTraversalPolicy(),
|
|
child: Padding(
|
|
padding: isDesktop
|
|
? EdgeInsets.zero
|
|
: EdgeInsets.only(top: widget.mobileTopOffset),
|
|
child: PhysicalShape(
|
|
elevation: 16,
|
|
color: cranePrimaryWhite,
|
|
clipper: const ShapeBorderClipper(
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(frontLayerBorderRadius),
|
|
topRight: Radius.circular(frontLayerBorderRadius),
|
|
),
|
|
),
|
|
),
|
|
child: Padding(
|
|
padding: isDesktop
|
|
? EdgeInsets.symmetric(
|
|
horizontal:
|
|
isSmallDesktop ? appPaddingSmall : appPaddingLarge)
|
|
.add(bottomPadding)
|
|
: const EdgeInsets.symmetric(horizontal: 20).add(bottomPadding),
|
|
child: Column(
|
|
children: <Widget>[
|
|
_header(),
|
|
Expanded(
|
|
child: MasonryGridView.count(
|
|
key: ValueKey<String>('CraneListView-${widget.index}'),
|
|
restorationId: widget.restorationId,
|
|
crossAxisCount: crossAxisCount,
|
|
crossAxisSpacing: 16.0,
|
|
itemBuilder: (BuildContext context, int index) =>
|
|
DestinationCard(destination: destinations![index]),
|
|
itemCount: destinations!.length,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Builds a Backdrop.
|
|
///
|
|
/// A Backdrop widget has two layers, front and back. The front layer is shown
|
|
/// by default, and slides down to show the back layer, from which a user
|
|
/// can make a selection. The user can also configure the titles for when the
|
|
/// front or back layer is showing.
|
|
class Backdrop extends StatefulWidget {
|
|
|
|
const Backdrop({
|
|
super.key,
|
|
required this.frontLayer,
|
|
required this.backLayerItems,
|
|
required this.frontTitle,
|
|
required this.backTitle,
|
|
});
|
|
final Widget frontLayer;
|
|
final List<BackLayerItem> backLayerItems;
|
|
final Widget frontTitle;
|
|
final Widget backTitle;
|
|
|
|
@override
|
|
State<Backdrop> createState() => _BackdropState();
|
|
}
|
|
|
|
class _BackdropState extends State<Backdrop>
|
|
with TickerProviderStateMixin, RestorationMixin {
|
|
final RestorableInt tabIndex = RestorableInt(0);
|
|
late TabController _tabController;
|
|
late Animation<Offset> _flyLayerHorizontalOffset;
|
|
late Animation<Offset> _sleepLayerHorizontalOffset;
|
|
late Animation<Offset> _eatLayerHorizontalOffset;
|
|
|
|
// How much the 'sleep' front layer is vertically offset relative to other
|
|
// front layers, in pixels, with the mobile layout.
|
|
static const double _sleepLayerTopOffset = 60.0;
|
|
|
|
@override
|
|
String get restorationId => 'tab_non_scrollable_demo';
|
|
|
|
@override
|
|
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
|
registerForRestoration(tabIndex, 'tab_index');
|
|
_tabController.index = tabIndex.value;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_tabController = TabController(length: 3, vsync: this);
|
|
_tabController.addListener(() {
|
|
// When the tab controller's value is updated, make sure to update the
|
|
// tab index value, which is state restorable.
|
|
setState(() {
|
|
tabIndex.value = _tabController.index;
|
|
});
|
|
});
|
|
|
|
// Offsets to create a horizontal gap between front layers.
|
|
final Animation<double> tabControllerAnimation = _tabController.animation!;
|
|
|
|
_flyLayerHorizontalOffset = tabControllerAnimation.drive(
|
|
Tween<Offset>(begin: Offset.zero, end: const Offset(-0.05, 0)));
|
|
|
|
_sleepLayerHorizontalOffset = tabControllerAnimation.drive(
|
|
Tween<Offset>(begin: const Offset(0.05, 0), end: Offset.zero));
|
|
|
|
_eatLayerHorizontalOffset = tabControllerAnimation.drive(Tween<Offset>(
|
|
begin: const Offset(0.10, 0), end: const Offset(0.05, 0)));
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_tabController.dispose();
|
|
tabIndex.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _handleTabs(int tabIndex) {
|
|
_tabController.animateTo(tabIndex,
|
|
duration: const Duration(milliseconds: 300));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool isDesktop = isDisplayDesktop(context);
|
|
final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
|
|
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
|
|
return Material(
|
|
color: cranePurple800,
|
|
child: Padding(
|
|
padding: const EdgeInsets.only(top: 12),
|
|
child: FocusTraversalGroup(
|
|
policy: ReadingOrderTraversalPolicy(),
|
|
child: Scaffold(
|
|
backgroundColor: cranePurple800,
|
|
appBar: AppBar(
|
|
automaticallyImplyLeading: false,
|
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
|
elevation: 0,
|
|
titleSpacing: 0,
|
|
flexibleSpace: CraneAppBar(
|
|
tabController: _tabController,
|
|
tabHandler: _handleTabs,
|
|
),
|
|
),
|
|
body: Stack(
|
|
children: <Widget>[
|
|
BackLayer(
|
|
tabController: _tabController,
|
|
backLayerItems: widget.backLayerItems,
|
|
),
|
|
Container(
|
|
margin: EdgeInsets.only(
|
|
top: isDesktop
|
|
? (isDisplaySmallDesktop(context)
|
|
? textFieldHeight * 3
|
|
: textFieldHeight * 2) +
|
|
20 * textScaleFactor / 2
|
|
: 175 + 140 * textScaleFactor / 2,
|
|
),
|
|
// To display the middle front layer higher than the others,
|
|
// we allow the TabBarView to overflow by an offset
|
|
// (doubled because it technically overflows top & bottom).
|
|
// The other front layers are top padded by this offset.
|
|
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
|
|
return OverflowBox(
|
|
maxHeight:
|
|
constraints.maxHeight + _sleepLayerTopOffset * 2,
|
|
child: TabBarView(
|
|
physics: isDesktop
|
|
? const NeverScrollableScrollPhysics()
|
|
: null, // use default TabBarView physics
|
|
controller: _tabController,
|
|
children: <Widget>[
|
|
SlideTransition(
|
|
position: _flyLayerHorizontalOffset,
|
|
child: _FrontLayer(
|
|
title: localizations.craneFlySubhead,
|
|
index: 0,
|
|
mobileTopOffset: _sleepLayerTopOffset,
|
|
restorationId: 'fly-subhead',
|
|
),
|
|
),
|
|
SlideTransition(
|
|
position: _sleepLayerHorizontalOffset,
|
|
child: _FrontLayer(
|
|
title: localizations.craneSleepSubhead,
|
|
index: 1,
|
|
mobileTopOffset: 0,
|
|
restorationId: 'sleep-subhead',
|
|
),
|
|
),
|
|
SlideTransition(
|
|
position: _eatLayerHorizontalOffset,
|
|
child: _FrontLayer(
|
|
title: localizations.craneEatSubhead,
|
|
index: 2,
|
|
mobileTopOffset: _sleepLayerTopOffset,
|
|
restorationId: 'eat-subhead',
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class CraneAppBar extends StatefulWidget {
|
|
|
|
const CraneAppBar({
|
|
super.key,
|
|
this.tabHandler,
|
|
required this.tabController,
|
|
});
|
|
final void Function(int)? tabHandler;
|
|
final TabController tabController;
|
|
|
|
@override
|
|
State<CraneAppBar> createState() => _CraneAppBarState();
|
|
}
|
|
|
|
class _CraneAppBarState extends State<CraneAppBar> {
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool isDesktop = isDisplayDesktop(context);
|
|
final bool isSmallDesktop = isDisplaySmallDesktop(context);
|
|
final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
|
|
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
|
|
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal:
|
|
isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: <Widget>[
|
|
const ExcludeSemantics(
|
|
child: FadeInImagePlaceholder(
|
|
image: ResizeImage(
|
|
AssetImage(
|
|
'crane/logo/logo.png',
|
|
package: 'flutter_gallery_assets',
|
|
),
|
|
width: 40,
|
|
height: 60,
|
|
),
|
|
placeholder: SizedBox(
|
|
width: 40,
|
|
height: 60,
|
|
),
|
|
width: 40,
|
|
height: 60,
|
|
),
|
|
),
|
|
Expanded(
|
|
child: Padding(
|
|
padding: const EdgeInsetsDirectional.only(start: 24),
|
|
child: Theme(
|
|
data: Theme.of(context).copyWith(
|
|
splashColor: Colors.transparent,
|
|
),
|
|
child: TabBar(
|
|
indicator: BorderTabIndicator(
|
|
indicatorHeight: isDesktop ? 28 : 32,
|
|
textScaleFactor: textScaleFactor,
|
|
),
|
|
controller: widget.tabController,
|
|
labelPadding: const EdgeInsets.symmetric(horizontal: 32),
|
|
isScrollable: true,
|
|
// left-align tabs on desktop
|
|
labelStyle: Theme.of(context).textTheme.labelLarge,
|
|
labelColor: cranePrimaryWhite,
|
|
physics: const BouncingScrollPhysics(),
|
|
unselectedLabelColor: cranePrimaryWhite.withOpacity(.6),
|
|
onTap: (int index) => widget.tabController.animateTo(
|
|
index,
|
|
duration: const Duration(milliseconds: 300),
|
|
),
|
|
tabs: <Widget>[
|
|
Tab(text: localizations.craneFly),
|
|
Tab(text: localizations.craneSleep),
|
|
Tab(text: localizations.craneEat),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|