questionasker
questionasker

Reputation: 2697

How to know if a flutter image has been seen by the user

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?

Upvotes: 6

Views: 2045

Answers (4)

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;
  }
}

Upvotes: 1

CopsOnRoad
CopsOnRoad

Reputation: 267664

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.

enter image description here


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}")));
        },
      ),
    );
  }
}

Upvotes: 7

Mazin Ibrahim
Mazin Ibrahim

Reputation: 7869

enter image description here

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.

Upvotes: 3

divyanshu bhargava
divyanshu bhargava

Reputation: 1585

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.';
}

Upvotes: 1

Related Questions