How to make Flickable ensure the visibility of an item inside of it?

3.1k views Asked by At

I have a Flickable that includes a large number of TextField objects laid out in a column with each TextField anchored to the bottom on the previous TextField. Everything is working fine except that when I use the tab key to navigate through these fields, eventually the focus goes to a TextField that is outside the visible rectangle of the Flickable and then the user can't see the cursor until they scroll down the Flickable manually.

Essentially I'm looking for some kind of ".ensureVisible()" method such that when a TextField receives the focus, the Flickable is automatically scrolled so that the just-focused TextField is entirely visible.

2

There are 2 answers

1
dtech On

Have you considered a more model-ar approach to this? I mean if you use something like a ListView you can simply change the currentItem at which point the view will automatically scroll to it if it is out of the visible range.

Additionally, it will only load the text elements that are in the visible range, saving on some memory.

But even with your current approach it won't be that complex to ensure visibility.

  Flickable {
    id: flick
    anchors.fill: parent
    contentHeight: col.height
    function ensureVisible(item) {
      var ypos = item.mapToItem(contentItem, 0, 0).y
      var ext = item.height + ypos
      if ( ypos < contentY // begins before
          || ypos > contentY + height // begins after
          || ext < contentY // ends before
          || ext > contentY + height) { // ends after
        // don't exceed bounds
        contentY = Math.max(0, Math.min(ypos - height + item.height, contentHeight - height))
      }
    }
    Column {
      id: col
      Repeater {
        id: rep
        model: 20
        delegate: Text {
          id: del
          text: "this is item " + index
          Keys.onPressed: rep.itemAt((index + 1) % rep.count).focus = true
          focus: index === 0
          color: focus ? "red" : "black"
          font.pointSize: 40
          onFocusChanged: if (focus) flick.ensureVisible(del)
        }
      }
    }
  }

The solution is quick and cruddy, but it will be trivial to put it into production shape. It is important to map to the contentItem rather than the flickable, as the latter would give the wrong results, taking the amount of current scrolling into account. Using mapping will make the solution agnostic to whatever positioning scheme you might be using, and will also support arbitrary levels of nested objects.

0
Fractal On

dtech's answer is spot on. It's easily combined with a nice snap-to animation and easy to modify for x-direction flickables too. Also, the user may be deliberately flicking or dragging the flickable. In my case, C++ code was controlling the text or display effects for the items in a grid layout, contained in the flickable. The flickable needed to flick nicely when the C++ code signalled it to do so, but not if the user was deliberately dragging or flicking. Here is dtech's function modified for an x-direction flickable:

function ensureVisible(item) {
    if (moving || dragging)
        return;
    var xpos = item.mapToItem(contentItem, 0, 0).x
    var ext = item.width + xpos
    if ( xpos < contentX // begins before
              || xpos > contentX + width // begins after
              || ext < contentX // ends before
              || ext > contentX + width) { // ends after
        // don't exceed bounds
        var destinationX = Math.max(0, Math.min(xpos - width + item.width, contentWidth - width))
        ensureVisAnimation.to = destinationX;
        ensureVisAnimation.from = contentX;
        ensureVisAnimation.start();
    }
}
//This animation is for the ensure-visible feature.
NumberAnimation on contentX {
    id: ensureVisAnimation
    to: 0               //Dummy value - will be set up when this animation is called.
    duration: 300
    easing.type: Easing.OutQuad;
}