Scroll a WebViewWidget inside a CustomScrollView - flutter

I'm trying to display a page that has a website on top with some scrollable items below. I would like the website itself to be scrollable (i.e. scroll around the HTML page vertically) as well as the items at the flutter level. Currently I have this which doesn't let you scroll the WebViewWidget at all, it just scrolls with the rest of the scroll items but not within the web page itself:
class MapPage extends StatefulWidget {
MapPage({super.key});
#override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
late final WebViewController controller;
#override
void initState() {
super.initState();
// Load a vertically long website that needs scrolling
controller = WebViewController()
..loadRequest(
Uri.parse('https://flutter.dev'),
)
..setJavaScriptMode(JavaScriptMode.unrestricted);
}
#override
Widget build(BuildContext context) {
List<Widget> scrollItems = [];
// TODO: How to make this not part of the scroll view?
scrollItems.add(SizedBox(
width: MediaQuery.of(context).size.width,
height: 400,
child: WebViewWidget(controller: controller)));
// Generate some sample data
var someData = [for (int i = 0; i < 100; i++) i];
for (var entry in someData) {
scrollItems.add(Text("A number: ${entry}"));
}
return CupertinoPageScaffold(
child:
CustomScrollView(semanticChildCount: someData.length + 1, slivers: [
CupertinoSliverNavigationBar(
largeTitle: Text("A page"),
),
SliverSafeArea(
top: false,
minimum: EdgeInsets.only(top: 0),
sliver: SliverToBoxAdapter(
child: CupertinoListSection(topMargin: 0, children: scrollItems),
))
]),
);
}
}
My first instinct was to put the WebViewWidget and CustomScrollView in a Column together but that doesn't work since the Column has infinite height. I'd also like the WebViewWidget to not be fixed on the screen but scroll with the items when the scroll comes from outside the area of the WebViewWidget, as if the scroll gestures there just got ignored by the CustomScrollView and passed to the WebViewWidget but I couldn't find a good way to get this working. Anyone have any pointers to what I can use to get this working?

Related

Flutter Wrap widget rendering glitches when height is updated

I am creating a form (not using the Form Widget) in Flutter where the user can add an arbitrary amount of items (treatments) which are rendered as InputChip widgets list in a Wrap widget.
The form uses a button (AddButton widget) which opens a form dialog which itself returns the newly created item (treatment) that is added to selectedItems:
class TreatmentsWidget extends StatefulWidget {
const TreatmentsWidget({super.key, required this.selectedItems});
final List<Treatment> selectedItems;
#override
State<TreatmentsWidget> createState() => _TreatmentsWidgetState();
}
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = widget.selectedItems.map(
(item) {
return InputChip(
label: Text('${item.name} - ${item.frequency}/${item.frequencyUnit.name})',
);
},
).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return const TreatmentDialog();
}).then((value) {
if (value != null) {
Treatment item = value;
setState(() {
widget.selectedItems.add(item);
});
}
});
}),
],
);
}
}
For some reason, when a new item is added to selectedItem and that the item overflows the current line, the layout is not recomputed such that the Wrap widget overflows the button:
However, as soon as the user scroll (the whole screen content is inside a SingleChildScrollView), the layout is recomputed and the Wrap takes the right amount of space:
How can I force a redraw when a new item is added to prevent this glitch?
The issue seems to be that the Column does not recompute its size on the current frame when one of his child size changes.
I ended up forcing rebuilding the Column using a ValueKey whenever chips.length changes:
class _TreatmentsWidgetState extends State<TreatmentsWidget> {
#override
Widget build(BuildContext context) {
var chips = ...;
return Column(
key: ValueKey(chips.length),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
children: chips,
),
AddButton(...),
],
);
}
}
I am still interested in a cleaner solution if it exists. Using a provider seems overkill for this case.

Flutter: How to dry-render a widget and get its size?

I'm trying to get the size of a widget. A common approach is to give a GlobalKey() to a widget, and retrieve its size after it has been laid out and shown to the user. Given the constraint (MediaQuery.of(context).size.width), is there a way to pre-render a widget and get its size (specifically height) before building the real one?
Edit: I want the height so badly because I need to pass the height to SliverAppBar. I can build the content first, and use setState to resize it after it get laid out. But this also mean that screen will flicker for once.
You can try offstage widget. So the layout will look like
Offstage(
offstage: true,
child: Container (
key: widgetKey
)
)
You can use Offstage. The trick is to use Offstage to render, then rebuild the moment you get the size using the postframecallback.
class _wrapperState extends State<wrapper> with WidgetsBindingObserver
{
Size? widgetSize;
GlobalKey widgetKey = GlobalKey();
late Widget sourceWidget;
#override
void initState()
{
WidgetsBinding.instance.addObserver(this);
// widget.child is your main widget you're trying to get the size.
sourceWidget = widget.child;
super.initState();
}
#override
Widget build(BuildContext context)
{
WidgetsBinding.instance.addPostFrameCallback((_)
{
if (widgetSize == null)
{
setState(()
{
widgetSize = widgetKey.currentContext!.size;
});
}
});
// Assign a key to your widget so that we can get its size later.
// Also this key will prevent the widget to get built twice.
Widget finalWidget = KeyedSubtree(
key: widgetKey,
child: sourceWidget
);
// If we don't have the size, then do the offstage.
// Postframe will kick off right after this.
if (widgetSize == null)
{
return Offstage(
child: Material(
body: Stack(
children: [
Positioned(
top: 0,
left: 0,
child: finalWidget,
)
],
),
),
);
}
// Now we do have our size.
// Do your main build now.
return MyComplexWidget();
}
}

How can I stick the last element of List to the bottom of the but push it down when required?

I have to create a ListView, where the last child is positioned at the bottom of the screen, even if the rest of the content is smaller. If the rest of the content grows larger than the available space, or if the available size is reduced, the bottom child should be pushed down (Kind of like position: sticky in CSS).
My problem is almost exactly the same as this question. The only difference is that the number of children in my list can change dynamically, and the solution proposed in the linked question does not handle that.
I have the following solution, but there are some problems with it. Each time the state is updated, the bottom child flashes, and another problem is that setState is called continiously.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
class ListWithBottomChild extends StatefulWidget {
const ListWithBottomChild({
required this.children,
required this.bottomChild,
this.padding = EdgeInsets.zero,
});
final List<Widget> children;
final Widget bottomChild;
final EdgeInsets padding;
#override
_ListWithBottomChildState createState() => _ListWithBottomChildState();
}
class _ListWithBottomChildState extends State<ListWithBottomChild> {
ScrollController _controller = ScrollController();
bool _isLarge = true;
late List<Widget> children;
#override
Widget build(BuildContext context) {
children = widget.children;
SchedulerBinding.instance?.addPostFrameCallback((_) {
if (_controller.hasClients) {
setState(() {
_isLarge = _controller.position.maxScrollExtent > 0;
});
}
if (!children.contains(widget.bottomChild) && _isLarge) {
children.add(widget.bottomChild);
}
});
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
child: SingleChildScrollView(
controller: _controller,
padding: widget.padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
),
if (!_isLarge) widget.bottomChild
],
);
}
}
Another idea that I have is to use a dynamically sized spacer before the bottom child, but I can't figure out a way to set the correct size.
You could simply write
Column(
children: children..add(widget.bottomChild)
)
I think this could solve it.

Controll flutter pageview widgets or variables for one page only

How to control specific widget to hide or to show in pageview. In PageView Widget in flutter for specific page only, how we can control over widget to be hidden or shown based on bool value. how we can utilize pageview controller to do this
you can change the children in the widget tree depending on a boolean value.
Example: child: condition ? WidgetWhenTrue : WidgetWhenFalse
UPDATE
The best way I found is, that you create your pages and page children dynamicly.
You could provide a List<Widget> which will represent the max. Content and then remove the widget you don't want to have.
Or you could add the widgets on the fly to the pages.
import 'package:flutter/material.dart';
class PageViewWidget extends StatefulWidget {
#override
_PageViewWidgetState createState() => _PageViewWidgetState();
}
class _PageViewWidgetState extends State<PageViewWidget> {
PageController _pageController;
#override
void dispose() {
_pageController.dispose();
super.dispose();
}
List<Text> maxContent = [
Text('text 1'),
Text('text 2'),
Text('text 3'),
Text('text 4'),
Text('text 5'),
Text('text 6')
];
bool condition = true;
Container dynamicPageChildren({Color color, List<int> delPos}) {
Container newPage;
List<Widget> newContent = List.from(maxContent);
// modify your Widget List
print('length = ${maxContent.length} ');
for (int i in delPos.reversed) {
// use reversed or provide the last elemt to remove first if not,
// your list will shrink and the element you want to remove last does not exist or is the wrong one
print('delete at pos $i');
newContent.removeAt(i);
}
// add it to the page
newPage = Container(
color: color,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: newContent));
return newPage;
}
List<Container> dynamicPages({List<Color> colorList}) {
// you could also pass the index positions into this function
// or call your logic to decide which index should not be displayed
List<Container> newPageList = [];
// modify your Widget List
int i = 0;
for (Color color in colorList) {
// example with given indices
// newPageList.add(dynamicPageChildren(color: color, delPos: [1, 3, 5]));
newPageList.add(dynamicPageChildren(color: color, delPos: [i]));
i = i + 1;
}
return newPageList;
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: PageView(
controller: _pageController,
children: dynamicPages(
colorList: [Colors.red, Colors.orange, Colors.yellow])),
),
);
}
}

flutter - View a web page inside a container

I'm trying to create a master-detail type container starting with a column of ListTiles on the left side of the screen. When a user taps on an item, a preset URL will then be displayed on the rest of the screen. Tapping a different item displays a different preset URL.
I've looked at the Flutter WebView Plugin and and webview_flutter packages, but either I don't understand them well enough (quite possible!) or they can't yet do everything I want them to to do.
Beside what I just mentioned, if possible I'd also like the web pages to open zoomed to fit the space they're in, but still be pinchable to other sizes.
p.s. I'm new to Flutter and am also confused about widget construction and memory management. If I try using something like a WebView widget, I don't know whether I just code a WebView widget every time I want to open a page, or if I somehow create a single WebView widget, add a controller, and code .loadFromUrl() methods.
You can create a Row with two children. First children will be ListView that will be consisted of ListTiles. Second children will be the WebView. When a user taps on the list tile, load the url with the controller. There is no need to rebuild the WebView every time in your case
Example by using webview_flutter:
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
WebViewController _controller;
List pages = ["https://google.com", "https://apple.com"];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: <Widget>[
Container(
width: 300,
child: ListView.builder(
itemCount: pages.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(pages[index]),
onTap: () {
if (_controller != null) {
_controller.loadUrl(pages[index]);
}
},
);
},
),
),
Expanded(
child: WebView(
onWebViewCreated: (WebViewController c) {
_controller = c;
},
initialUrl: 'https://stackoverflow.com',
),
),
],
));
}
}
Just wrap the webview inside a SizedBox
SizedBox(
height: 300,
child: WebView()
)