preface
Long time no see everybody, because work reason here silence for a long time, but now I come back! Today’s article to introduce the content, is we often use a scene: buried point. In order to track the behavioral characteristics of users, and then analyze the data quantitatively, and optimize the product, we often need to report the data burying point at a specific time, presumably you are familiar with it. Showing buried points is one of the most frequently used scenarios.
๐ฅฒ Slide the pain of buried point
In Flutter, we usually report presentation buries during the initState lifecycle, which is of course fine in normal usage scenarios. However, this solution does not work in the sliding scenario, so let’s see.
Obviously, we printed widgets that weren’t shown otherwise. If you do so, the buried point report is not accurate, which will cause irreparable loss to the service.
๐คฏ ScrollView loading mechanism
Why does this happen? After reviewing the source code, we see that all ScrollViews are drawn in a viewPort area. To make sliding more smooth, scrollViews are usually loaded outside of the viewport area, called cacheExtent. Items that fall into this cache are laid out even if they are not yet visible on the screen. InitState is then executed. ListView, a subclass of ScrollView, also uses this mechanism.
Naturally, the simplest solution would be to disable the preloading mechanism.
ListView.builder(
cacheExtent: 0,
itemCount: 40,
itemBuilder: (context, index) {
returnItem(index: index); },),Copy the code
Well, that’s the end of this article, have you learned. ๐
๐ค New problem
You might look at this and think: That’s it? I think this guy’s got a story to tell.
Just kidding, as you can easily imagine, there is a high probability that this will cause performance problems. In the real business of our internationalization, considering the global user devices, the performance of mobile phones in some regions is still at the level of iPhone 5S, and the complexity of the business, it is certainly not so simple to cancel the preloading can be solved.
When testing at that time, it was found that if the caching mechanism was removed, the average frame rate would drop by 5-10 frames, which was still the test result on a better OnePlus phone, which was of course unacceptable. (Not to mention ListView performance issues with 1.x version Flutter itself)
So what we want is a highly accurate burying scheme for user behavior on Flutter without affecting the performance of the ScrollView.
๐คจ broken
Once you’ve thought through the requirements, you’re halfway there. After reviewing the existing information in the industry, we found that Idle fish technology has shared a good solution: # Reveal! How to design a highly accurate Burial-point frame for Flutter. However, there is no open source plan for this solution, so I have to make one myself. How do we solve this problem?
As mentioned earlier, each ScrollView has its own ViewPort to determine its drawing scope. This ViewPort generates a RenderObjectElement that can render the region individually, minimizing the impact of return. So the question now becomes we want to calculate when an Item goes into the ViewPort.
A complex problem needs to be abstracted into a simpler problem and solved step by step, so let’s think of item as a point, and see what information we need to figure out if an item is in a ViewPort.
It’s easy to think about the Scroll Offset of the slide, the ViewPort Length of the slide, and the Item itself, That is, the Exposure Offset of the current Item from the sliding starting point.
Imagine sliding, an Item slides in from the right side of the ViewPort, enters the ViewPort, is seen by the user, and then slides out from the left side of the ViewPort. We can abstract this process into the following four states:
- (Scroll Offset + ViewPort Length < Exposure Offset)
- (Scroll Offset + ViewPort Length > Exposure Offset)
- The Item in the ViewPort
- Exposure Offset < Scroll Offset
For the left to right, here are the states:
- Exposure Offset < Scroll Offset
- [Exposure Offset > Scroll Offset]
- The Item in the ViewPort
- (Scroll Offset + ViewPort Length < Exposure Offset)
It can be found from observation that the judging timing of Item is different from that of Item drawn from the left and right, so we need to distinguish the two sliding cases.
Now let’s add the Item Width and use the above conclusion to calculate it.
We’re going to assume for the moment that an Item is only a presentation if it’s all in the ViewPort.
- (Scroll Offset + ViewPort Length < Exposure Offset)
- (Scroll Offset + ViewPort Length > Exposure Offset)
- The Item in the ViewPort
- Exposure Offset + Item Width < Scroll Offset
For the left to right, here are the states:
- Exposure Offset + Item Width < Scroll Offset
- Exposure Offset + Item Width > Scroll Offset
- The Item in the ViewPort
- (Scroll Offset + ViewPort Length < Exposure Offset)
๐งฉ How to obtain this information
Now that you know how, it’s just a matter of looking for the pieces of the puzzle.
Item size information
This one is relatively simple. We all know that we can use Widget BuildContext to get the corresponding RenderObject, and use it to get the current Item’s length and width.
// The size of the exposure pit is named here. For different sliding directions, we need to use different lengths.
final exposurePitSize = (context.findRenderObject() as RenderBox).size;
Copy the code
The context here is we want to determine whether the exposure of the context of the Item, if it is not clear on this concept, you can go to see the Flutter | understand BuildContext.
Note: Not every Widget will create a RenderObject, only RenderObjectWidget will. ListView will default to help add a RepaintBoundary each Item, the Widget is a SingleChildRenderObjectWidget, So each Item actually has a corresponding RenderObject.
// the build method of SliverChildListDelegate
if (addRepaintBoundaries) child = RepaintBoundary(child: child);
Copy the code
ViewPort Indicates the size
The ViewPort exists in the ListView hierarchy, so we need to find it from the ancestor node. Fortunately, Flutter already provides us with this method.
static RenderAbstractViewport? of(RenderObject? object) {
while(object ! =null) {
if (object is RenderAbstractViewport)
return object;
object = object.parent asRenderObject? ; }return null;
}
Copy the code
We have just got the Item corresponding to render objects, RenderAbstractViewport. Of can pass the RenderObject upward looking for ancestor nodes, Until we find the RenderAbstractViewport that’s closest to it, we’ll get the ViewPort information we want.
Size? getViewPortSize(BuildContext context) {
final RenderObject? box = context.findRenderObject();
final RenderAbstractViewport? viewport = RenderAbstractViewport.of(box);
assert(() {
if(viewport ! =null) {
debugPrint('Please make sure you have a `ScrollView` in ancestor');
return false;
}
return true;
});
finalSize? size = viewport? .paintBounds.size;return size;
}
Copy the code
The distance between the Item and the sliding start of the ViewPort
In getOffsetToReveal, another method of RenderAbstractViewport, we can get the starting position of the current RenderObject sliding relative to the ViewPort.
double getExposureOffset(BuildContext context) {
final RenderObject? box = context.findRenderObject();
final RenderAbstractViewport? viewport = RenderAbstractViewport.of(box);
if (viewport == null || box == null| |! box.attached) {return 0.0;
}
// Box is the RenderObject of the current Item
// When alignment is 0, the relative offset from the starting point is obtained
// Get the relative offset from the destination when it is 1.
final RevealedOffset offsetRevealToTop =
viewport.getOffsetToReveal(box, 0.0, rect: Rect.zero);
return offsetRevealToTop.offset;
}
Copy the code
Sliding distance
There are usually two ways to obtain the slip distance:
- through
ScrollController
To obtain. - Use the Scrollable Widget
Notification
Mechanism.
Having to write ScrollController every time you write code seems a bit cumbersome, so we chose Notification. (It’s also more versatile.)
Scroll Notification
The Scrollable Widget notifies its ancestors about scroll changes, which can be captured using a NotificationListener. Currently, there are the following types of Notification:
ScrollStartNotification
: Scrolling starts when initiatedNotification
.ScrollUpdateNotification
: Continuously initiate while scrollingNotification
. (High frequency)ScrollEndNotification
: launched at the end of scrollNotification
.UserScrollNotification
: Initiates notification when the user changes the scrolling direction. (Usually occurs when scrollViews in different directions are nested with each other)
We use a NotificationListener here to get the slide information.
Widget buildNotificationWidget(BuildContext context, Widget child) {
return NotificationListener<ScrollNotification>(
onNotification: (scrollNotification) {
// Here you can get the scroll information
},
child: ScrollView,
);
}
Copy the code
Solve information sharing problems
Look at this, it looks like we have all the pieces of the puzzle, but what does it feel like? ๐ง
If you’re keen, you’ll see that with our current design, you can’t get all the information in one place.
Scroll Notification will only send Notification notifications to ancestor nodes, that is, we can’t get them at the Item level!
If we want to determine the buried point exposure in Item, we must obtain the scrollNotification in the higher ancestor node.
Of course, there are many solutions. Sharing state is a common Case in state management, but introducing a state management library in order to slide buried point exposure seems to be a bit of a waste. Therefore, it is better to use the primitive Inherit mechanism of Flutter to share data.
What is the Inherit mechanism
To understand the Inherit mechanism, you need to know about the three trees of Flutter. There are many explanations of Flutter on the web, so I will not go into detail. If you are interested, you can read the following article about how the Widget, Element and Render form the tree structure. .
In a nutshell, the Inherit mechanism is a way to share data from top to bottom in a Flutter. We know that a Flutter builds its views through a tree structure, The InheritedWidget allows its data to be accessed by widgets in all of the child nodes.
Is it possible that each Element holds an InheritedElement called Map
? A reference to the Map of _inheritedWidgets when our Element is mounted to the Element Tree (_updateInheritance is called when the mount is performed), Will keep a copy of the _InheritedWidget references saved in parent.
void _updateInheritance() {
assert(_lifecycleState == _ElementLifecycle.active); _inheritedWidgets = _parent? ._inheritedWidgets; }Copy the code
The Element created by the InheritedWidget inserts itself into the map when it is mounted, thus enabling top-down data sharing.
@override void _updateInheritance() { assert(_lifecycleState == _ElementLifecycle.active); final Map<Type, InheritedElement>? incomingWidgets = _parent? ._inheritedWidgets; if (incomingWidgets ! = null) _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets); else _inheritedWidgets = HashMap<Type, InheritedElement>(); _inheritedWidgets! [widget.runtimeType] = this; }Copy the code
Based on this, we can complete the calculation of sliding buried point exposure. Congratulations, congratulations. ๐น
With it you
For experienced developers like us, when we see a good article like this, the first thing we want to do is [?]
Just bring it to me
So for your precious (water-skiing/chitchat/baby-sitting /…) Time, this sliding burying point scheme has landed in the Pub warehouse, you can rest assured to eat.
Currently supported are:
- Lazy exposure mode: Exposure only when scrolling ends
- Exposure ratio: You can control how much of an exposure the Item displays
- Track when the Item is out of sight: The exposure time can be obtained
- Support for all ScrollViews: includes
ListView
,GridView
,CustomScrollView
And so on.
I will maintain this project all the time (after all, I will also use it myself). If you want to know the latest progress of this project, you can follow the Github of this project, or if you need to add functional requirements, you are welcome to contact me via email ~
Pub: pub.flutter-io.cn/packages/fl…
Github address: github.com/Vadaski/flu…
Email address: [email protected]
Write in the last
The solution is actually were used to in the company last year, there has been no time to open source, here thanks to idle fish technology also provide valuable ideas, gather together a few odds and ends of time recently gave it to finished, put before the first day of National Day finished writing the article, hope everybody can through my harvest to share a bit
Finally, please allow me to announce: Welcome to the international Passenger side/architecture team of Didi. Recently, the business of the department has been developing rapidly, and there are a lot of HC (Android/iOS/Flutter!). , here you can get access to the latest progress of Flutter and promote the construction of group Flutter. Pure Native students are also very welcome! Interested students can add my wechat 17608014106.
I am Xinlei, an engineer who learned Flutter happily with you. Happy National Day, everyone. See you later ๐