mirror of
https://github.com/flutter/samples.git
synced 2026-05-29 18:38:57 +00:00
[Gallery] Fix directory structure (#312)
This commit is contained in:
384
gallery/lib/feature_discovery/feature_discovery.dart
Normal file
384
gallery/lib/feature_discovery/feature_discovery.dart
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright 2019 The Flutter team. 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/scheduler.dart';
|
||||
|
||||
import 'package:gallery/feature_discovery/animation.dart';
|
||||
import 'package:gallery/feature_discovery/overlay.dart';
|
||||
|
||||
/// [Widget] to enforce a global lock system for [FeatureDiscovery] widgets.
|
||||
///
|
||||
/// This widget enforces that at most one [FeatureDiscovery] widget in its
|
||||
/// widget tree is shown at a time.
|
||||
///
|
||||
/// Users wanting to use [FeatureDiscovery] need to put this controller
|
||||
/// above [FeatureDiscovery] widgets in the widget tree.
|
||||
class FeatureDiscoveryController extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
FeatureDiscoveryController(this.child);
|
||||
|
||||
static _FeatureDiscoveryControllerState of(BuildContext context) {
|
||||
final matchResult =
|
||||
context.findAncestorStateOfType<_FeatureDiscoveryControllerState>();
|
||||
if (matchResult != null) {
|
||||
return matchResult;
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'FeatureDiscoveryController.of() called with a context that does not '
|
||||
'contain a FeatureDiscoveryController.\n The context used was:\n '
|
||||
'$context');
|
||||
}
|
||||
|
||||
@override
|
||||
_FeatureDiscoveryControllerState createState() =>
|
||||
_FeatureDiscoveryControllerState();
|
||||
}
|
||||
|
||||
class _FeatureDiscoveryControllerState
|
||||
extends State<FeatureDiscoveryController> {
|
||||
bool _isLocked = false;
|
||||
|
||||
/// Flag to indicate whether a [FeatureDiscovery] widget descendant is
|
||||
/// currently showing its overlay or not.
|
||||
///
|
||||
/// If true, then no other [FeatureDiscovery] widget should display its
|
||||
/// overlay.
|
||||
bool get isLocked => _isLocked;
|
||||
|
||||
/// Lock the controller.
|
||||
///
|
||||
/// Note we do not [setState] here because this function will be called
|
||||
/// by the first [FeatureDiscovery] ready to show its overlay, and any
|
||||
/// additional [FeatureDiscovery] widgets wanting to show their overlays
|
||||
/// will already be scheduled to be built, so the lock change will be caught
|
||||
/// in their builds.
|
||||
void lock() => _isLocked = true;
|
||||
|
||||
/// Unlock the controller.
|
||||
void unlock() => setState(() => _isLocked = false);
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
assert(
|
||||
context.findAncestorStateOfType<_FeatureDiscoveryControllerState>() ==
|
||||
null,
|
||||
'There should not be another ancestor of type '
|
||||
'FeatureDiscoveryController in the widget tree.',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
/// Widget that highlights the [child] with an overlay.
|
||||
///
|
||||
/// This widget loosely follows the guidelines set forth in the Material Specs:
|
||||
/// https://material.io/archive/guidelines/growth-communications/feature-discovery.html.
|
||||
class FeatureDiscovery extends StatefulWidget {
|
||||
/// Title to be displayed in the overlay.
|
||||
final String title;
|
||||
|
||||
/// Description to be displayed in the overlay.
|
||||
final String description;
|
||||
|
||||
/// Icon to be promoted.
|
||||
final Icon child;
|
||||
|
||||
/// Flag to indicate whether to show the overlay or not anchored to the
|
||||
/// [child].
|
||||
final bool showOverlay;
|
||||
|
||||
/// Callback invoked when the user dismisses an overlay.
|
||||
final void Function() onDismiss;
|
||||
|
||||
/// Callback invoked when the user taps on the tap target of an overlay.
|
||||
final void Function() onTap;
|
||||
|
||||
/// Color with which to fill the outer circle.
|
||||
final Color color;
|
||||
|
||||
@visibleForTesting
|
||||
static final overlayKey = Key('overlay key');
|
||||
|
||||
@visibleForTesting
|
||||
static final gestureDetectorKey = Key('gesture detector key');
|
||||
|
||||
FeatureDiscovery({
|
||||
@required this.title,
|
||||
@required this.description,
|
||||
@required this.child,
|
||||
@required this.showOverlay,
|
||||
this.onDismiss,
|
||||
this.onTap,
|
||||
this.color,
|
||||
}) {
|
||||
assert(title != null);
|
||||
assert(description != null);
|
||||
assert(child != null);
|
||||
assert(showOverlay != null);
|
||||
}
|
||||
|
||||
@override
|
||||
_FeatureDiscoveryState createState() => _FeatureDiscoveryState();
|
||||
}
|
||||
|
||||
class _FeatureDiscoveryState extends State<FeatureDiscovery>
|
||||
with TickerProviderStateMixin {
|
||||
bool showOverlay = false;
|
||||
FeatureDiscoveryStatus status = FeatureDiscoveryStatus.closed;
|
||||
|
||||
AnimationController openController;
|
||||
AnimationController rippleController;
|
||||
AnimationController tapController;
|
||||
AnimationController dismissController;
|
||||
|
||||
Animations animations;
|
||||
OverlayEntry overlay;
|
||||
|
||||
Widget buildOverlay(BuildContext ctx, Offset center) {
|
||||
debugCheckHasMediaQuery(ctx);
|
||||
debugCheckHasDirectionality(ctx);
|
||||
|
||||
final deviceSize = MediaQuery.of(ctx).size;
|
||||
final color = widget.color ?? Theme.of(ctx).primaryColor;
|
||||
|
||||
// Wrap in transparent [Material] to enable widgets that require one.
|
||||
return Material(
|
||||
key: FeatureDiscovery.overlayKey,
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
key: FeatureDiscovery.gestureDetectorKey,
|
||||
onTap: dismiss,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: Colors.transparent,
|
||||
),
|
||||
),
|
||||
Background(
|
||||
animations: animations,
|
||||
status: status,
|
||||
color: color,
|
||||
center: center,
|
||||
deviceSize: deviceSize,
|
||||
textDirection: Directionality.of(ctx),
|
||||
),
|
||||
Content(
|
||||
animations: animations,
|
||||
status: status,
|
||||
center: center,
|
||||
deviceSize: deviceSize,
|
||||
title: widget.title,
|
||||
description: widget.description,
|
||||
textTheme: Theme.of(ctx).textTheme,
|
||||
),
|
||||
Ripple(
|
||||
animations: animations,
|
||||
status: status,
|
||||
center: center,
|
||||
),
|
||||
TapTarget(
|
||||
animations: animations,
|
||||
status: status,
|
||||
center: center,
|
||||
child: widget.child,
|
||||
onTap: tap,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Method to handle user tap on [TapTarget].
|
||||
///
|
||||
/// Tapping will stop any active controller and start the [tapController].
|
||||
void tap() {
|
||||
openController.stop();
|
||||
rippleController.stop();
|
||||
dismissController.stop();
|
||||
tapController.forward(from: 0.0);
|
||||
}
|
||||
|
||||
/// Method to handle user dismissal.
|
||||
///
|
||||
/// Dismissal will stop any active controller and start the
|
||||
/// [dismissController].
|
||||
void dismiss() {
|
||||
openController.stop();
|
||||
rippleController.stop();
|
||||
tapController.stop();
|
||||
dismissController.forward(from: 0.0);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: (ctx, _) {
|
||||
if (overlay != null) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
// [OverlayEntry] needs to be explicitly rebuilt when necessary.
|
||||
overlay.markNeedsBuild();
|
||||
});
|
||||
} else {
|
||||
if (showOverlay && !FeatureDiscoveryController.of(ctx).isLocked) {
|
||||
final entry = OverlayEntry(
|
||||
builder: (_) => buildOverlay(ctx, getOverlayCenter(ctx)),
|
||||
);
|
||||
|
||||
// Lock [FeatureDiscoveryController] early in order to prevent
|
||||
// another [FeatureDiscovery] widget from trying to show its
|
||||
// overlay while the post frame callback and set state are not
|
||||
// complete.
|
||||
FeatureDiscoveryController.of(ctx).lock();
|
||||
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
overlay = entry;
|
||||
status = FeatureDiscoveryStatus.closed;
|
||||
openController.forward(from: 0.0);
|
||||
});
|
||||
Overlay.of(context).insert(entry);
|
||||
});
|
||||
}
|
||||
}
|
||||
return widget.child;
|
||||
});
|
||||
}
|
||||
|
||||
/// Compute the center position of the overlay.
|
||||
Offset getOverlayCenter(BuildContext parentCtx) {
|
||||
final box = parentCtx.findRenderObject() as RenderBox;
|
||||
final size = box.size;
|
||||
final topLeftPosition = box.localToGlobal(Offset.zero);
|
||||
final centerPosition = Offset(
|
||||
topLeftPosition.dx + size.width / 2,
|
||||
topLeftPosition.dy + size.height / 2,
|
||||
);
|
||||
return centerPosition;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
initAnimationControllers();
|
||||
initAnimations();
|
||||
showOverlay = widget.showOverlay;
|
||||
}
|
||||
|
||||
void initAnimationControllers() {
|
||||
openController = AnimationController(
|
||||
duration: const Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
})
|
||||
..addStatusListener((animationStatus) {
|
||||
if (animationStatus == AnimationStatus.forward) {
|
||||
setState(() => status = FeatureDiscoveryStatus.open);
|
||||
} else if (animationStatus == AnimationStatus.completed) {
|
||||
rippleController.forward(from: 0.0);
|
||||
}
|
||||
});
|
||||
|
||||
rippleController = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
})
|
||||
..addStatusListener((animationStatus) {
|
||||
if (animationStatus == AnimationStatus.forward) {
|
||||
setState(() => status = FeatureDiscoveryStatus.ripple);
|
||||
} else if (animationStatus == AnimationStatus.completed) {
|
||||
rippleController.forward(from: 0.0);
|
||||
}
|
||||
});
|
||||
|
||||
tapController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
vsync: this,
|
||||
)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
})
|
||||
..addStatusListener((animationStatus) {
|
||||
if (animationStatus == AnimationStatus.forward) {
|
||||
setState(() => status = FeatureDiscoveryStatus.tap);
|
||||
} else if (animationStatus == AnimationStatus.completed) {
|
||||
widget.onTap?.call();
|
||||
cleanUponOverlayClose();
|
||||
}
|
||||
});
|
||||
|
||||
dismissController = AnimationController(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
vsync: this,
|
||||
)
|
||||
..addListener(() {
|
||||
setState(() {});
|
||||
})
|
||||
..addStatusListener((animationStatus) {
|
||||
if (animationStatus == AnimationStatus.forward) {
|
||||
setState(() => status = FeatureDiscoveryStatus.dismiss);
|
||||
} else if (animationStatus == AnimationStatus.completed) {
|
||||
widget.onDismiss?.call();
|
||||
cleanUponOverlayClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void initAnimations() {
|
||||
assert(openController != null);
|
||||
assert(rippleController != null);
|
||||
assert(tapController != null);
|
||||
assert(dismissController != null);
|
||||
|
||||
animations = Animations(
|
||||
openController,
|
||||
tapController,
|
||||
rippleController,
|
||||
dismissController,
|
||||
);
|
||||
}
|
||||
|
||||
/// Clean up once overlay has been dismissed or tap target has been tapped.
|
||||
///
|
||||
/// This is called upon [tapController] and [dismissController] end.
|
||||
void cleanUponOverlayClose() {
|
||||
FeatureDiscoveryController.of(context).unlock();
|
||||
setState(() {
|
||||
status = FeatureDiscoveryStatus.closed;
|
||||
showOverlay = false;
|
||||
overlay?.remove();
|
||||
overlay = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(FeatureDiscovery oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.showOverlay != oldWidget.showOverlay) {
|
||||
showOverlay = widget.showOverlay;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
overlay?.remove();
|
||||
openController?.dispose();
|
||||
rippleController?.dispose();
|
||||
tapController?.dispose();
|
||||
dismissController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user