“I am participating in the Mid-Autumn Festival Creative Submission Contest, please see: Mid-Autumn Festival Creative Submission Contest for details

preface

Some of you may have read this article:

3 Steps to achieve a custom Chang ‘e flight to the Moon (Flutter version)

There’s one big question left in this article, besides the whole crap and the mind-boggling UI:

There is a problem with the clickable position of the item, which is different from the actual display position

Now this article will address that question;

In accordance with international practice, the first rendering (this time, in order to clearly show the click area, change the background to red and add index) :

Let’s first sort out what we need to change

1. Item size needs to be adjusted

[3. Modify drawing, draw according to path requirements]

if (tf? .position ! = null) {/// The height of the item is set to 100, and the listView seems to force the height of the item to be fixed at the height of the listView. ChildItemOffset = Offset(tf! .position.dx - child.size.width / 2, tf.position.dy - 50); }Copy the code

The reason for this is, as stated in the notes:

Take the horizontal listView as an example, where the height of all items is fixed.

sliver_list.dart:

@override void performLayout() { final SliverConstraints constraints = this.constraints; childManager.didStartLayout(); childManager.setDidUnderflow(false); final double scrollOffset = constraints.scrollOffset + constraints.cacheOrigin; Assert (scrollOffset > = 0.0); final double remainingExtent = constraints.remainingCacheExtent; Assert (remainingExtent > = 0.0); final double targetEndScrollOffset = scrollOffset + remainingExtent; final BoxConstraints childConstraints = constraints.asBoxConstraints(); . }Copy the code

sliver.dart:

/// Returns [BoxConstraints] that reflects the sliver constraints. /// /// The `minExtent` and `maxExtent` are used as the constraints in the main /// axis. If non-null, the given `crossAxisExtent` is used as a tight /// constraint in the cross axis. Otherwise, the [crossAxisExtent] from this /// object is used as a constraint in the cross axis. /// /// Useful for slivers that Have [RenderBox] children. BoxConstraints({double minExtent = 0.0, double maxExtent = double.infinity, double? crossAxisExtent, }) { crossAxisExtent ?? = this.crossAxisExtent; switch (axis) { case Axis.horizontal: return BoxConstraints( minHeight: crossAxisExtent, maxHeight: crossAxisExtent, minWidth: minExtent, maxWidth: maxExtent, ); case Axis.vertical: return BoxConstraints( minWidth: crossAxisExtent, maxWidth: crossAxisExtent, minHeight: minExtent, maxHeight: maxExtent, ); }}Copy the code

Both minHeight and maxHeight are set to crossAxisExtent;

Now we need to do the item click area judgment, naturally need to obtain the position and size of the item, if the size of the item can be obtained, this problem can also be solved incidentally ~

2. Click position calculation and judgment

For most ListViews, or Slivers, and their subclasses, the click process is pretty much the same:

Call the hitTestChildren method to determine whether the child is clicked

The specific implementation of the hitTestChildren method depends on the functionality required by each subclass, but most of the sliver and its subclasses we use look like this:

RenderSliverMultiBoxAdaptor, for example, we are the most commonly used to the listView and the gridView is to use it:

@override bool hitTestChildren(SliverHitTestResult result, { required double mainAxisPosition, required double crossAxisPosition }) { RenderBox? child = lastChild; final BoxHitTestResult boxResult = BoxHitTestResult.wrap(result); while (child ! = null) { if (hitTestBoxChild(boxResult, child, mainAxisPosition: mainAxisPosition, crossAxisPosition: crossAxisPosition)) return true; child = childBefore(child); } return false; }Copy the code

The method is simple: iterate over the child, using the hitTestBoxChild method to determine if the child is clicked.

And this hitTestBoxChild is just a normal way of judging the child based on the sliding distance and the sliding direction, and passing the click event to it;

However, now the display position of item has been changed to a custom path. If you want to follow this logic, it will naturally appear that the clicking position is inconsistent with the actual display as mentioned at the beginning

3. Click event pass

Of course, click events can’t just end at the listView level;

Solution and code modification

1. Item size adaptive:

The solution to this problem is also very simple, his mandatory allowed minimum height can, for example, in the method of performLayout constraints. AsBoxConstraints () followed by a statement like this:

ChildConstraints = BoxConstraints(minWidth: 0, maxWidth: 0) childConstraints = BoxConstraints(minWidth: 0, maxWidth: 0) childConstraints.maxWidth, minHeight: 0, maxHeight: childConstraints.maxHeight);Copy the code

Allow its minimum height to be 0;

In this way, the constraint level can be minimal adaptive. The 50 magic number mentioned above can also be changed to child.size. Height / 2

Click on the location and determine its effectiveness

Now all we need to do, in a nutshell, is to determine if the click location is on our custom path item, and if so, pass it

HitTestBoxChild = hitTestBoxChild = hitTestBoxChild ();

  • Modify the click event section and pass the actual location of the click eventhitTestBoxChild ]
  • [Determine whether to click on child]
  • [Pass click event]

Modify click events

First of all, if you make a breakpoint, you’ll see that the entry to hitTestBoxChild is not the location of the actual click pixel, but a converted value.

As mentioned above, the click event is passed layer by layer from top to bottom. If you trace the ViewPort, you will find that the position of the click event passed to the child is processed as follows:

The viewport. The dart — — RenderViewportBase

@override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {

  ......
  
  for (final RenderSliver child in childrenInHitTestOrder) {
    if (!child.geometry!.visible) {
      continue;
    }
    final Matrix4 transform = Matrix4.identity();
    applyPaintTransform(child, transform); // must be invertible
    final bool isHit = result.addWithOutOfBandPosition(
      paintTransform: transform,
      hitTest: (BoxHitTestResult result) {
        return child.hitTest(
          sliverResult,
          mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
          crossAxisPosition: crossAxisPosition,
        );
      },
    );
    if (isHit) {
      return true;
    }
  }
  return false;
}
Copy the code

One child. HitTest method into the ginseng, is a modified numerical computeChildMainAxisPosition method

So, just can be in before custom ViewPort OverScrollRenderViewportBase file, change this to mainAxisPosition, click transfer original location;

Determine whether to click on child

This is the hitTestBoxChild method is responsible for the specific part of the above simple analysis of the steps and functions of this method;

However, since we are a custom path, so although this method wrote a lot, for now, nothing useful… All wood big

So, if this piece needs to be redesigned, my idea is as follows:

  • Put useful things like the position and offset of the child in paint into a parentData. So when hitTest is triggered, you can get the actual location of the item directly from parentData without having to calculate or do anything else;

  • After getting the actual position, iterate over the child and subtract the actual position of the child from the click position to determine whether the child is in the range of child.size.

  • Send the event subtracting the child’s actual position to the child for subsequent hitTest event processing;

After the last three steps, you’ll see that it doesn’t do any good, not even the hitTestBoxChild method fires;

The reason is also very simple… The hitTest method specifies the maximum and minimum range… And the original pointer position, just outside the maximum range…

So you need to specify the maximum extent of hitTestExtent manually, for example in the performLayout method, in the constructor:

The last

Item now responds to events correctly. However, if you add the change effect to the Item, it will look like there is a problem…

It seems necessary to wrap all effects, including panning, transformation and so on, in a Widget and let the Widget handle them all.

Is the next step to implement a surrogate widget that is completely from widget to RenderObject and complies with flutter specifications?