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

(WIP) Add scrollable content indicator #86

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -570,10 +570,10 @@ packages:
dependency: transitive
description:
name: scrollable_positioned_list
sha256: f998e48b93314f29e27d31448c8f095d342e6680a020ed0b97524ccb85edf672
sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287"
url: "https://pub.dev"
source: hosted
version: "0.3.2"
version: "0.3.8"
simple_html_css:
dependency: "direct main"
description:
Expand Down
17 changes: 4 additions & 13 deletions lib/questionnaires/view/src/questionnaire_scroller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ class QuestionnaireScroller extends StatefulWidget {
});

@override
State<StatefulWidget> createState() {
return _QuestionnaireScrollerState();
}
State<QuestionnaireScroller> createState() => _QuestionnaireScrollerState();
}

class _QuestionnaireScrollerState extends State<QuestionnaireScroller> {
Expand Down Expand Up @@ -157,30 +155,23 @@ class _QuestionnaireScrollerState extends State<QuestionnaireScroller> {
questionnaireModelDefaults: widget.questionnaireModelDefaults,
builder: (BuildContext context) {
_belowFillerContext = context;
final questionnaireFiller = QuestionnaireResponseFiller.of(context);

final totalLength = questionnaireFiller.fillerItemModels.length;

_logger.trace(
'Scroll position: ${_itemPositionsListener.itemPositions.value}',
);

return widget.scaffoldBuilder.build(
context,
setStateCallback: (fn) {
setState(fn);
},
setStateCallback: (fn) => setState(fn),
child: LayoutBuilder(
builder: (context, constraints) {
const edgeInsets = 8.0;
const twice = 2;

return ScrollablePositionedList.builder(
return QuestionnaireScrollerView(
itemScrollController: _listScrollController,
itemPositionsListener: _itemPositionsListener,
itemCount: totalLength,
padding: const EdgeInsets.all(edgeInsets),
minCacheExtent: 200, // Allow tabbing to prev/next items
// Allow tabbing to prev/next items
itemBuilder: (BuildContext context, int i) {
return Row(
children: [
Expand Down
101 changes: 101 additions & 0 deletions lib/questionnaires/view/src/questionnaire_scroller_view.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import 'package:faiadashu/faiadashu.dart';
import 'package:flutter/material.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';

class QuestionnaireScrollerView extends StatefulWidget {
final ItemScrollController itemScrollController;
final ItemPositionsListener itemPositionsListener;
final Widget Function(BuildContext, int) itemBuilder;

const QuestionnaireScrollerView({
super.key,
required this.itemScrollController,
required this.itemPositionsListener,
required this.itemBuilder,
});

@override
State<QuestionnaireScrollerView> createState() =>
_QuestionnaireScrollerViewState();
}

class _QuestionnaireScrollerViewState extends State<QuestionnaireScrollerView> {
final _scrollOffsetController = ScrollOffsetController();

double _buttonOpacity = 0.0;
bool _showScrollDownButton = false;

@override
void initState() {
super.initState();
widget.itemPositionsListener.itemPositions
.addListener(_updateButtonVisibility);
}

@override
void dispose() {
super.dispose();
widget.itemPositionsListener.itemPositions
.removeListener(_updateButtonVisibility);
}

void _updateButtonVisibility() {
final items = QuestionnaireResponseFiller.of(context).fillerItemModels;
final positions = widget.itemPositionsListener.itemPositions.value;
// Check if the bottom of the list is visible
final isEndVisible = positions.any(
(ItemPosition position) =>
position.index == items.length - 1 && position.itemTrailingEdge <= 1,
);

final newValue = isEndVisible ? 0.0 : 1.0;
if (_buttonOpacity != newValue) {
setState(() {
_buttonOpacity = newValue;
_showScrollDownButton = true;
});
}
}

void _scrollToBottomItem() {
_scrollOffsetController.animateScroll(
offset: 250,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOutCubic,
);
}

void _onAnimationEnded() {
setState(() {
_showScrollDownButton = false;
});
}

@override
Widget build(BuildContext context) {
final itemCount =
QuestionnaireResponseFiller.of(context).fillerItemModels.length;

return Stack(
children: [
ScrollablePositionedList.builder(
itemScrollController: widget.itemScrollController,
itemPositionsListener: widget.itemPositionsListener,
scrollOffsetController: _scrollOffsetController,
itemCount: itemCount,
itemBuilder: (context, index) => widget.itemBuilder(context, index),
padding: const EdgeInsets.all(8.0),
minCacheExtent: 200,
),
if (_showScrollDownButton &&
QuestionnaireTheme.of(context).showScrollDownButton)
QuestionnaireTheme.of(context).scrollDownButton(
context,
_buttonOpacity,
_onAnimationEnded,
_scrollToBottomItem,
),
],
);
}
}
113 changes: 108 additions & 5 deletions lib/questionnaires/view/src/questionnaire_stepper_page_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ class QuestionnaireStepperPageView extends StatefulWidget {
static QuestionnaireStepperPageViewData of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<
_QuestionnaireStepperPageViewInheritedWidget>();
assert(result != null,
'No QuestionnaireStepperInheritedWidget found in context',);
assert(
result != null,
'No QuestionnaireStepperInheritedWidget found in context',
);
return result!.data;
}

Expand All @@ -33,13 +35,15 @@ class QuestionnaireStepperPageView extends StatefulWidget {
class _QuestionnaireStepperPageViewState
extends State<QuestionnaireStepperPageView> {
final PageController _pageController = PageController();
bool _isPageSwiping = false;
bool _hasRequestsRunning = false;
QuestionnaireItemFiller? _currentQuestionnaireItemFiller;

@override
void initState() {
super.initState();
widget.data.controller._attach(this);
_pageController.addListener(_scrollListener);
}

/// Determines if we can proceed to the next page.
Expand Down Expand Up @@ -93,6 +97,12 @@ class _QuestionnaireStepperPageViewState
widget.data.onPageChanged?.call(index);
}

void _scrollListener() {
setState(() {
_isPageSwiping = _pageController.page?.round() != _pageController.page;
});
}

@override
Widget build(BuildContext context) {
return _QuestionnaireStepperPageViewInheritedWidget(
Expand All @@ -114,9 +124,12 @@ class _QuestionnaireStepperPageViewState
_updateVisibleItem(index);
if (data == null) return null;

return QuestionnaireTheme.of(context).stepperPageItemBuilder(
context,
data,
return _ScrollableArea(
showScrollDownButton: !_isPageSwiping,
child: QuestionnaireTheme.of(context).stepperPageItemBuilder(
context,
data,
),
);
},
physics: widget.data.physics,
Expand All @@ -127,6 +140,8 @@ class _QuestionnaireStepperPageViewState
@override
void dispose() {
widget.data.controller._detach();
_pageController.removeListener(_scrollListener);
_pageController.dispose();
super.dispose();
}
}
Expand Down Expand Up @@ -257,3 +272,91 @@ class QuestionnaireStepperPageViewData {
this.controller = controller ?? QuestionnaireStepperPageViewController();
}
}

class _ScrollableArea extends StatefulWidget {
final Widget child;
final bool showScrollDownButton;

const _ScrollableArea({
required this.child,
this.showScrollDownButton = true,
});

@override
State<StatefulWidget> createState() => _ScrollableAreaState();
}

class _ScrollableAreaState extends State<_ScrollableArea> {
late ScrollController _scrollController;
bool _showScrollDownButton = true;
bool _hasMoreContent = false;

@override
void initState() {
super.initState();
_scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) => _checkScroll());
_scrollController.addListener(_scrollListener);
}

void _checkScroll() {
final hasMoreContent = _scrollController.position.maxScrollExtent > 0;
if (hasMoreContent != _hasMoreContent) {
setState(() {
_hasMoreContent = hasMoreContent;
});
}
}

void _scrollListener() {
setState(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent) {
_hasMoreContent = false;
_showScrollDownButton = true;
} else {
_hasMoreContent = true;
}
});
}

void _onAnimationEnded() {
setState(() {
_showScrollDownButton = widget.showScrollDownButton && _hasMoreContent;
});
}

void _scrollToBottomItem() {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOutCubic,
);
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
SingleChildScrollView(
controller: _scrollController,
child: widget.child,
),
if (_showScrollDownButton &&
QuestionnaireTheme.of(context).showScrollDownButton)
QuestionnaireTheme.of(context).scrollDownButton(
context,
widget.showScrollDownButton && _hasMoreContent ? 1 : 0,
_onAnimationEnded,
_scrollToBottomItem,
),
],
);
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
37 changes: 37 additions & 0 deletions lib/questionnaires/view/src/questionnaire_theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class QuestionnaireTheme extends InheritedWidget {
class QuestionnaireThemeData {
static final _logger = Logger(QuestionnaireThemeData);

/// Returns whether a scroll down button is shown
final bool showScrollDownButton;

/// Returns whether user will be offered option to skip question.
final bool canSkipQuestions;

Expand Down Expand Up @@ -155,6 +158,13 @@ class QuestionnaireThemeData {
int pageIndex,
) stepperQuestionnaireItemFiller;

final Widget Function(
BuildContext context,
double buttonOpacity,
VoidCallback onEndAnimation,
VoidCallback scrollToBottom,
) scrollDownButton;

/// Builds layouts for QuestionnaireStepper pages.
/// If there are no more pages to show, this method must return `null`.
///
Expand All @@ -166,6 +176,7 @@ class QuestionnaireThemeData {

const QuestionnaireThemeData({
this.canSkipQuestions = false,
this.showScrollDownButton = true,
this.showProgress = true,
this.showScore = true,
this.autoCompleteThreshold = defaultAutoCompleteThreshold,
Expand All @@ -185,6 +196,7 @@ class QuestionnaireThemeData {
this.stepperQuestionnaireItemFiller =
_defaultStepperQuestionnaireItemFiller,
this.stepperPageItemBuilder = _defaultStepperPageItemBuilder,
this.scrollDownButton = _defaultScrollDownButton,
});

/// Returns a [QuestionnaireItemFiller] for a given [QuestionnaireResponseFiller].
Expand Down Expand Up @@ -450,4 +462,29 @@ class QuestionnaireThemeData {
child: itemFiller,
);
}

static Widget _defaultScrollDownButton(
BuildContext context,
double buttonOpacity,
VoidCallback onEndAnimation,
VoidCallback scrollToBottom,
) {
return Positioned(
bottom: 10.0,
left: 0.0,
right: 0.0,
child: Center(
child: AnimatedOpacity(
opacity: buttonOpacity,
duration: const Duration(milliseconds: 250),
onEnd: onEndAnimation,
child: FloatingActionButton(
mini: true,
onPressed: scrollToBottom,
child: const Icon(Icons.arrow_downward),
),
),
),
);
}
}
1 change: 1 addition & 0 deletions lib/questionnaires/view/view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export 'src/questionnaire_loading_indicator.dart';
export 'src/questionnaire_page_scaffold.dart';
export 'src/questionnaire_scroller.dart';
export 'src/questionnaire_scroller_page.dart';
export 'src/questionnaire_scroller_view.dart';
export 'src/questionnaire_stepper.dart';
export 'src/questionnaire_stepper_page.dart';
export 'src/questionnaire_stepper_page_view.dart';
Expand Down
Loading