Related
I'm trying to build a custom pop-up in Flutter/Dart using the following features:
Non-blocking. The popup should be triggered from a scrollable view and should not block the view from being scrolled after the popup is shown.
The popup should contain custom content that supports pushing and popping pages inside the popup, including scrollable views.
The popup should support an arrow pointing to a specific location on the screen where it was triggered from.
Please refer to the attached images for an example of this implementation in IOS. I desire to achieve the same thing using Flutter/Dart.
The popups included in Flutter framework are rudimentary. Any ideas on how this is done?
I'm not a flutter pro at all but just out of curiosity I tried to implement as much as I can of your targets in my free time.
You can show a popup without blocking the scroll. And you can push and pop the content of popup.
I'm not claiming that it's an efficient or a healthy way to do things. It's not complete but I think you can go on and build on it. If you decide to use it I think it would be best to;
Edit the PopupBase showPopup method to avoid misplacement of popup as it can overflow in certain situations.
The arrow which will point to the clicked location can also be added after the improvement of placement.
class MyScaffold extends StatefulWidget {
const MyScaffold({Key? key}) : super(key: key);
#override
State<MyScaffold> createState() => _MyScaffoldState();
}
class _MyScaffoldState extends State<MyScaffold> {
CustomPopupController customPopupController = CustomPopupController();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("NonBlocking PopUp")),
body: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: SingleChildScrollView(
scrollDirection: Axis.vertical,
child: PopupBase(
child: RichTextContent(),
)),
),
),
],
),
);
}
}
class PopupBase extends StatefulWidget {
const PopupBase({Key? key, required this.child}) : super(key: key);
final Widget child;
#override
State<PopupBase> createState() => _PopupBaseState();
static _PopupBaseState of(BuildContext context) =>
context.findAncestorStateOfType<_PopupBaseState>()!;
}
class _PopupBaseState extends State<PopupBase> {
List<Widget> popupStack = List.empty(growable: true);
#override
void initState() {
super.initState();
popupStack.add(widget.child);
}
void showPopup(CustomPopup popup, Offset position) {
setState(() {
popupStack.add(Positioned(
child: popup,
left: position.dx,
top: position.dy,
));
});
}
void removePopup(CustomPopup popup) {
print("popupbase remove");
setState(() {
popupStack.removeLast();
});
print(popupStack.length);
}
#override
Widget build(BuildContext context) {
return Stack(
children: popupStack,
);
}
}
class CustomPopupController {
late bool Function(Widget) push;
late Widget Function()? pop;
}
class CustomPopup extends StatefulWidget {
CustomPopup({Key? key, required this.content, required this.controller})
: super(key: key);
Widget content;
final CustomPopupController controller;
#override
State<CustomPopup> createState() => _CustomPopupState();
static _CustomPopupState of(BuildContext context) =>
context.findAncestorStateOfType<_CustomPopupState>()!;
}
class _CustomPopupState extends State<CustomPopup> {
#override
void initState() {
super.initState();
widget.controller.push = push;
widget.controller.pop = pop;
}
final List<Widget> _contentStack = List.empty(growable: true);
bool push(Widget widgetToPush) {
_contentStack.add(widget.content);
setState(() {
widget.content = widgetToPush;
});
return false;
}
Widget pop() {
if (_contentStack.isNotEmpty) {
Widget temp = widget.content;
setState(() {
widget.content = _contentStack.last;
_contentStack.removeLast();
});
return temp;
} else {
PopupBase.of(context).removePopup(this.widget);
return widget.content;
}
}
#override
Widget build(BuildContext context) {
return Card(
child: SizedBox(
height: 300,
width: 200,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
color: Colors.red,
height: 25,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
(_contentStack.isNotEmpty)
? Padding(
padding: EdgeInsets.only(left: 10),
child: InkWell(
onTap: () {
pop();
},
child: Icon(Icons.arrow_back_ios)),
)
: Container(),
Expanded(child: Container()),
Padding(
padding: EdgeInsets.only(left: 10),
child: InkWell(
onTap: () {
pop();
},
child: Icon(Icons.close)),
)
],
),
),
Expanded(
child: SingleChildScrollView(
child: widget.content,
))
],
)),
);
}
}
class RichTextContent extends StatelessWidget {
RichTextContent({Key? key}) : super(key: key);
TextStyle text = TextStyle(color: Colors.black);
TextStyle clickable = TextStyle(color: Colors.blue);
#override
Widget build(BuildContext context) {
CustomPopupController controller = CustomPopupController();
return RichText(
text: TextSpan(children: [
TextSpan(style: text, text: """Lorem ipsum dolor sit """),
TextSpan(
text: 'amet',
style: clickable,
recognizer: TapGestureRecognizer()
..onTapUp = (TapUpDetails details) {
PopupBase.of(context).showPopup(
CustomPopup(content: Text("Popup"), controller: controller),
details.localPosition);
}),
TextSpan(
style: text,
text:
""", consectetur adipiscing elit. Nulla bibendum at massa et euismod. Praesent ex ipsum, ultrices ut rhoncus et, efficitur vehicula mi. Duis sollicitudin dolor sed tristique molestie. Ut eu elit velit. Cras et lorem quis risus mattis porta vitae ac velit. Nunc laoreet malesuada lectus at laoreet. Etiam ut tristique nulla. Pellentesque eros est, pretium sit amet convallis ut, convallis eu justo. Nulla suscipit blandit massa. Vestibulum vitae magna eu urna faucibus hendrerit. Etiam eros nibh, venenatis ac ullamcorper vitae, venenatis ac neque. Aenean vitae erat massa. Aliquam vulputate facilisis volutpat. Sed tincidunt dolor a enim dictum, eget tristique nisi laoreet. Quisque suscipit, odio et mattis mattis, lectus justo lacinia enim, nec finibus mi ante gravida sem.
Nunc ac nunc nec sapien porttitor volutpat. In et accumsan est. Duis non dui porta, pharetra dolor consequat, bibendum lorem. Quisque suscipit sit amet mi ac placerat. Integer cursus, est nec aliquet consequat, felis erat sodales sem, eget maximus ante libero in nibh. Cras et orci magna. Suspendisse viverra nibh eget nulla mattis laoreet. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi tortor diam, pulvinar quis erat a, posuere vehicula ante. Sed aliquet et magna nec finibus. Morbi in dapibus risus. Nullam ac imperdiet enim.
Nam enim mauris, volutpat et metus ut, euismod porttitor odio. Duis at orci hendrerit, posuere tortor sed, convallis nibh. Nullam lobortis est eget magna finibus porttitor. Nulla facilisi. Donec bibendum ac lorem eget consectetur. Phasellus at lacinia augue. Integer a mi quam. Morbi malesuada maximus diam. In orci nisi, mollis sed urna eget, fermentum efficitur libero. Cras auctor est sit amet ex aliquet, eu rutrum turpis gravida. Donec mattis, erat eget ultricies accumsan, odio urna egestas lectus, at mattis lorem sem scelerisque dolor. Curabitur aliquet venenatis bibendum.
"""),
]));
}
}
You can find a live example of this. Using selectable plugin.
It is not an easy task but you can Modify this code to do what you want.
When I launch a keyboard (on iOS and Android) I have an extra 50 px padding added above it which shows an empty space.
This isn't caused by other suggested problems where there are multiple Scaffold widgets. I know that this is caused by my layout and I'm not sure if it can be fixed at the moment.
I have a static navigation bar that appears across the app at the bottom. This is 50px high and sits below CupertinoApp. When the keyboard is launched, extra padding is added to fill this space it seems. This navigation bar is a Container widget with InkWell links to open views directly, sort of like CupertinoTabScaffold except I don't have tabs, it just pushes or replaces the current navigation item
Adding resizeToAvoidBottomInset: false, can't be used as this removes the scroll action or doesn't include the 50px so content is hidden.
This problem only occurs when the keyboard is active
The CupertinoApp builder
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => Future<bool>.value(true),
child: Column(
children: <Widget>[
Expanded(
child: DefaultTextStyle(
style: Fonts.defaultFontStyle(),
child: CupertinoApp(
navigatorKey: navigatorKey,
routes: routes.builder,
)),
),
BottomBar(),
],
),
);
}
Adding a minimal example to get across a better idea
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
final NAVIGATION_HEIGHT = 50.0;
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
static const home = "/";
static const about = "/about";
static const products = "/products";
Map<String, WidgetBuilder> builder = {
home: (BuildContext context) => MyPage(),
about: (BuildContext context) => MyPage(),
products: (BuildContext context) => MyPage(),
};
Widget _createButtonsBuilder(int index) {
String pageTitle = builder.keys.toList()[index].substring(1);
if (pageTitle.length == 0) {
pageTitle = "Home";
}
return Expanded(
child: Material(
color: Colors.black87,
child: InkWell(
onTap: () {
navigatorKey.currentState.push(CupertinoPageRoute<void>(
builder: (BuildContext context) => MyPage(
title: "$pageTitle",
navigationHeight: NAVIGATION_HEIGHT,
),
));
},
child: Container(
padding: EdgeInsets.all(10.0),
height: NAVIGATION_HEIGHT,
child: Text(
"$pageTitle",
style: TextStyle(
inherit: true, color: Colors.white, fontSize: 20.0),
textAlign: TextAlign.center,
),
)),
),
);
}
Widget _BottomBar() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: List.generate(builder.keys.toList().length,
(int i) => _createButtonsBuilder(i)),
);
}
#override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Container(
color: Colors.white,
child: Column(
children: <Widget>[
Expanded(
child: CupertinoApp(
title: 'Flutter Demo',
routes: builder,
navigatorKey: navigatorKey,
),
),
_BottomBar()
],
),
),
);
}
}
class MyPage extends StatefulWidget {
MyPage({Key key, this.title, this.navigationHeight}) : super(key: key);
final String title;
final double navigationHeight;
#override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
#override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(),
child: Scrollbar(
child: SingleChildScrollView(
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'${widget.title}',
),
SizedBox(height: 40.0),
Container(
margin: EdgeInsets.all(30.0),
child: CupertinoTextField(
placeholder: "Tap in here",
),
),
Container(
margin: EdgeInsets.all(30.0),
child: Text(
"When tapping to add focus to the above CupertinoTextField Widget the keyboard will appear as normal but because of the _BottomBar() outside the CupertinoApp the height (${widget.navigationHeight}px) is included in the padding above the keyboard")),
Container(
margin: EdgeInsets.all(30.0),
child: Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi interdum blandit diam sed faucibus. Donec ut enim in ante luctus sagittis. Donec vestibulum aliquet nunc, in efficitur erat molestie quis. In dictum aliquet neque. Vivamus pharetra nibh dictum urna sodales malesuada. Nunc porta condimentum mi, sed laoreet erat maximus vitae. Nunc luctus nisi urna, a luctus nisi consectetur quis. Sed rhoncus euismod nisl, in laoreet leo molestie sed. Suspendisse aliquet commodo dui, sit amet rhoncus sapien venenatis in. Nulla tempus libero diam, non cursus odio euismod at. Fusce nec ipsum ipsum. Mauris congue blandit risus, vitae pretium leo euismod id. Vivamus venenatis finibus diam id auctor. Donec vel urna finibus erat viverra bibendum. Vivamus sagittis eros id bibendum tristique. Nullam eleifend elit dapibus elit porttitor, ac egestas libero mollis.\n\nMorbi eleifend ligula sed leo placerat tristique. Mauris consequat fringilla maximus. Fusce pharetra ultrices risus, quis fermentum urna ultrices non. Vivamus suscipit nunc non ipsum ultrices laoreet. Etiam sed vestibulum eros, nec tempus neque. Vestibulum efficitur mauris ac ipsum aliquet, et tincidunt massa suscipit. Vivamus enim justo, viverra tempor purus eu, elementum tempor tortor. Sed rhoncus gravida sem, vitae molestie augue iaculis et. Donec augue ligula, interdum id interdum sit amet, condimentum in dolor. Donec dignissim erat lorem, ut accumsan felis porta id. Nunc lorem enim, maximus ut odio at, ultrices sodales velit."))
],
),
),
),
) // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
I wrapped my CupertinoApp in Scaffold widget which exposed bottomNavigationBar.
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => Future<bool>.value(true),
child: Scaffold(
body: DefaultTextStyle(
style: Fonts.defaultFontStyle(),
child: CupertinoApp(
navigatorKey: navigatorKey,
routes: routes.builder,
)),
),
bottomNavigationBar: HomeBottomBar(),
));
}
And then added resizeToAvoidBottomPadding = true to any CupertinoPageScaffold's that have CupertinoTextfield children
EDIT:
ENTIRE DART FILE -- Here is the entire dart file. Im not seeing how to call the method in the ARR declaration and return the correct mapping. Just need to generate the array from the http call and we are good. This is our first run at returning multiple records from JSON and iterating thru it.
import 'package:flutter/material.dart';
import 'package:lightbridge_mobile/screens/forum/assets/colors.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:lightbridge_mobile/screens/forum/assets/app_bar_forum.dart';
import 'package:lightbridge_mobile/models/forum_answers.dart';
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
class ForumDetailPage extends StatefulWidget {
#override
_ForumDetailPageState createState() => new _ForumDetailPageState();
}
class _ForumDetailPageState extends State<ForumDetailPage> {
#override
Widget build(BuildContext context) {
var questionSection = new Padding(
padding: const EdgeInsets.all(8.0),
child: new Column(
children: <Widget>[
new Text(
// Post Title
"How do I become a expert in programming as well as design ??",
textScaleFactor: 1.5,
style: new TextStyle(fontWeight: FontWeight.bold, fontSize: 15.0 ),
),
new Padding(
padding: const EdgeInsets.all(10.0),
child: new Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
new IconWithText(Icons.laptop_windows, "Technology", iconColor: Colors.grey),
new IconWithText(
Icons.question_answer,
"Answered",
iconColor: Colors.grey,
),
new IconWithText(Icons.remove_red_eye, "54", iconColor: Colors.grey)
],
),
),
new Divider( height: 1.0)
],
),
);
var responses = new Container(
padding: const EdgeInsets.all(8.0),
child: new ListView.builder(
itemBuilder: (BuildContext context, int index) => new ForumPost(ForumPostArr[index]),
itemCount: ForumPostArr.length,
)
);
return new Scaffold(
appBar : LBForumAppBar().getAppBar(),
body: new Column(
children: <Widget>[
questionSection,
new Expanded(
child: new Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: responses,
))
],
),
);
}
}
var ForumPostArr = [
new ForumPostEntry("User1", "2 Days ago", 0 , 0 , "Hello,\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
new ForumPostEntry("User2", "23 Hours ago", 1 , 0 , "Pellentesque justo metus, finibus porttitor consequat vitae, tincidunt vitae quam. Vestibulum molestie sem diam. Nullam pretium semper tempus. Maecenas lobortis lacus nunc, id lacinia nunc imperdiet tempor. Mauris mi ipsum, finibus consectetur eleifend a, maximus eget lorem. Praesent a magna nibh. In congue sapien sed velit mattis sodales. Nam tempus pulvinar metus, in gravida elit tincidunt in. Curabitur sed sapien commodo, fringilla tortor eu, accumsan est. Proin tincidunt convallis dolor, a faucibus sapien auctor sodales. Duis vitae dapibus metus. Nulla sit amet porta ipsum, posuere tempor tortor.\n\nCurabitur mauris dolor, cursus et mi id, mattis sagittis velit. Duis eleifend mi et ante aliquam elementum. Ut feugiat diam enim, at placerat elit semper vitae. Phasellus vulputate quis ex eu dictum. Cras sapien magna, faucibus at lacus vel, faucibus viverra lorem. Phasellus quis dui tristique, ultricies velit non, cursus lectus. Suspendisse neque nisl, vestibulum non dui in, vulputate placerat elit. Sed at convallis mauris, eu blandit dolor. Vivamus suscipit iaculis erat eu condimentum. Aliquam erat volutpat. Curabitur posuere commodo arcu vel consectetur."),
new ForumPostEntry("User3", "2 Days ago", 5 , 0 , "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
new ForumPostEntry("User4", "2 Days ago", 0 , 0 , "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."),
];
Future<List<ForumAnswers>> getForumAnswers(String postID) async {
final response =
await http.post('http://api/ForumAnswers',
headers: {"Content-Type": "application/json",
'Accept': 'application/json',},
body: json.encode({'PostID' : postID }));
if (response.statusCode == 200) {
// If the call to the server was successful, parse the JSON
List l = json.decode(response.body);
List<ForumAnswers> posts = l.map((m) => ForumAnswers.fromJson(m)).toList();
return posts;
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load user');
}
}
class ForumPostEntry{
final String username;
final String hours;
final int likes;
final int dislikes;
final String text;
ForumPostEntry(this.username, this.hours, this.likes, this.dislikes, this.text);
}
class ForumPost extends StatelessWidget {
final ForumPostEntry entry;
ForumPost(this.entry);
#override
Widget build(BuildContext context) {
return new Container(
margin: const EdgeInsets.only(bottom: 10.0),
decoration: new BoxDecoration(
color: Colors.grey,
borderRadius: const BorderRadius.all(const Radius.circular(20.0)),
),
child: new Column(
children: <Widget>[
new Container(
decoration: new BoxDecoration(
color: Colors.grey[600],
borderRadius: const BorderRadius.only(
topLeft: const Radius.circular(20.0),
topRight: const Radius.circular(20.0)),
),
child: new Row(
children: <Widget>[
new Icon(
Icons.person,
size: 50.0,
color: Colors.white
),
new Expanded(
child: new Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
new Text(
entry.username
),
new Text(
entry.hours
),
],
),
),
new Row(
children: <Widget>[
new Padding(
padding: const EdgeInsets.all(2.0),
child: new Icon(Icons.thumb_up, color: Colors.white),
),
new Padding(
padding: const EdgeInsets.all(2.0),
child: new Text(entry.likes.toString()),
),
new Padding(
padding: const EdgeInsets.all(2.0),
child: new Icon(Icons.thumb_down, color: Colors.white),
),
new Padding(
padding: const EdgeInsets.only(right: 8.0, left: 2.0),
child: new Text(entry.dislikes.toString()),
),
],
)
],
),
),
new Container(
margin: const EdgeInsets.only(left: 2.0,right: 2.0,bottom: 2.0),
padding: const EdgeInsets.all(8.0),
decoration: new BoxDecoration(
color: Colors.grey[200],
borderRadius: const BorderRadius.only(bottomLeft :const Radius.circular(20.0),bottomRight :const Radius.circular(20.0))
),
child: new Text(entry.text),
),
],
),
);
}
}
class IconWithText extends StatelessWidget {
final IconData iconData;
final String text;
final Color iconColor;
IconWithText(this.iconData, this.text, {this.iconColor});
#override
Widget build(BuildContext context) {
return new Container(
child: new Row(
children: <Widget>[
new Icon(
this.iconData,
color: this.iconColor,
),
new Padding(
padding: const EdgeInsets.only(left: 8.0),
child: new Text(this.text),
),
],
),
);
}
}
Presumably, your json consists of an array at the outer most level; it's enclosed in [..].
l, the result of json.decode will be a List<dynamic>, though the list will happen to contain Maps.
You should find that this works better:
List l = json.decode(response.body);
List<ForumAnswers> posts = l.map((m) => ForumAnswers.fromJson(m)).toList();
Note that if you want to return posts you should change the signature to returning Future<List<ForumAnswers>>.
use these
factory ForumAnswers.fromJson(Map parsedjson) {
return ForumAnswers(
content: parsedjson['content'],
username: parsedjson['username'],
createDate : parsedjson['createDate'],
upvote : parsedjson['upvote'],
);
if not follow these link https://medium.com/flutter-community/parsing-complex-json-in-flutter-747c46655f51
I have an Image component in a scrollable screen. At beginning when the screen open, the image cannot be seen but you need to scroll down to view it.
How can you make sure the image is completely seen by the user after they have scrolled to it? I want to count the image impression of user.
How do you achieve this in flutter?
I didn't have much information about your code, so this is how I solved it. The impression is only counted when the image is completely visible on the screen, you can change that using _count = expression. And I used simple Container for Image.
Take a look at this screenshot first.
Code
void main() => runApp(MaterialApp(home: HomePage()),);
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
ScrollController _scrollController;
double _heightListTile = 56, _heightContainer = 200, _oldOffset = 0, _heightBox, _initialAdd;
int _initialCount, _count, _previousCount = 0, _itemsInList = 4;
#override
void initState() {
super.initState();
_heightBox = ((_itemsInList) * _heightListTile) + _heightContainer;
_scrollController = ScrollController();
_scrollController.addListener(() {
double offset = _scrollController.offset;
if (offset >= _oldOffset) {
_oldOffset = offset;
_count = _initialCount + (offset + _initialAdd) ~/ _heightBox;
if (_count != _previousCount) setState(() {});
_previousCount = _count;
}
});
Timer.run(() {
bool isIos = Theme.of(context).platform == TargetPlatform.iOS;
var screenHeight = MediaQuery.of(context).size.height - (isIos ? 100 : 80); // for non notches phone use 76 instead of 100 (it's the height of status and navigation bar)
_initialCount = screenHeight ~/ _heightBox;
_initialAdd = screenHeight % _heightBox;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_count == null ? "Let's count" : "Images shown = ${_count}")),
body: ListView.builder(
itemCount: 100,
controller: _scrollController,
itemBuilder: (context, index) {
if (index == 0) return Container();
if (index != 0 && index % (_itemsInList + 1) == 0) {
return Container(
height: _heightContainer,
alignment: Alignment.center,
color: Colors.blue[(index * 20) % 1000],
child: Text("Image #${(index + 1) ~/ 5}"),
);
}
return SizedBox(height: _heightListTile, child: ListTile(title: Text("Item ${index}")));
},
),
);
}
}
This solution will detect if your Image has been fully visible on your user screen, and will change the AppBar title if so. Assuming you want to display a single page with some content and an Image:
class ImageDisplayDetection extends StatefulWidget {
ImageDisplayDetection({Key key,}) : super(key: key);
#override
_ImageDisplayDetectionState createState() => _ImageDisplayDetectionState();
}
class _ImageDisplayDetectionState extends State<ImageDisplayDetection> {
ScrollController _controller; // To get the current scroll offset
var _itemSize = 400.0 ; // The height of your image
double _listSize = 2000.0 ;
double position = 1500.0 ; // position from the top of the list where the image begins
var seen = false ; // to report the visibility of your image
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener); // The listener will be used to check if the image has become visible
super.initState();
}
_scrollListener() {
setState(() {
// This 60.0 is the assumed hieght of the bottom navigation buttons so the image won't be considered visible unless it is above these buttons
if((_controller.offset + MediaQuery.of(context).size.height) >= position + _itemSize + 60.0){
seen = true ;
}
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.grey.shade200 ,
appBar: new AppBar(title: new Text(seen ? 'Image Displayed Successfully' : 'Image not displayed'),),
body: ListView.builder(
controller: _controller ,
itemCount: 1,
itemBuilder: (context, index) {
return Container(
height: _listSize ,
child: new Stack(
children: <Widget>[
// You can include other childern here such as TextArea
Positioned(
top: position,
child: SizedBox(
height: _itemSize,
width: _itemSize,
child: ClipRRect(
borderRadius: BorderRadius.circular(5.0),
child: Image.asset('assets/images/2.jpg'), //Change the image widget to match your own image source and name
),
),
),
],
),
);
}),
);
}
}
If you rather want a listview with multiple ListTiles you can opt for this answer which can detect whether a child which an arbitrary index has become visible and is displayed at certain position in the screen.
There is no way yet to know the visible item in the listView. Follow this issue. You can add items in the listview and check whether you have reached the bottom of the list using the ScrollController.
import 'package:flutter/material.dart';
void main() => runApp(MainPage());
class MainPage extends StatefulWidget {
#override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
ScrollController _controller;
#override
void initState() {
_controller = ScrollController();
_controller.addListener(_scrollListener);
super.initState();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.white,
body: ListView(
controller: _controller,
children: <Widget>[
Text(text),
Text(text),
Text(text),
Text(text),
Text(text),
Image.network(
'https://sample-videos.com/img/Sample-png-image-200kb.png'),
Text(text),
],
),
),
);
}
_scrollListener() {
if (_controller.offset >= _controller.position.maxScrollExtent &&
!_controller.position.outOfRange) {
// reached at the bottom of list
// Increment the view by one
}
}
String text =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur id ornare orci. In aliquet sed leo vel suscipit. Suspendisse eget dolor arcu. Duis fermentum quam suscipit nisl interdum fermentum. Aliquam laoreet, mi eu gravida rutrum, elit ex ornare erat, in egestas leo augue ac nisl. Sed vitae commodo metus, nec vulputate dui. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Phasellus rhoncus tellus nec diam elementum laoreet. Phasellus ac sapien leo. Donec dolor ante, porta quis pellentesque quis, iaculis vitae quam. Sed bibendum tortor a vestibulum malesuada. Duis non nisl congue, fringilla nulla et, laoreet odio.';
}
https://pub.dev/packages/visibility_detector can be used for this. It has a VisibilityDetector widget that can wrap any other widget and notify when the visible area of the widget changed. With a bit more logic, we can create an ImpressionDetector, which notifies when a widget is seen on screen for more than X time units:
/// Widget that fires a callback when its [child] is visible on screen continuously
/// for more than [durationForImpression]. We consider that the [child] is visible
/// if its visible fraction is more than [minVisibilityThreshold].
///
/// This widget is built from [VisibilityDetector]. Thus, [childKey] is a key that
/// should be unique between all [ImpressionDetector] (similarly to VisibilityDetector).
///
/// Callback [onImpression] is only fired once per widget. Notably, if the widget
/// goes off screen and later comes back, it may be reconstructed as a new widget
/// in which case there could be a new callback fired.
class ImpressionDetector extends StatefulWidget {
final Key childKey;
final Widget child;
final VoidCallback onImpression;
final Duration durationForImpression;
final double minVisibilityThreshold;
ImpressionDetector({
#required this.childKey,
#required this.child,
#required this.onImpression,
this.durationForImpression = const Duration(seconds: 2),
this.minVisibilityThreshold = 0.75,
});
#override
State<StatefulWidget> createState() => _ImpressionDetectorState();
}
class _ImpressionDetectorState extends State<ImpressionDetector> {
bool beingViewed = false;
int beingViewedCount = 0;
bool callbackCalled = false;
#override
Widget build(BuildContext context) {
if (callbackCalled) {
return widget.child;
}
return VisibilityDetector(
key: widget.childKey,
onVisibilityChanged: (info) {
if (!beingViewed) {
if (info.visibleFraction > widget.minVisibilityThreshold) {
startBeingViewed();
}
} else {
if (info.visibleFraction < widget.minVisibilityThreshold) {
stopBeingViewed();
}
}
},
child: widget.child,
);
}
void startBeingViewed() {
beingViewed = true;
int currentBeingViewed = ++beingViewedCount;
Future.delayed(widget.durationForImpression, () {
if (currentBeingViewed == beingViewedCount) {
widget.onImpression();
setState(() {
callbackCalled = true;
});
}
});
}
void stopBeingViewed() {
beingViewed = false;
++beingViewedCount;
}
}
Consider the following code:
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new FlatButton(
child: new Container(
child: new Center(child: new Text("ABOVE")),
height: 300.0,
color: const Color.fromARGB(255, 255, 0, 0),
),
),
new Expanded(
child: new Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",)
),
new FlatButton(
child: new Container(
child: new Center(child: new Text("BELOW")),
height: 300.0,
color: const Color.fromARGB(255, 255, 0, 0),
),
),
],
);
}
}
On my Pixel, this produces:
My expectation was that the text would continue up until it reaches the bottom button, then clip. If I set overflow: TextOverflow.ellipsis then it truncates after the first line:
If I set maxLines: 3 then it continues until the fourth line:
However, I can't find a way to just continue until it reaches the bottom button.
Can anyone enlighten me?
There is no direct styling option that does this, but however you can do it by first calculating maxLines that can occupy the available view in runtime and then just specify overflow and maxLines properties.
To get available height, use a LayoutBuilder the provides the constrains
lineHeight = fontSize * textScaleFactor * lineHeightScaleFactor
maxlines = (available height/line height)
Example:
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new Material(
child: new Column(
children: <Widget>[
new FlatButton(
child: new Container(
child: new Center(child: new Text("ABOVE")),
height: 300.0,
color: const Color.fromARGB(255, 255, 0, 0),
),
),
new Expanded(child: new LayoutBuilder(builder: (context, constrains) {
double lineScaleFactor = 1.1; // this is multiplied with fontsize to get lineHeight
TextStyle style = new TextStyle(fontSize: 16.0,height: lineScaleFactor);
double scale = 1.0;
double lineHeight = style.fontSize*scale*lineScaleFactor;
return new Container(
child: new Text(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
overflow: TextOverflow.ellipsis,
style: style,
textScaleFactor: scale,
maxLines: (constrains.maxHeight ~/ lineHeight),
),
);
})),
new FlatButton(
child: new Container(
child: new Center(child: new Text("BELOW")),
height: 300.0,
color: const Color.fromARGB(255, 255, 0, 0),
),
),
],
),
);
}
}
Hope that helped!