Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/tip navigation: New optional navigation options for the Showcase() widget #273

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
16 changes: 16 additions & 0 deletions lib/src/showcase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class Showcase extends StatefulWidget {
final VoidCallback? onTargetDoubleTap;
final VoidCallback? onTargetLongPress;
final BorderRadius? tipBorderRadius;
final bool showForwardBackNav;
final bool showTipCountIndex;
final bool showEndIcon;

/// if disableDefaultTargetGestures parameter is true
/// onTargetClick, onTargetDoubleTap, onTargetLongPress and
Expand Down Expand Up @@ -107,6 +110,9 @@ class Showcase extends StatefulWidget {
this.onTargetDoubleTap,
this.tipBorderRadius,
this.disableDefaultTargetGestures = false,
this.showForwardBackNav = false,
this.showTipCountIndex = false,
this.showEndIcon = false,
}) : height = null,
width = null,
container = null,
Expand All @@ -123,6 +129,9 @@ class Showcase extends StatefulWidget {
: (onTargetClick == null ? false : true),
"onTargetClick is required if you're using disposeOnTap");

/// Showcase.withWidget allows a widget to be passed to the tooltip instead of just a description String
/// It is expected that a user would build their own implementation of forward / back [showForwardBackNav],
/// tip count [showTipCountIndex], and end icon [showEndIcon] with this constructor.
const Showcase.withWidget({
required this.key,
required this.child,
Expand Down Expand Up @@ -154,6 +163,9 @@ class Showcase extends StatefulWidget {
this.disableDefaultTargetGestures = false,
}) : showArrow = false,
onToolTipClick = null,
showForwardBackNav = false,
showTipCountIndex = false,
showEndIcon = false,
assert(overlayOpacity >= 0.0 && overlayOpacity <= 1.0,
"overlay opacity must be between 0 and 1.");

Expand Down Expand Up @@ -335,6 +347,7 @@ class _ShowcaseState extends State<Showcase> {
),
if (!_isScrollRunning)
ToolTipWidget(
globalKey: widget.key,
position: position,
offset: offset,
screenSize: screenSize,
Expand All @@ -354,6 +367,9 @@ class _ShowcaseState extends State<Showcase> {
showCaseWidgetState.disableAnimation,
animationDuration: widget.animationDuration,
borderRadius: widget.tipBorderRadius,
showForwardBackNav: widget.showForwardBackNav,
showTipCountIndex: widget.showTipCountIndex,
showEndIcon: widget.showEndIcon,
),
],
)
Expand Down
159 changes: 117 additions & 42 deletions lib/src/tooltip_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,19 @@ import 'dart:math';

import 'package:flutter/material.dart';

import '../showcaseview.dart';
import 'get_position.dart';
import 'measure_size.dart';
import 'tooltip_widget_nav.dart';

const _kDefaultPaddingFromParent = 14.0;
const _kEndIconPaddingAll = 2.0;
const _kEndIconSize = 16.0;
const _kEndIconTotalWidth = _kEndIconSize + _kEndIconPaddingAll;
const _kMinRightPaddingIfEndIconEnabled = 2.0;

class ToolTipWidget extends StatefulWidget {
final GlobalKey globalKey;
final GetPosition? position;
final Offset? offset;
final Size? screenSize;
Expand All @@ -48,9 +55,13 @@ class ToolTipWidget extends StatefulWidget {
final Duration animationDuration;
final bool disableAnimation;
final BorderRadius? borderRadius;
final bool showForwardBackNav;
final bool showTipCountIndex;
final bool showEndIcon;

const ToolTipWidget({
Key? key,
required this.globalKey,
required this.position,
required this.offset,
required this.screenSize,
Expand All @@ -69,6 +80,9 @@ class ToolTipWidget extends StatefulWidget {
this.contentPadding = const EdgeInsets.symmetric(vertical: 8),
required this.disableAnimation,
required this.borderRadius,
required this.showForwardBackNav,
required this.showTipCountIndex,
required this.showEndIcon,
}) : super(key: key);

@override
Expand All @@ -78,7 +92,6 @@ class ToolTipWidget extends StatefulWidget {
class _ToolTipWidgetState extends State<ToolTipWidget>
with SingleTickerProviderStateMixin {
Offset? position;

bool isArrowUp = false;

late final AnimationController _parentController;
Expand Down Expand Up @@ -130,6 +143,9 @@ class _ToolTipWidgetState extends State<ToolTipWidget>
widget.contentPadding!.right +
widget.contentPadding!.left);
var maxTextWidth = max(titleLength, descriptionLength);
if (widget.showEndIcon) {
maxTextWidth += _kEndIconTotalWidth;
}
if (maxTextWidth > widget.screenSize!.width - tooltipScreenEdgePadding) {
tooltipWidth = widget.screenSize!.width - tooltipScreenEdgePadding;
} else {
Expand Down Expand Up @@ -157,8 +173,13 @@ class _ToolTipWidgetState extends State<ToolTipWidget>
if (widget.position != null) {
var rightPosition = widget.position!.getCenter() + (tooltipWidth * 0.5);

// Accommodate end icon offset width if enabled
double minRightPadding = (widget.showEndIcon)
? _kMinRightPaddingIfEndIconEnabled
: _kDefaultPaddingFromParent;

return (rightPosition + tooltipWidth) > MediaQuery.of(context).size.width
? _kDefaultPaddingFromParent
? minRightPadding
: null;
}
return null;
Expand Down Expand Up @@ -238,6 +259,7 @@ class _ToolTipWidgetState extends State<ToolTipWidget>

const arrowWidth = 18.0;
const arrowHeight = 9.0;
const defaultBorderRadius = 8.0;

if (widget.container == null) {
return Positioned(
Expand Down Expand Up @@ -293,60 +315,113 @@ class _ToolTipWidgetState extends State<ToolTipWidget>
),
),
),
Padding(
Container(
padding: EdgeInsets.only(
top: isArrowUp ? arrowHeight - 1 : 0,
bottom: isArrowUp ? 0 : arrowHeight - 1,
),
child: ClipRRect(
borderRadius:
widget.borderRadius ?? BorderRadius.circular(8.0),
child: GestureDetector(
onTap: widget.onTooltipTap,
child: Container(
width: tooltipWidth,
padding: widget.contentPadding,
color: widget.tooltipColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Column(
crossAxisAlignment: widget.title != null
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: <Widget>[
widget.title != null
? Text(
widget.title!,
style: widget.titleTextStyle ??
child: Stack(
children: [
Padding(
// Conditional container padding to accommodate an offset endIcon
padding: EdgeInsets.only(
right: (widget.showEndIcon)
? _kEndIconTotalWidth / 2
: 0),
child: ClipRRect(
borderRadius: widget.borderRadius ??
BorderRadius.circular(defaultBorderRadius),
child: GestureDetector(
onTap: widget.onTooltipTap,
child: Container(
width: tooltipWidth,
padding: widget.contentPadding,
color: widget.tooltipColor,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.center,
children: <Widget>[
Column(
crossAxisAlignment: widget.title != null
? CrossAxisAlignment.start
: CrossAxisAlignment.center,
children: <Widget>[
widget.title != null
? Text(
widget.title!,
style:
widget.titleTextStyle ??
Theme.of(context)
.textTheme
.headline6!
.merge(
TextStyle(
color: widget
.textColor,
),
),
)
: const SizedBox(),
Text(
widget.description!,
style: widget.descTextStyle ??
Theme.of(context)
.textTheme
.headline6!
.subtitle2!
.merge(
TextStyle(
color: widget.textColor,
),
),
),
TooltipWidgetNav(
globalKey: widget.globalKey,
showForwardBackNav:
widget.showForwardBackNav,
showTipCountIndex:
widget.showTipCountIndex,
textColor: widget.textColor,
)
: const SizedBox(),
Text(
widget.description!,
style: widget.descTextStyle ??
Theme.of(context)
.textTheme
.subtitle2!
.merge(
TextStyle(
color: widget.textColor,
),
),
),
],
)
],
],
)
],
),
),
),
),
),
),
if (widget.showEndIcon)
Positioned(
right: 0,
top: (isArrowUp) ? arrowHeight - 1 : 0,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
ShowCaseWidget.of(
widget.globalKey.currentContext!)
.dismiss();
},
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.zero,
topRight: Radius.circular(50),
bottomRight: Radius.circular(50),
bottomLeft: Radius.circular(50),
),
child: Container(
padding: const EdgeInsets.all(
_kEndIconPaddingAll),
color: widget.tooltipColor,
child: Icon(
Icons.close,
size: _kEndIconSize,
color: widget.textColor,
),
),
),
),
),
],
),
),
],
Expand Down
92 changes: 92 additions & 0 deletions lib/src/tooltip_widget_nav.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';

import '../showcaseview.dart';

/// Tooltip Widget Nav
/// Shows tooltip navigation and index / count elements if the conditions are indicated.
class TooltipWidgetNav extends StatelessWidget {
final GlobalKey globalKey;
final bool showForwardBackNav;
final bool showTipCountIndex;
final Color? textColor;
const TooltipWidgetNav({
Key? key,
required this.globalKey,
required this.showForwardBackNav,
required this.showTipCountIndex,
required this.textColor,
}) : super(key: key);

@override
Widget build(BuildContext context) {
var ids = ShowCaseWidget.of(globalKey.currentContext!).ids;
var activeWidgetId =
ShowCaseWidget.of(globalKey.currentContext!).activeWidgetId;
bool isFirstTip = activeWidgetId == 0;

if (showForwardBackNav || showTipCountIndex) {
return Column(
children: [
// const SizedBox(height: 4.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (showForwardBackNav)
GestureDetector(
behavior: HitTestBehavior.translucent,

// Disable if activeWidgetId (index) == 0
onTap: (isFirstTip)
? null
: () {
ShowCaseWidget.of(globalKey.currentContext!)
.previous();
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0, top: 4.0),
child: Icon(
Icons.keyboard_arrow_left,
color: (isFirstTip)
? textColor?.withOpacity(0.3) ?? Colors.black26
: textColor,
),
),
),
if (showTipCountIndex &&
ids != null &&
activeWidgetId != null) ...[
const SizedBox(width: 4.0),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(
"${activeWidgetId + 1} / ${ids.length}",
style: TextStyle(
color: textColor,
),
),
),
const SizedBox(width: 4.0),
],
if (showForwardBackNav)
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
ShowCaseWidget.of(globalKey.currentContext!).next();
},
child: Padding(
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
child: Icon(
Icons.keyboard_arrow_right,
color: textColor,
),
),
),
],
),
],
);
} else {
return const SizedBox();
}
}
}