Skip to content

Commit

Permalink
Add support for HtmlWidget.renderMode (#484)
Browse files Browse the repository at this point in the history
WidgetFactory breaking changes:

- `buildColumnPlaceholder` removed `trimMarginVertical` named param
- `buildColumnWidget` removed `tsh` param
- `onTapAnchor` replaced `anchorContext` param with `scrollTo`
  • Loading branch information
daohoangson committed Jun 9, 2021
1 parent b68fef3 commit b549768
Show file tree
Hide file tree
Showing 21 changed files with 967 additions and 115 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/flutter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Setup Flutter
uses: subosito/flutter-action@v1
- run: ./tool/test.sh --coverage
- run: bash <(curl -s https://codecov.io/bash)
- uses: codecov/codecov-action@v1

ios:
name: iOS Test
Expand Down
61 changes: 60 additions & 1 deletion demo_app/lib/screens/huge_html.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5193,11 +5193,70 @@ class HugeHtmlScreen extends StatelessWidget {
ShowPerfIconButton(),
],
),
body: ListView(
children: [
ListTile(
title: Text('renderMode: Column'),
onTap: () => Navigator.push(
context, MaterialPageRoute(builder: (_) => _ColumnScreen())),
),
ListTile(
title: Text('renderMode: ListView'),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => _ListViewScreen())),
),
ListTile(
title: Text('renderMode: SliverList'),
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (_) => _SliverListScreen())),
),
],
),
);
}

class _ColumnScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('renderMode: Column')),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: HtmlWidget(kHtml),
child: RepaintBoundary(
child: HtmlWidget(kHtml, renderMode: RenderMode.Column),
),
),
),
);
}

class _ListViewScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('renderMode: ListView')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: HtmlWidget(kHtml, renderMode: RenderMode.ListView),
),
);
}

class _SliverListScreen extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
body: CustomScrollView(
slivers: [
SliverAppBar(
title: Text('renderMode: SliverList'),
floating: true,
expandedHeight: 200,
flexibleSpace: Placeholder(),
),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: HtmlWidget(kHtml, renderMode: RenderMode.SliverList),
),
],
),
);
}
Binary file added demo_app/test/anchor/listview/down/target.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo_app/test/anchor/listview/down/top.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo_app/test/anchor/sliverlist/up/bottom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo_app/test/anchor/sliverlist/up/target.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
59 changes: 59 additions & 0 deletions packages/core/lib/src/core_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
import 'package:html/dom.dart' as dom;

import 'core_html_widget.dart';
import 'core_widget_factory.dart';

export 'external/csslib.dart';
export 'widgets/css_sizing.dart';
Expand Down Expand Up @@ -46,6 +47,23 @@ typedef CustomStylesBuilder = Map<String, String>? Function(
/// For those needs, a custom [WidgetFactory] is the way to go.
typedef CustomWidgetBuilder = Widget? Function(dom.Element element);

/// A callback to scroll the anchor identified by [id] into the viewport.
///
/// By default, an internal implementation is given to [WidgetFactory.onTapAnchor]
/// when an anchor is tapped to handle the scrolling.
/// A wf subclass can use this to change the [curve], the animation [duration]
/// or even request scrolling to a different anchor.
///
/// The future is resolved after scrolling is completed.
/// It will be `true` if scrolling succeed or `false` otherwise.
typedef EnsureVisible = Future<bool> Function(
String id, {
Curve curve,
Duration duration,
Curve jumpCurve,
Duration jumpDuration,
});

/// A set of values that should trigger rebuild.
class RebuildTriggers {
final List _values;
Expand Down Expand Up @@ -77,6 +95,45 @@ class RebuildTriggers {
}
}

/// The HTML body render modes.
enum RenderMode {
/// The body will be rendered as a `Column` widget.
///
/// This is the default render mode.
/// It's good enough for small / medium document and can be used easily.
Column,

/// The body will be rendered as a `ListView` widget.
///
/// It's good for medium / large document in a dedicated page layout
/// (e.g. the HTML document is the only thing on the screen).
ListView,

/// The body will be rendered as a `SliverList` sliver.
///
/// It's good for large / huge document and can be put in the same scrolling
/// context with other contents.
/// A [CustomScrollView] or similar is required for this to work.
SliverList,
}

/// An extension on [Widget] to keep track of anchors.
extension WidgetAnchors on Widget {
static final _anchors = Expando<Iterable<Key>>();

/// Anchor keys of this widget and its children.
Iterable<Key>? get anchors => _anchors[this];

/// Set anchor keys.
bool setAnchorsIfUnset(Iterable<Key>? anchors) {
if (anchors == null) return false;
final existing = _anchors[this];
if (existing != null) return false;
_anchors[this] = anchors;
return true;
}
}

/// A widget builder that supports builder callbacks.
class WidgetPlaceholder<T> extends StatelessWidget {
/// The origin of this widget.
Expand All @@ -99,6 +156,8 @@ class WidgetPlaceholder<T> extends StatelessWidget {
built = builder(context, built) ?? widget0;
}

built.setAnchorsIfUnset(anchors);

return built;
}

Expand Down
46 changes: 33 additions & 13 deletions packages/core/lib/src/core_html_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class HtmlWidget extends StatefulWidget {

/// The callback to handle async build snapshot.
///
/// By default, a [CircularProgressIndicator] will be shown until
/// By default, a platform-dependent indicator will be shown until
/// the widget tree is ready.
/// This default builder doesn't do any error handling
/// (it will just ignore any errors).
Expand Down Expand Up @@ -92,6 +92,13 @@ class HtmlWidget extends StatefulWidget {
]);
final RebuildTriggers? _rebuildTriggers;

/// The render mode.
///
/// - [RenderMode.Column] is the default mode, suitable for small / medium document.
/// - [RenderMode.ListView] has better performance as it renders contents lazily.
/// - [RenderMode.SliverList] has similar performance as `ListView` and can be put inside a `CustomScrollView`.
final RenderMode renderMode;

/// The default styling for text elements.
final TextStyle? textStyle;

Expand All @@ -112,6 +119,7 @@ class HtmlWidget extends StatefulWidget {
this.onTapImage,
this.onTapUrl,
RebuildTriggers? rebuildTriggers,
this.renderMode = RenderMode.Column,
this.textStyle = const TextStyle(),
}) : _rebuildTriggers = rebuildTriggers,
super(key: key);
Expand Down Expand Up @@ -205,6 +213,30 @@ class _HtmlWidgetState extends State<HtmlWidget> {
return built;
}

Widget _buildAsyncBuilder(
BuildContext context, AsyncSnapshot<Widget> snapshot) {
final built = snapshot.data;
if (built != null) return built;

final indicator = Theme.of(context).platform == TargetPlatform.iOS
? const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: CupertinoActivityIndicator()))
: const Center(
child: Padding(
padding: EdgeInsets.all(8),
child: CircularProgressIndicator()));

switch (widget.renderMode) {
case RenderMode.Column:
case RenderMode.ListView:
return indicator;
case RenderMode.SliverList:
return SliverToBoxAdapter(child: indicator);
}
}

Widget _buildSync() {
Timeline.startSync('Build $widget (sync)');

Expand Down Expand Up @@ -244,18 +276,6 @@ class _RootTsb extends TextStyleBuilder {
void reset() => _output = null;
}

Widget _buildAsyncBuilder(
BuildContext context, AsyncSnapshot<Widget> snapshot) =>
snapshot.data ??
Center(
child: Padding(
padding: EdgeInsets.all(8),
child: Theme.of(context).platform == TargetPlatform.iOS
? CupertinoActivityIndicator()
: CircularProgressIndicator(),
),
);

Widget _buildBody(_HtmlWidgetState state, dom.NodeList domNodes) {
final rootMeta = state._rootMeta;
final wf = state._wf;
Expand Down
Loading

1 comment on commit b549768

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.