Skip to content

Add more configuration to the viewabilityConfig #891

Open
@IslamRustamov

Description

@IslamRustamov

Introduction

I want to discuss an ability to add more configuration to the viewabilityConfig, so that it's possible to more effectively control the calculations of viewable items of the VirtualizedLists.

Details

Let's bring up an example.

  1. We have a FlatList of images;
  2. We have a viewabilityConfig with itemVisiblePercentThreshold: 15;
  3. We have an absolutely positioned View above the FlatList. This View is serving a purpose of some custom header and we add paddingTop: HEADER_HEIGHT to the contentContainerStyle of the FlatList so that we see all of the list;
  4. Every time an image becomes "not visible" we want to make it slightly opaque.

Here is the result:

Image

We can see that the image AT THE BOTTOM is getting slightly opaque, when less than 15% of it is not visible. However, we can't say the same thing about the images on top of the screen. (couldn't upload gifs for some reason so bear with my screenshots)

Why is this happening? Because our header is an absolute View and the image is continuing to be visible underneath the header. And FlatList cannot correctly computate the offset from top.

In order to solve this problem, I suggest to make the next changes, which are going to allow FlatList to correctly work with absolute Views on top or below the screen:

In ViewabilityConfig (@react-native/virtualized-lists/Lists/VirtualizedList.d.ts) interface we add absoluteStartOffset and absoluteEndOffset:

export interface ViewabilityConfig {
  /**
   * Minimum amount of time (in milliseconds) that an item must be physically viewable before the
   * viewability callback will be fired. A high number means that scrolling through content without
   * stopping will not mark the content as viewable.
   */
  minimumViewTime?: number | undefined;

  /**
   * Percent of viewport that must be covered for a partially occluded item to count as
   * "viewable", 0-100. Fully visible items are always considered viewable. A value of 0 means
   * that a single pixel in the viewport makes the item viewable, and a value of 100 means that
   * an item must be either entirely visible or cover the entire viewport to count as viewable.
   */
  viewAreaCoveragePercentThreshold?: number | undefined;

  /**
   * Similar to `viewAreaCoveragePercentThreshold`, but considers the percent of the item that is visible,
   * rather than the fraction of the viewable area it covers.
   */
  itemVisiblePercentThreshold?: number | undefined;

  /**
   * Nothing is considered viewable until the user scrolls or `recordInteraction` is called after
   * render.
   */
  waitForInteraction?: boolean | undefined;

  /**
   * Offset from the start of the list
   */
  absoluteStartOffset?: number | undefined; // <-- add this

  /**
   * Offset from the end of the list
   */
  absoluteEndOffset?: number | undefined; // <-- and this
}

In computeViewableItems (@react-native/virtualized-lists/Lists/ViewabilityHelper.js) we make the next changes:

  /**
   * Determines which items are viewable based on the current metrics and config.
   */
  computeViewableItems(
    props: CellMetricProps,
    scrollOffset: number,
    viewportHeight: number,
    listMetrics: ListMetricsAggregator,
    // Optional optimization to reduce the scan size
    renderRange?: {
      first: number,
      last: number,
      ...
    },
  ): Array<number> {
...
      const absoluteStartOffset = this._config.absoluteStartOffset ?? 0; // <-- add this
      const absoluteEndOffset = this._config.absoluteEndOffset ?? 0;  // <-- add this

      const top = Math.floor(metrics.offset - scrollOffset - absoluteStartOffset);  // <-- change here
      const bottom = Math.floor(top + metrics.length);

      if (top < viewportHeight - absoluteStartOffset && bottom > 0) {  // <-- change here
        firstVisible = idx;
        if (
          _isViewable(
            viewAreaMode,
            viewablePercentThreshold,
            top,
            bottom,
            viewportHeight - absoluteStartOffset - absoluteEndOffset,  // <-- change here
            metrics.length,
          )
        ) {
          viewableIndices.push(idx);
        }

Now, if I pass my absoluteStartOffset to the viewabilityConfig - we will see the next result:

Image

We can see that both TOP and BOTTOM images are opaque, which means that viewability calculations are correct now.

We can also make it work with absolute views, located at the bottom of the screen by utilising absoluteEndOffset:

Image

You may say that it's my fault that I'm using an absolute view as a header for some reason, but there are real-life scenarios where this is being used with more complicated UI than I showed in the examples above (the reason why I suggest this change is exactly because it fixes the problem for my application which has some animations on top and rebuilding UI is not as easy as simply adding these changes).

Adding custom getItemLayout with different offset is not a solution to this problem.

This solution also works for horizontal lists.

Here is the full gist of example: https://gist.github.com/IslamRustamov/653747e65be5f59dcce953345f3dff2a

The idea of this came from https://github.com/fredrikolovsson/react-native-viewability-tracking-view

Discussion points

  1. Adding absoluteStartOffset and absoluteEndOffset to the viewabilityConfig

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions