ViewPort - Scroll on MouseDrag (Tactile devices)


#1

Hi Juce Team,

Doing applications for iOs, I was implementing a sub class of ListBox to be able to scroll it with a mouseDrag like the native mobile lists implementations. I thought It would be nice to go further by proposing you an implementation in Viewport that allows you to forget about scrollbars.
I tested it and it works perfectly with ListBox but there might be some modifications to do to other component that use viewport and mouseDrag. (might be in conflict with mouseDrag scrolling)

The code I join behaves like iOs native viewports except that it does not bounce when the top/buttom is reached.

 

Implem in Viewport:
To enable it I added this method to the viewport class:  void setShouldScrollOnDrag(bool)
When set to TRUE here is the new behavior for ListBox:

 - Disables Drag and Drop in ListBox::RowComponent

 - Force Selection on MouseUp

 - Disable Selection on MouseUp if ListBox was dragged/scrolled (isScrollingOnDrag())

Other method added: bool getShouldScrollOnDrag() /*getter*/

Other method added: bool isScrollingOnDrag() /* Returns wether the viewport is beeing scrolled by a mouse drag or not */

 

Behaviour in Viewport, explanations:

 - Counts fingers down to be able to scroll with only one finger and avoid multitouch weird behaviors

 - Starts a timer on MouseUp if the list was draged in order to keep on dragging according to the velocity that was given to the scroll. This is helpful to reach top/end of the list faster.

 

There is few things you can tweak manualy:

 - Init Velocity, how fast the auto scrolling starts (0.5)

 - Velocity reducer, how fast the auto scrolling slows down after each timer iteration (0.9)

 - Distance in Px reached by the finger before the dragging enables (to avoid scrolling by mistake) (5-10 px)

 - Speed that is necessary to trigger the autodrag (1)

 - Timer (10ms) ratio smooth/perfs

Here is the patch for viewport:

From 543c0e0f29d607f7fbe3ffca7e7683a79b565635 Mon Sep 17 00:00:00 2001
From: BastienC <b.commelongue@uvi.net>
Date: Fri, 24 Apr 2015 10:23:39 +0200
Subject: [PATCH] - Scroll on mouseDrag viewport

---
 modules/juce_gui_basics/layout/juce_Viewport.cpp | 104 ++++++++++++++++++++++-
 modules/juce_gui_basics/layout/juce_Viewport.h   |  17 ++++
 2 files changed, 120 insertions(+), 1 deletion(-)

diff --git a/modules/juce_gui_basics/layout/juce_Viewport.cpp b/modules/juce_gui_basics/layout/juce_Viewport.cpp
index 21360a0..9d5a236 100644
--- a/modules/juce_gui_basics/layout/juce_Viewport.cpp
+++ b/modules/juce_gui_basics/layout/juce_Viewport.cpp
@@ -22,6 +22,99 @@
   ==============================================================================
 */
 
+class Viewport::ScrollOnDragViewPort : public MouseListener, public Timer
+{
+public:
+  ScrollOnDragViewPort(Viewport &o) : owner(o),
+                                      numberFingerDown(0),
+                                      isDragging(false)
+  {
+    o.addMouseListener(this, true);
+  }
+
+  //==============================================================================
+  void mouseUp(const MouseEvent &e) override
+  {
+    if (numberFingerDown)
+      --numberFingerDown;
+
+    if (isDragging)
+    {
+      if (abs(lastScrollSpeed.x) > 1 || abs(lastScrollSpeed.y) > 1) //Autodrag Threshold
+       startTimer(10);
+    }
+    isDragging = false;
+  }
+  
+  //==============================================================================
+  void mouseDrag(const MouseEvent &e) override
+  {
+    if (!owner.shouldScrollOnDrag || numberFingerDown > 1)
+      return;
+    if (isDragging == false) //init drag
+    {
+      draggingVelocity = Point<double>(0.5, 0.5);
+      startScrollPx.x = owner.getViewPosition().x;
+      startScrollPx.y = owner.getViewPosition().y;
+      lastScrollingDistance = Point<int>(0, 0);
+      lastScrollSpeed = Point<int>(0, 0);
+    }
+    Point<int> distanceInPx = e.getOffsetFromDragStart();
+
+    lastScrollSpeed.y = e.getDistanceFromDragStartY() - lastScrollingDistance.y;
+    lastScrollSpeed.x = e.getDistanceFromDragStartX() - lastScrollingDistance.x;
+
+    if (!isDragging && abs(distanceInPx.x) < 10 && abs(distanceInPx.y) < 10)
+      return;
+    isDragging = true;
+
+    Point<int> moveInPx;
+    moveInPx.x = (startScrollPx.x + (-distanceInPx.x));
+    moveInPx.y = (startScrollPx.y + (-distanceInPx.y));
+
+    owner.setViewPosition(moveInPx.x, moveInPx.y);
+    lastScrollingDistance = distanceInPx;
+  }
+  
+  //==============================================================================
+  void mouseDown(const MouseEvent &e) override
+  {
+    stopTimer();
+    ++numberFingerDown;
+  }
+
+  //==============================================================================
+  void timerCallback() override
+  {
+    Point<int> stepSize;
+    
+    stepSize.x = (int)(lastScrollingDistance.x + lastScrollSpeed.x * draggingVelocity.x);
+    stepSize.y = (int)(lastScrollingDistance.y + lastScrollSpeed.y * draggingVelocity.y);
+
+    if (lastScrollingDistance.x == stepSize.x && lastScrollingDistance.y == stepSize.y)
+      return stopTimer();
+
+    Point<int>    moveInPx;
+    moveInPx.x = (startScrollPx.x + (-stepSize.x));
+    moveInPx.y = (startScrollPx.y + (-stepSize.y));
+
+    owner.setViewPosition(moveInPx.x, moveInPx.y);
+    lastScrollingDistance = stepSize;
+    draggingVelocity *= 0.9; //Reduce velocity
+  }
+
+private:
+  friend class Viewport;
+  Viewport& owner;
+
+  Point<double> draggingVelocity;
+  Point<int> lastScrollingDistance, lastScrollSpeed, startScrollPx;
+  int numberFingerDown;
+  bool isDragging;
+};
+
+
+//==============================================================================
 Viewport::Viewport (const String& name)
   : Component (name),
     customScrollBarThickness(false),
@@ -34,7 +127,8 @@ Viewport::Viewport (const String& name)
     allowScrollingWithoutScrollbarV (false),
     allowScrollingWithoutScrollbarH (false),
     verticalScrollBar (true),
-    horizontalScrollBar (false)
+    horizontalScrollBar (false),
+    shouldScrollOnDrag(false)
 {
     // content holder is used to clip the contents so they don't overlap the scrollbars
     addAndMakeVisible (contentHolder);
@@ -50,6 +144,8 @@ Viewport::Viewport (const String& name)
 
     setInterceptsMouseClicks (false, true);
     setWantsKeyboardFocus (true);
+
+    scrollOnDrag = new ScrollOnDragViewPort(*this);
 }
 
 Viewport::~Viewport()
@@ -63,6 +159,12 @@ void Viewport::visibleAreaChanged (const Rectangle<int>&) {}
 void Viewport::viewedComponentChanged (Component*) {}
 
 //==============================================================================
+bool Viewport::isScrollingOnDrag()
+{
+  return scrollOnDrag->isDragging;
+}
+
+//==============================================================================
 void Viewport::deleteContentComp()
 {
     if (contentComp != nullptr)
diff --git a/modules/juce_gui_basics/layout/juce_Viewport.h b/modules/juce_gui_basics/layout/juce_Viewport.h
index 78aa4fa..b19281d 100644
--- a/modules/juce_gui_basics/layout/juce_Viewport.h
+++ b/modules/juce_gui_basics/layout/juce_Viewport.h
@@ -240,6 +240,17 @@ public:
     */
     ScrollBar* getHorizontalScrollBar() noexcept                { return &horizontalScrollBar; }
 
+    /** Enables or not the possibility to scroll in the viewport using mouseDrag in the viewport
+    */
+    void setShouldScrollOnDrag(bool should) { shouldScrollOnDrag = should;}
+    
+    /** True if mouseDrag is scrolling the viewport
+    */
+    bool getShouldScrollOnDrag() const { return shouldScrollOnDrag; }
+
+    /** True if the viewport is currenly beeing scrolled via a mouseDrag, mouse is down and dragging in the viewport.
+    */
+    bool isScrollingOnDrag();
 
     //==============================================================================
     /** @internal */
@@ -278,6 +289,12 @@ private:
     void updateVisibleArea();
     void deleteContentComp();
 
+    bool shouldScrollOnDrag;
+
+    class ScrollOnDragViewPort;
+    friend class ScrollOnDragViewPort;
+    ScopedPointer<ScrollOnDragViewPort> scrollOnDrag;
+
    #if JUCE_CATCH_DEPRECATED_CODE_MISUSE
     // If you get an error here, it's because this method's parameters have changed! See the new definition above..
     virtual int visibleAreaChanged (int, int, int, int) { return 0; }
--
1.9.0.msysgit.0

 

Here is the patch for listbox:

From 1eb2f4ba033fc43362ee6856bafbe7f1cc07c484 Mon Sep 17 00:00:00 2001
From: BastienC <b.commelongue@uvi.net>
Date: Fri, 24 Apr 2015 10:46:44 +0200
Subject: [PATCH] - Listbox uses Viewport with scrollOnDrag

---
 modules/juce_gui_basics/widgets/juce_ListBox.cpp | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/modules/juce_gui_basics/widgets/juce_ListBox.cpp b/modules/juce_gui_basics/widgets/juce_ListBox.cpp
index c90db18..f4c89b8 100644
--- a/modules/juce_gui_basics/widgets/juce_ListBox.cpp
+++ b/modules/juce_gui_basics/widgets/juce_ListBox.cpp
@@ -68,7 +68,7 @@ public:
 
         if (isEnabled())
         {
-            if (owner.selectOnMouseDown && ! selected)
+            if (owner.selectOnMouseDown && !selected && !owner.getViewport()->getShouldScrollOnDrag())
             {
                 owner.selectRowsBasedOnModifierKeys (row, e.mods, false);
 
@@ -84,7 +84,7 @@ public:
 
     void mouseUp (const MouseEvent& e) override
     {
-        if (isEnabled() && selectRowOnMouseUp && ! isDragging)
+      if (isEnabled() && selectRowOnMouseUp && !isDragging && !owner.getViewport()->isScrollingOnDrag())
         {
             owner.selectRowsBasedOnModifierKeys (row, e.mods, true);
 
@@ -102,6 +102,9 @@ public:
 
     void mouseDrag (const MouseEvent& e) override
     {
+      if (owner.getViewport()->getShouldScrollOnDrag())
+        return;
+
         if (ListBoxModel* m = owner.getModel())
         {
             if (isEnabled() && ! (e.mouseWasClicked() || isDragging))
--
1.9.0.msysgit.0

Please let me know what you think,

Thank you,
BastienC

 


#2

Amazing, I was going to try and implement this myself for Android, but decided against it as I know the JUCE team are looking at gestures for the next release. I'd been looking at using Android Java for the UI partly so that I could have smooth scrollable lists for editing. I will try this out asap and see if it works for my needs. 

Thank you for sharing!


#3

I can confirm this works for me. Makes ListBox much more useable for mobile. Thanks again.


#4

@Jules, do you plan to add something like that in Juce by default ?

 

Thanks,


#5

Sounds interesting - we'll take a look!


#6

Is this something on the JUCE roadmap? I'd like to reiterate that the patch works really well for my app, would be nice to have a solution that doesnt involve keeping a fork of JUCE.


#7

Yes, we do have this in our to-do-list, just haven't got around to it just yet!


#8

OK thanks, good to know :)


#9

Hi Jules,

Did you get the chance to implement this natively in juce ?

Thanks,


#10

Actually no, I don't think we ever did.. Should probably have a look at it!


#11

FYI I've added this now. Feedback welcome!

We actually already had an AnimatedPosition class that uses a much better momentum algorithm, so I've used that. There are still issues if you have components inside the viewport that respond to clicks but that'll only be fixable in the future when we add mouse-cancelling callbacks.


#12

This works perfectly! Thanks Jules!


On most cases I use this "ScrollOnDrag" feature with ListBox class. There is a little tweak to do in this class to behave perfectly.
When a mouseDown happens in ListBox there is a conflict with "ScrollOnDrag" as this could mean a possible scrolling or a row selection.

There is 2 little things to add in ListBox::mouseDown and ListBox::mouseUp.

1/ When isScrollOnDragEnabled()==true then the ListBox should not select a row on mouseDown

2/On mouseUp, ListBox should not select a row if isCurrentlyScrollingOnDrag()==true


 juce_ListBox.cpp >>

    void mouseDown (const MouseEvent& e) override
    {
        isDragging = false;
        selectRowOnMouseUp = false;

        if (isEnabled())
        {
            if (owner.selectOnMouseDown && ! selected && !owner.getViewport()->isScrollOnDragEnabled())
            {
                owner.selectRowsBasedOnModifierKeys (row, e.mods, false);

                if (ListBoxModel* m = owner.getModel())
                    m->listBoxItemClicked (row, e);
            }
            else
            {
                selectRowOnMouseUp = true;
            }
        }
    }

    void mouseUp (const MouseEvent& e) override
    {
        if (isEnabled() && selectRowOnMouseUp && ! isDragging && !owner.getViewport()->isCurrentlyScrollingOnDrag())
        {
            owner.selectRowsBasedOnModifierKeys (row, e.mods, true);

            if (ListBoxModel* m = owner.getModel())
                m->listBoxItemClicked (row, e);
        }
    }

Thanks!

 


#13

Hi there. Can anyone confirm that the above changes (or similar) is needed to get a scrolling list with selectable rows on a touchscreen (iOS device)? I tried it with the latest Juce version and it seems to me that only the changes for mouseUp is needed in order to get it to work. Best case would be if it worked with no changes to Juce. Thanks


#14

Is it possible to make the viewport stop scrolling automatically by clicking on it?
It is really good to have it scrolling automatically by dragging fingers/mouse depending on the dragging speed, but also being able to stop it would be awesome!.

Regards

EDITED!:

If anyone minds…I added this at juce_viewport:

 void mouseDown (const MouseEvent&) override
{
    ++numTouches;

	if (offsetX.isTimerOnx()) {
		offsetX.drag(offsetX.getPosition());
		offsetY.drag(offsetY.getPosition());
	}
}

and this to: juce_animatedPosition

int isTimerOnx(void) {
	return isTimerRunning();
}

It seems to work properly, so it stops scrolling automatically when clicking on it.

Regards