1
0
mirror of https://github.com/flutter/samples.git synced 2025-11-12 07:48:55 +00:00

[Gallery] Fix directory structure (#312)

This commit is contained in:
Pierre-Louis
2020-02-05 20:11:54 +01:00
committed by GitHub
parent 082592e9a9
commit cee267cf88
762 changed files with 12 additions and 12 deletions

View File

@@ -0,0 +1,247 @@
// 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';
/// Animations class to compute animation values for overlay widgets.
///
/// Values are loosely based on Material Design specs, which are minimal.
class Animations {
final AnimationController openController;
final AnimationController tapController;
final AnimationController rippleController;
final AnimationController dismissController;
static const backgroundMaxOpacity = 0.96;
static const backgroundTapRadius = 20.0;
static const rippleMaxOpacity = 0.75;
static const tapTargetToContentDistance = 20.0;
static const tapTargetMaxRadius = 44.0;
static const tapTargetMinRadius = 20.0;
static const tapTargetRippleRadius = 64.0;
Animations(
this.openController,
this.tapController,
this.rippleController,
this.dismissController,
);
Animation<double> backgroundOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: backgroundMaxOpacity)
.animate(CurvedAnimation(
parent: openController,
curve: Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: backgroundMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: backgroundMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: dismissController,
curve: Interval(0.2, 1.0, curve: Curves.ease),
));
default:
return AlwaysStoppedAnimation<double>(backgroundMaxOpacity);
}
}
Animation<double> backgroundRadius(
FeatureDiscoveryStatus status,
double backgroundRadiusMax,
) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: backgroundRadiusMax)
.animate(CurvedAnimation(
parent: openController,
curve: Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(
begin: backgroundRadiusMax,
end: backgroundRadiusMax + backgroundTapRadius)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: backgroundRadiusMax, end: 0)
.animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
default:
return AlwaysStoppedAnimation<double>(backgroundRadiusMax);
}
}
Animation<Offset> backgroundCenter(
FeatureDiscoveryStatus status,
Offset start,
Offset end,
) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<Offset>(start);
case FeatureDiscoveryStatus.open:
return Tween<Offset>(begin: start, end: end).animate(CurvedAnimation(
parent: openController,
curve: Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<Offset>(begin: end, end: start).animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<Offset>(begin: end, end: start).animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
default:
return AlwaysStoppedAnimation<Offset>(end);
}
}
Animation<double> contentOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: openController,
curve: Interval(0.4, 0.7, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: tapController,
curve: Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: dismissController,
curve: Interval(0, 0.4, curve: Curves.ease),
));
default:
return AlwaysStoppedAnimation<double>(1.0);
}
}
Animation<double> rippleOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.ripple:
return Tween<double>(begin: rippleMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: rippleController,
curve: Interval(0.3, 0.8, curve: Curves.ease),
));
default:
return AlwaysStoppedAnimation<double>(0);
}
}
Animation<double> rippleRadius(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.ripple:
if (rippleController.value >= 0.3 && rippleController.value <= 0.8) {
return Tween<double>(begin: tapTargetMaxRadius, end: 79.0)
.animate(CurvedAnimation(
parent: rippleController,
curve: Interval(0.3, 0.8, curve: Curves.ease),
));
}
return AlwaysStoppedAnimation<double>(tapTargetMaxRadius);
default:
return AlwaysStoppedAnimation<double>(0);
}
}
Animation<double> tapTargetOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: openController,
curve: Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: tapController,
curve: Interval(0.1, 0.6, curve: Curves.ease),
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: dismissController,
curve: Interval(0.2, 0.8, curve: Curves.ease),
));
default:
return AlwaysStoppedAnimation<double>(1.0);
}
}
Animation<double> tapTargetRadius(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<double>(tapTargetMinRadius);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: tapTargetMinRadius, end: tapTargetMaxRadius)
.animate(CurvedAnimation(
parent: openController,
curve: Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.ripple:
if (rippleController.value < 0.3) {
return Tween<double>(
begin: tapTargetMaxRadius, end: tapTargetRippleRadius)
.animate(CurvedAnimation(
parent: rippleController,
curve: Interval(0, 0.3, curve: Curves.ease),
));
} else if (rippleController.value < 0.6) {
return Tween<double>(
begin: tapTargetRippleRadius, end: tapTargetMaxRadius)
.animate(CurvedAnimation(
parent: rippleController,
curve: Interval(0.3, 0.6, curve: Curves.ease),
));
}
return AlwaysStoppedAnimation<double>(tapTargetMaxRadius);
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: tapTargetMaxRadius, end: tapTargetMinRadius)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: tapTargetMaxRadius, end: tapTargetMinRadius)
.animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
default:
return AlwaysStoppedAnimation<double>(tapTargetMaxRadius);
}
}
}
/// Enum to indicate the current status of a [FeatureDiscovery] widget.
enum FeatureDiscoveryStatus {
closed, // Overlay is closed.
open, // Overlay is opening.
ripple, // Overlay is rippling.
tap, // Overlay is tapped.
dismiss, // Overlay is being dismissed.
}

View 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();
}
}

View File

@@ -0,0 +1,401 @@
// 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 'dart:math';
import 'package:flutter/material.dart';
import 'package:gallery/feature_discovery/animation.dart';
const contentHeight = 80.0;
const contentWidth = 300.0;
const contentHorizontalPadding = 40.0;
const tapTargetRadius = 44.0;
const tapTargetToContentDistance = 20.0;
const gutterHeight = 88.0;
/// Background of the overlay.
class Background extends StatelessWidget {
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Color of the background.
final Color color;
/// Device size.
final Size deviceSize;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Directionality of content.
final TextDirection textDirection;
static const horizontalShift = 20.0;
static const padding = 40.0;
Background({
@required this.animations,
@required this.center,
@required this.color,
@required this.deviceSize,
@required this.status,
@required this.textDirection,
}) {
assert(animations != null);
assert(center != null);
assert(color != null);
assert(deviceSize != null);
assert(status != null);
assert(textDirection != null);
}
/// Compute the center position of the background.
///
/// If [center] is near the top or bottom edges of the screen, then
/// background is centered there.
/// Otherwise, background center is calculated and upon opening, animated
/// from [center] to the new calculated position.
Offset get centerPosition {
if (_isNearTopOrBottomEdges(center, deviceSize)) {
return center;
} else {
final start = center;
// dy of centerPosition is calculated to be the furthest point in
// [Content] from the [center].
double endY;
if (_isOnTopHalfOfScreen(center, deviceSize)) {
endY = center.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
if (endY < 0.0) {
endY = center.dy + tapTargetRadius + tapTargetToContentDistance;
}
} else {
endY = center.dy + tapTargetRadius + tapTargetToContentDistance;
if (endY + contentHeight > deviceSize.height) {
endY = center.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
}
}
// Horizontal background center shift based on whether the tap target is
// on the left, center, or right side of the screen.
double shift;
if (_isOnLeftHalfOfScreen(center, deviceSize)) {
shift = horizontalShift;
} else if (center.dx == deviceSize.width / 2) {
shift = textDirection == TextDirection.ltr
? -horizontalShift
: horizontalShift;
} else {
shift = -horizontalShift;
}
// dx of centerPosition is calculated to be the middle point of the
// [Content] bounds shifted by [horizontalShift].
final textBounds = _getContentBounds(deviceSize, center);
final left = min(textBounds.left, center.dx - 88.0);
final right = max(textBounds.right, center.dx + 88.0);
final endX = (left + right) / 2 + shift;
final end = Offset(endX, endY);
return animations.backgroundCenter(status, start, end).value;
}
}
/// Compute the radius.
///
/// Radius is a function of the greatest distance from [center] to one of
/// the corners of [Content].
double get radius {
final textBounds = _getContentBounds(deviceSize, center);
final textRadius = _maxDistance(center, textBounds) + padding;
if (_isNearTopOrBottomEdges(center, deviceSize)) {
return animations.backgroundRadius(status, textRadius).value;
} else {
// Scale down radius if icon is towards the middle of the screen.
return animations.backgroundRadius(status, textRadius).value * 0.8;
}
}
double get opacity => animations.backgroundOpacity(status).value;
@override
Widget build(BuildContext context) {
return Positioned(
left: centerPosition.dx,
top: centerPosition.dy,
child: FractionalTranslation(
translation: Offset(-0.5, -0.5),
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
),
));
}
/// Compute the maximum distance from [point] to the four corners of [bounds].
double _maxDistance(Offset point, Rect bounds) {
double distance(double x1, double y1, double x2, double y2) {
return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}
final tl = distance(point.dx, point.dy, bounds.left, bounds.top);
final tr = distance(point.dx, point.dy, bounds.right, bounds.top);
final bl = distance(point.dx, point.dy, bounds.left, bounds.bottom);
final br = distance(point.dx, point.dy, bounds.right, bounds.bottom);
return max(tl, max(tr, max(bl, br)));
}
}
/// Widget that represents the text to show in the overlay.
class Content extends StatelessWidget {
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Description.
final String description;
/// Device size.
final Size deviceSize;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Title.
final String title;
/// [TextTheme] to use for drawing the [title] and the [description].
final TextTheme textTheme;
Content({
@required this.animations,
@required this.center,
@required this.description,
@required this.deviceSize,
@required this.status,
@required this.title,
@required this.textTheme,
}) {
assert(animations != null);
assert(center != null);
assert(description != null);
assert(deviceSize != null);
assert(status != null);
assert(title != null);
assert(textTheme != null);
}
double get opacity => animations.contentOpacity(status).value;
@override
Widget build(BuildContext context) {
final position = _getContentBounds(deviceSize, center);
return Positioned(
left: position.left,
height: position.bottom - position.top,
width: position.right - position.left,
top: position.top,
child: Opacity(
opacity: opacity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTitle(textTheme),
SizedBox(height: 12.0),
_buildDescription(textTheme),
],
),
),
);
}
Widget _buildTitle(TextTheme theme) {
return Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.title.copyWith(color: Colors.white),
);
}
Widget _buildDescription(TextTheme theme) {
return Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.subhead.copyWith(color: Colors.white70),
);
}
}
/// Widget that represents the ripple effect of [TapTarget].
class Ripple extends StatelessWidget {
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
Ripple({
@required this.animations,
@required this.center,
@required this.status,
}) {
assert(animations != null);
assert(center != null);
assert(status != null);
}
double get radius => animations.rippleRadius(status).value;
double get opacity => animations.rippleOpacity(status).value;
@override
Widget build(BuildContext context) {
return Positioned(
left: center.dx,
top: center.dy,
child: FractionalTranslation(
translation: Offset(-0.5, -0.5),
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
);
}
}
/// Wrapper widget around [child] representing the anchor of the overlay.
class TapTarget extends StatelessWidget {
/// Animations.
final Animations animations;
/// Device size.
final Offset center;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Callback invoked when the user taps on the [TapTarget].
final void Function() onTap;
/// Child widget that will be promoted by the overlay.
final Icon child;
TapTarget({
@required this.animations,
@required this.center,
@required this.status,
@required this.onTap,
@required this.child,
}) {
assert(animations != null);
assert(center != null);
assert(status != null);
assert(onTap != null);
assert(child != null);
}
double get radius => animations.tapTargetRadius(status).value;
double get opacity => animations.tapTargetOpacity(status).value;
@override
Widget build(BuildContext context) {
return Positioned(
left: center.dx,
top: center.dy,
child: FractionalTranslation(
translation: Offset(-0.5, -0.5),
child: InkWell(
onTap: onTap,
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: child,
),
),
),
),
);
}
}
/// Method to compute the bounds of the content.
///
/// This is exposed so it can be used for calculating the background radius
/// and center and for laying out the content.
Rect _getContentBounds(Size deviceSize, Offset overlayCenter) {
double top;
if (_isOnTopHalfOfScreen(overlayCenter, deviceSize)) {
top = overlayCenter.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
if (top < 0) {
top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance;
}
} else {
top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance;
if (top + contentHeight > deviceSize.height) {
top = overlayCenter.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
}
}
final left = max(contentHorizontalPadding, overlayCenter.dx - contentWidth);
final right =
min(deviceSize.width - contentHorizontalPadding, left + contentWidth);
return Rect.fromLTRB(left, top, right, top + contentHeight);
}
bool _isNearTopOrBottomEdges(Offset position, Size deviceSize) {
return position.dy <= gutterHeight ||
(deviceSize.height - position.dy) <= gutterHeight;
}
bool _isOnTopHalfOfScreen(Offset position, Size deviceSize) {
return position.dy < (deviceSize.height / 2.0);
}
bool _isOnLeftHalfOfScreen(Offset position, Size deviceSize) {
return position.dx < (deviceSize.width / 2.0);
}