Merge pull request #75 from gskinnerTeam/feature/focus_states

Add focus state to AppBtn
This commit is contained in:
Shawn 2023-03-02 10:22:02 -07:00 committed by GitHub
commit 289edfcde4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 144 additions and 114 deletions

View File

@ -124,15 +124,32 @@ class AppBtn extends StatelessWidget {
overlayColor: ButtonStyleButton.allOrNull<Color>(Colors.transparent), // disable default press effect overlayColor: ButtonStyleButton.allOrNull<Color>(Colors.transparent), // disable default press effect
shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape), shape: ButtonStyleButton.allOrNull<OutlinedBorder>(shape),
padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding ?? EdgeInsets.all($styles.insets.md)), padding: ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(padding ?? EdgeInsets.all($styles.insets.md)),
enableFeedback: enableFeedback, enableFeedback: enableFeedback,
); );
Widget button = TextButton( Widget button = _CustomFocusBuilder(
onPressed: onPressed, builder: (context, focus) => Stack(
style: style, children: [
child: DefaultTextStyle( TextButton(
style: DefaultTextStyle.of(context).style.copyWith(color: textColor), onPressed: onPressed,
child: content, style: style,
focusNode: focus,
child: DefaultTextStyle(
style: DefaultTextStyle.of(context).style.copyWith(color: textColor),
child: content,
),
),
if (focus.hasFocus)
Positioned.fill(
child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular($styles.corners.md),
border: Border.all(color: $styles.colors.accent1, width: 3))),
),
)
],
), ),
); );
@ -180,3 +197,20 @@ class _ButtonPressEffectState extends State<_ButtonPressEffect> {
); );
} }
} }
class _CustomFocusBuilder extends StatefulWidget {
const _CustomFocusBuilder({Key? key, required this.builder}) : super(key: key);
final Widget Function(BuildContext context, FocusNode focus) builder;
@override
State<_CustomFocusBuilder> createState() => _CustomFocusBuilderState();
}
class _CustomFocusBuilderState extends State<_CustomFocusBuilder> {
late final _focusNode = FocusNode()..addListener(() => setState(() {}));
@override
Widget build(BuildContext context) {
return widget.builder.call(context, _focusNode);
}
}

View File

@ -40,7 +40,7 @@ class TimelineEventCard extends StatelessWidget {
/// Text content /// Text content
Expanded( Expanded(
child: Text(text, style: $styles.text.body), child: Focus(child: Text(text, style: $styles.text.body)),
), ),
], ],
), ),

View File

@ -102,54 +102,55 @@ class _WonderEditorialScreenState extends State<WonderEditorialScreen> {
/// Scrolling content - Includes an invisible gap at the top, and then scrolls over the illustration /// Scrolling content - Includes an invisible gap at the top, and then scrolls over the illustration
TopCenter( TopCenter(
child: SizedBox( child: SizedBox(
//width: $styles.sizes.maxContentWidth1, child: FocusTraversalGroup(
child: CustomScrollView( child: CustomScrollView(
primary: false, primary: false,
controller: _scroller, controller: _scroller,
scrollBehavior: ScrollConfiguration.of(context).copyWith(), scrollBehavior: ScrollConfiguration.of(context).copyWith(),
slivers: [ slivers: [
/// Invisible padding at the top of the list, so the illustration shows through the btm /// Invisible padding at the top of the list, so the illustration shows through the btm
SliverToBoxAdapter( SliverToBoxAdapter(
child: SizedBox(height: illustrationHeight), child: SizedBox(height: illustrationHeight),
),
/// Text content, animates itself to hide behind the app bar as it scrolls up
SliverToBoxAdapter(
child: ValueListenableBuilder<double>(
valueListenable: _scrollPos,
builder: (_, value, child) {
double offsetAmt = max(0, value * .3);
double opacity = (1 - offsetAmt / 150).clamp(0, 1);
return Transform.translate(
offset: Offset(0, offsetAmt),
child: Opacity(opacity: opacity, child: child),
);
},
child: _TitleText(widget.data, scroller: _scroller),
), ),
),
/// Collapsing App bar, pins to the top of the list /// Text content, animates itself to hide behind the app bar as it scrolls up
SliverAppBar( SliverToBoxAdapter(
pinned: true, child: ValueListenableBuilder<double>(
collapsedHeight: minAppBarHeight, valueListenable: _scrollPos,
toolbarHeight: minAppBarHeight, builder: (_, value, child) {
expandedHeight: maxAppBarHeight, double offsetAmt = max(0, value * .3);
backgroundColor: Colors.transparent, double opacity = (1 - offsetAmt / 150).clamp(0, 1);
elevation: 0, return Transform.translate(
leading: SizedBox.shrink(), offset: Offset(0, offsetAmt),
flexibleSpace: SizedBox.expand( child: Opacity(opacity: opacity, child: child),
child: _AppBar( );
widget.data.type, },
scrollPos: _scrollPos, child: _TitleText(widget.data, scroller: _scroller),
sectionIndex: _sectionIndex,
), ),
), ),
),
/// Editorial content (text and images) /// Collapsing App bar, pins to the top of the list
_ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex), SliverAppBar(
], pinned: true,
collapsedHeight: minAppBarHeight,
toolbarHeight: minAppBarHeight,
expandedHeight: maxAppBarHeight,
backgroundColor: Colors.transparent,
elevation: 0,
leading: SizedBox.shrink(),
flexibleSpace: SizedBox.expand(
child: _AppBar(
widget.data.type,
scrollPos: _scrollPos,
sectionIndex: _sectionIndex,
),
),
),
/// Editorial content (text and images)
_ScrollingContent(widget.data, scrollPos: _scrollPos, sectionNotifier: _sectionIndex),
],
),
), ),
), ),
), ),

View File

@ -20,7 +20,7 @@ class _ScrollingContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Text buildText(String value) => Text(_fixNewlines(value), style: $styles.text.body); Widget buildText(String value) => Focus(child: Text(_fixNewlines(value), style: $styles.text.body));
Widget buildDropCapText(String value) { Widget buildDropCapText(String value) {
final TextStyle dropStyle = $styles.text.dropCase; final TextStyle dropStyle = $styles.text.dropCase;

View File

@ -38,7 +38,7 @@ class AboutDialogContent extends StatelessWidget {
} }
} }
double fontSize = $styles.text.bodySmall.fontSize!; double fontSize = $styles.text.body.fontSize!;
fontSize *= MediaQuery.textScaleFactorOf(context); fontSize *= MediaQuery.textScaleFactorOf(context);
return SingleChildScrollView( return SingleChildScrollView(
child: Column(children: [ child: Column(children: [

View File

@ -3,9 +3,11 @@ part of '../timeline_screen.dart';
/// A vertically aligned stack of dots that represent global events /// A vertically aligned stack of dots that represent global events
/// The event closest to the [selectedYr] param will be visible selected /// The event closest to the [selectedYr] param will be visible selected
class _EventMarkers extends StatefulWidget { class _EventMarkers extends StatefulWidget {
const _EventMarkers(this.selectedYr, {Key? key, required this.onEventChanged}) : super(key: key); const _EventMarkers(this.selectedYr, {Key? key, required this.onEventChanged, required this.onMarkerPressed})
: super(key: key);
final void Function(TimelineEvent? event) onEventChanged; final void Function(TimelineEvent? event) onEventChanged;
final void Function(TimelineEvent event) onMarkerPressed;
final int selectedYr; final int selectedYr;
@override @override
@ -62,8 +64,12 @@ class _EventMarkersState extends State<_EventMarkers> {
/// Create a marker for each event /// Create a marker for each event
List<Widget> markers = timelineLogic.events.map((event) { List<Widget> markers = timelineLogic.events.map((event) {
double offsetY = _calculateOffsetY(event.year); double offsetY = _calculateOffsetY(event.year);
return _EventMarker(offsetY, return _EventMarker(
isSelected: event == selectedEvent, semanticLabel: '${event.year}: ${event.description}'); offsetY,
event: event,
isSelected: event == selectedEvent,
onPressed: widget.onMarkerPressed,
);
}).toList(); }).toList();
/// Stack of fractionally positioned markers /// Stack of fractionally positioned markers
@ -110,11 +116,13 @@ class _EventMarker extends StatelessWidget {
this.offset, { this.offset, {
Key? key, Key? key,
required this.isSelected, required this.isSelected,
required this.semanticLabel, required this.event,
required this.onPressed,
}) : super(key: key); }) : super(key: key);
final double offset; final double offset;
final TimelineEvent event;
final bool isSelected; final bool isSelected;
final String semanticLabel; final void Function(TimelineEvent event) onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -126,8 +134,9 @@ class _EventMarker extends StatelessWidget {
height: 0, height: 0,
child: OverflowBox( child: OverflowBox(
maxHeight: 30, maxHeight: 30,
child: Semantics( child: AppBtn.basic(
label: semanticLabel, semanticLabel: '${event.year}: ${event.description}',
onPressed: () => onPressed(event),
child: Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
height: 30, height: 30,

View File

@ -45,6 +45,11 @@ class _ScalingViewportState extends State<_ScrollingViewport> {
AppHaptics.selectionClick(); AppHaptics.selectionClick();
} }
void _handleMarkerPressed(event) {
final pos = controller.calculateScrollPosFromYear(event.year);
controller.scroller.animateTo(pos, duration: $styles.times.med, curve: Curves.easeOutBack);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@ -132,6 +137,7 @@ class _ScalingViewportState extends State<_ScrollingViewport> {
builder: (_, __) => _EventMarkers( builder: (_, __) => _EventMarkers(
controller.calculateYearFromScrollPos(), controller.calculateYearFromScrollPos(),
onEventChanged: _handleEventMarkerChanged, onEventChanged: _handleEventMarkerChanged,
onMarkerPressed: _handleMarkerPressed,
), ),
), ),
], ],

View File

@ -4,11 +4,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
static double bottomPadding = 0; static double bottomPadding = 0;
static double buttonInset = 12; static double buttonInset = 12;
const WonderDetailsTabMenu( const WonderDetailsTabMenu({Key? key, required this.tabController, this.showBg = false, required this.wonderType})
{Key? key,
required this.tabController,
this.showBg = false,
required this.wonderType})
: super(key: key); : super(key: key);
final TabController tabController; final TabController tabController;
@ -36,10 +32,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
), ),
// Buttons // Buttons
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(left: $styles.insets.sm, right: $styles.insets.xxs, bottom: bottomPadding),
left: $styles.insets.sm,
right: $styles.insets.xxs,
bottom: bottomPadding),
// TabButtons are a Stack with a row of icon buttons, and an illustrated home button sitting on top. // TabButtons are a Stack with a row of icon buttons, and an illustrated home button sitting on top.
// The home buttons shows / hides itself based on `showHomeBtn` // The home buttons shows / hides itself based on `showHomeBtn`
// The row contains an animated placeholder gap which makes room for the icon as it transitions in. // The row contains an animated placeholder gap which makes room for the icon as it transitions in.
@ -48,33 +41,27 @@ class WonderDetailsTabMenu extends StatelessWidget {
children: [ children: [
// Main tab btns + animated gap // Main tab btns + animated gap
Center( Center(
child: Row( child: FocusTraversalGroup(
crossAxisAlignment: CrossAxisAlignment.start, child: Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisSize: MainAxisSize.min,
// Home btn children: [
_WonderHomeBtn( // Home btn
size: homeBtnSize, _WonderHomeBtn(
wonderType: wonderType, size: homeBtnSize,
borderSize: showBg ? 6 : 2, wonderType: wonderType,
), borderSize: showBg ? 6 : 2,
_TabBtn(0, tabController, ),
iconImg: 'editorial', _TabBtn(0, tabController,
label: $strings.wonderDetailsTabLabelInformation, iconImg: 'editorial', label: $strings.wonderDetailsTabLabelInformation, color: iconColor),
color: iconColor), _TabBtn(1, tabController,
_TabBtn(1, tabController, iconImg: 'photos', label: $strings.wonderDetailsTabLabelImages, color: iconColor),
iconImg: 'photos', _TabBtn(2, tabController,
label: $strings.wonderDetailsTabLabelImages, iconImg: 'artifacts', label: $strings.wonderDetailsTabLabelArtifacts, color: iconColor),
color: iconColor), _TabBtn(3, tabController,
_TabBtn(2, tabController, iconImg: 'timeline', label: $strings.wonderDetailsTabLabelEvents, color: iconColor),
iconImg: 'artifacts', ],
label: $strings.wonderDetailsTabLabelArtifacts, ),
color: iconColor),
_TabBtn(3, tabController,
iconImg: 'timeline',
label: $strings.wonderDetailsTabLabelEvents,
color: iconColor),
],
), ),
), ),
], ],
@ -87,11 +74,7 @@ class WonderDetailsTabMenu extends StatelessWidget {
} }
class _WonderHomeBtn extends StatelessWidget { class _WonderHomeBtn extends StatelessWidget {
const _WonderHomeBtn( const _WonderHomeBtn({Key? key, required this.size, required this.wonderType, required this.borderSize})
{Key? key,
required this.size,
required this.wonderType,
required this.borderSize})
: super(key: key); : super(key: key);
final double size; final double size;
@ -113,8 +96,7 @@ class _WonderHomeBtn extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(99), borderRadius: BorderRadius.circular(99),
color: wonderType.fgColor, color: wonderType.fgColor,
image: DecorationImage( image: DecorationImage(image: AssetImage(wonderType.homeBtn), fit: BoxFit.fill),
image: AssetImage(wonderType.homeBtn), fit: BoxFit.fill),
), ),
), ),
); );
@ -143,12 +125,9 @@ class _TabBtn extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool selected = tabController.index == index; bool selected = tabController.index == index;
final MaterialLocalizations localizations = final MaterialLocalizations localizations = MaterialLocalizations.of(context);
MaterialLocalizations.of(context); final iconImgPath = '${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
final iconImgPath = String tabLabel = localizations.tabLabel(tabIndex: index + 1, tabCount: tabController.length);
'${ImagePaths.common}/tab-$iconImg${selected ? '-active' : ''}.png';
String tabLabel = localizations.tabLabel(
tabIndex: index + 1, tabCount: tabController.length);
tabLabel = '$label: $tabLabel'; tabLabel = '$label: $tabLabel';
final double btnWidth = (context.widthPx / 6).clamp(_minWidth, _maxWidth); final double btnWidth = (context.widthPx / 6).clamp(_minWidth, _maxWidth);
return MergeSemantics( return MergeSemantics(
@ -157,15 +136,12 @@ class _TabBtn extends StatelessWidget {
label: tabLabel, label: tabLabel,
child: ExcludeSemantics( child: ExcludeSemantics(
child: AppBtn.basic( child: AppBtn.basic(
padding: EdgeInsets.only( padding: EdgeInsets.only(top: $styles.insets.md + $styles.insets.xs, bottom: $styles.insets.sm),
top: $styles.insets.md + $styles.insets.xs,
bottom: $styles.insets.sm),
onPressed: () => tabController.index = index, onPressed: () => tabController.index = index,
semanticLabel: label, semanticLabel: label,
minimumSize: Size(btnWidth, 0), minimumSize: Size(btnWidth, 0),
// Image icon // Image icon
child: Image.asset(iconImgPath, child: Image.asset(iconImgPath, height: 32, width: 32, color: selected ? null : color),
height: 32, width: 32, color: selected ? null : color),
), ),
), ),
), ),

View File

@ -32,7 +32,7 @@ class _EventsListState extends State<_EventsList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return widget.blurOnScroll ? _buildScrollingListWithBlur() : _buildScrollingList(); return FocusTraversalGroup(child: widget.blurOnScroll ? _buildScrollingListWithBlur() : _buildScrollingList());
} }
/// The actual content of the scrolling list /// The actual content of the scrolling list

View File

@ -1,3 +1,7 @@
# 2.0.12
- Artifact search remembers open/closed panel state
- Add focus state to all buttons
# 2.0.11 # 2.0.11
- Fixed landscape not working on some tablets - Fixed landscape not working on some tablets
- Fixed app restarting when folded, or switching between multi-window mode. - Fixed app restarting when folded, or switching between multi-window mode.