SmoothScroll Module [Superseded by Roblox Update 421]

This module is no longer needed.

In Update 421, Roblox added smooth scrolling to ScrollingFrames.

We had a good long run here, and I know this module helped a lot of people while it was around. I hope you all benefited from it, and I’m glad to see Roblox improving the platform so modules like these are unnecessary!

I’m certain that one day, Roblox will make this behavior the default. Until that day, this is what I’ll be using!

^ Just saying, I called it.

I’m leaving the post below for documentation + posterity. Besides, we might find some use for it again in the future, who knows.






I’ll start with this note:
It is tied to framerate, so it’ll scroll smoothly regardless of FPS. I view this as a feature, but it’s worth noting that slower devices will scroll slower.


The issue:

ScrollingFrames on PC are painful to use. Scrolling on them jumps the CanvasPosition around, and they have no inertia at all.
If there’s text in a ScrollingFrame, it’s hard to read because when you scroll it jumps and you lose your place. My game (Lua Learning) is a lot of reading, so being able to scroll without losing track is a big deal. This module is designed for my case, but I’m sure many of you could use it as well.

(These GIFs are 25FPS, but the smooth version is juicy in game at 60FPS.)

Default:

DefaultScroll

SmoothScroll:

SmoothScroll


The Solution:

I’ve seen a couple weird ways people have done it, none of them easy to just plug into your game.
One way was made out of multiple frames and required you to completely change your code, others are hard coded to a single frame, etc.

So, I wrote another open-source module!
Implementation and usage is incredibly easy. You use regular ScrollingFrames when creating your GUIs, and just tell the module to make it smooth. It does the rest!


The API:

(I used Rodocs to document the module, and I highly recommend it.)

function SmoothScroll.Enable(Frame, Sensitivity, Friction, Inverted, Axis)

Sets a ScrollingFrame to scroll smoothly

Parameters:

  • Frame [ScrollingFrame]
    The ScrollingFrame object to apply smoothing to

  • Sensitivity [Optional Number]
    How many pixels it scrolls per wheel turn

  • Friction [Optional Number]
    What the velocity is multiplied by each frame (Clamped between 0.2 and 0.99)

  • Inverted [Optional Bool]
    Inverts the scrolling direction

  • Axis [Optional String]
    “X” or “Y”. If left out, will default to whichever Axis is scrollable or “Y” if both are valid

Returns:

  • nil
function SmoothScroll.Disable(Frame)

Sets a ScrollingFrame to scroll normally

Parameters:

  • Frame [ScrollingFrame]
    The ScrollingFrame object to remove smoothing from

Returns:

  • nil

Example usage:

It’s super simple to use. It’s also coded defensively, so even if you mess up, it’ll either use default or just ‘warn’ and not smooth it. This means the module should never halt or break your code. If you find an error case that does, let me know!

Simple use example
local SmoothScroll	= require(script.SmoothScroll)

local ScreenGui		= game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui")

SmoothScroll.Enable(ScreenGui.ScrollingFrame)
This example automatically smooths any ScrollingFrame in your game except ones that you tag with "DontSmooth" via ColectionService. This code is currently running in Lua Learning! It's super useful.
local CollectionService = game:GetService("CollectionService")

local Player	= game.Players.LocalPlayer
local PlayerGui	= Player:WaitForChild("PlayerGui")


local SmoothScroll	= require(script.SmoothScroll)

local Smoothed = {}

local function DescendantAdded(Descendant)
	if Descendant:IsA("ScrollingFrame") and not Smoothed[Descendant] and not CollectionService:HasTag(Descendant, "DontSmooth") then
		SmoothScroll.Enable(Descendant)
		Smoothed[Descendant] = true
	end
end

local function DescendantRemoving(Descendant)
	if Descendant:IsA("ScrollingFrame") and Smoothed[Descendant] then
		SmoothScroll.Disable(Descendant)
		Smoothed[Descendant] = nil
	end
end

-- Initialize

for _, Descendant in pairs(PlayerGui:GetDescendants()) do
	DescendantAdded(Descendant)
end

PlayerGui.DescendantAdded:Connect(DescendantAdded)
PlayerGui.DescendantRemoving:Connect(DescendantRemoving)

Module:

Repository:


Conclusion:

I hope you guys find this as useful as I do! I’m certain that one day, Roblox will make this behavior the default. Until that day, this is what I’ll be using! I hope they also let us disable/alter the sensitivity and friction of the mobile ScrollingFrames.




Enjoying my work? I love to create and share with the community, for free.

If you’d like to help fund my work, consider sponsoring me on GitHub or donating on BuyMeACoffee!

158 Likes

I’ve never been a huge fan of the instantaneous repositioning of ScrollingFrames, but I feel it’s gotten the job done well enough that I’ve never paid it any actual attention. It feels natural to have instantaneous scrolling on Roblox. I probably won’t use SmoothScroll, but I’ll definitely have a look through it.

Your contribution is thanked.


One comment to offer in regards to the thread’s opener, the limitations of the module. I hope that these limitations can be resolved, if possible, at some point. That’d make this module much more powerful. There is one limitation that can be resolved though, which is where I step in hopefully to the rescue.

The zooming function of the MouseWheel is done via the ContextActionService. It is bound at Enum.ContextActionPriority.High, which has a value of 3000. This means that it can be authoritative of binds at lower priorities, such as your function.

If you want to prioritise the scroll over the mouse action, you will need to bind your scrolling function the same way with a higher value (e.g. Enum.ContextActionPriority.High.Value + 1000) and then return a ContextActionResult to interfere with the mouse wheel.

While the mouse is scrolling through a ScrollingFrame, return Enum.ContextActionResult.Sink. This will make your bound scroll-wheel function run and then prevent lower-priority actions from being ran. If the user is not interacting with a ScrollingFrame, do nothing and return Enum.ContextActionResult.Pass.

Returning sink will run your bound function and then make that a cut-off point, meaning lower-bound actions will not be run. As for returning pass, it essentially tells the ContextActionService to run your bound function and then pass the torch to lower priority bindings.

One caveat is that you might hamper other MouseWheel functions, but maybe that is what is necessary. I wouldn’t want any mouse wheel inputs ran while I’m trying to scroll.

Relevant article: ContextActionResult


That’s all. Great work.

14 Likes

That’s super helpful! Thank you so much for sharing this.
I’d been using UserInputService, but I will make the switch and update the post when done.

EDIT: Made the change, works beautifully, and edited to OP to remove that limitation.

5 Likes

Thank you for making this module, I really like how the smooth scrolling looks.

Just one question. Is it possible to make it so that you can scroll through a frame when the mouse is over a descendant of the scrolling frame?

Here is what I mean:
https://gyazo.com/9111f699e6da6a1e25093845abebb27f.gif

^ Currently, when my mouse is hovering on the scrolling frame it scrolls fine, but when the mouse is over an object inside the frame, it doesn’t let me scroll.

Thanks :slight_smile:

5 Likes

Does it work when over a TextLabel or something that has no interactions?

I think the issue is that the items in it are eating the input event and not letting my module fire.

If you’re willing, could you DM me the file of the frame and any relevant code?

1 Like

After some more testing, I’ve found that the ContextActionService binding doesn’t fire when your mouse is hovering over something that changes the mouse icon.
(It’s not about the icon, but that’s the indicator)

As you can see here, when the mouse is over the objects, it goes black. Once it’s black, the binding no longer fires.

I don’t know what to do about this. Why doesn’t the binding fire?

2 Likes

You can get it to work even if the mouse is hovering on a button if you use UserInputService.InputChanged instead of sticking to ContextActionService. Just ignore gameProcessedEvent.

	game:GetService("UserInputService").InputChanged:Connect(function(Input, gameProcessedEvent)
		if Input.UserInputType == Enum.UserInputType.MouseWheel then
			-- true if the mouse is on a button, false otherwise.
			-- probably why ContextActionService won't trigger since the game sinks it internally
			-- before you ever get a chance to process it.
			print(gameProcessedEvent)

			for Frame, Info in pairs(Objects) do
				if Info.Hovering and Frame.Visible == true then
					Info.Velocity = Info.Velocity - (Info.Sens * Input.Position.Z * (Info.Inverted and -1 or 1))
				end
			end
		end
	end)

Frames also have an InputChanged event of their own.

3 Likes

Hey, that’s pretty good! Pretty neat feature, good job on that!

1 Like

I’ve had to disable this in my game for now.

I got bug reports from a few people that they couldn’t scroll because they don’t have a mouse.

I forgot laptop touchpad people exist!

Is there a way to detect touchpads? I can’t find anything in UserInputServicethat specific to it.

1 Like

I used to do this, but the issue was that the camera would zoom during your scroll because the event wasn’t sunk. @colbert2677 solved this by introducing the ContextActionService method.

But now we’re back to square one!

1 Like

Uh oh. Did that ContextActionService solution bring trouble to those using the dreaded touch pad over a mouse? (I disable my touch pad every time after my computer starts up.)

I believe that the touch pad fires off the same events as the mouse, given there is no specific input type for it. That being said, I am certain that ScrollingFrames have scroll inertia when interacted with a touch pad, much like mobile.

You may want to experiment around by printing the UserInputType after interacting with the Gui, then using a touch pad to see what comes out. I don’t have access so I wouldn’t know. Scrolling is done by holding two fingers on the pad and moving up or down.

2 Likes

I don’t have a touch pad to test with :disappointed_relieved:
If I did, I’d definitely just use it and see what fires.

Can anyone with a touch pad help out?

1 Like

I got you. My test confirmed what I already knew but now that I have a computer to access, I can explain the scoop to you.

Repro code:

local UserInputService = game:GetService("UserInputService")

UserInputService.InputBegan:Connect(function (InputObject)
    print("InputState:", InputObject.UserInputState, "\nInputType:", InputObject.UserInputType)
end)

Results are exactly as I expected.

  • Left side is equivalent to MouseButton1
  • Right side is equivalent to MouseButton2
  • There is no wheel or MouseButton3 equivalent

There is no way to detect input from a touch pad because it’s connected to the same logic that makes a mouse work. Your touch pad is the actual click input device and a mouse (notice that mouses are plugged in via USB cord) is used to make click input easier to work with. All your buttons can be used on one device in one hand comfortably.

Those who do not have a mouse and are using a touch pad will not be able to scroll through anything using this module, so attempting to find a solution for touch pad users is moot. As it is, forget the module. Those using touch pads can’t interact with ScrollingFrames without using the bar. What I said earlier about touch pads having scroll inertia is false.

The only workaround is to get that scroll bar working or find a solution that relies solely on the movement and positioning of the mouse.

2 Likes

Thank you for the complete answer!

I’ll work on this more in the future.

2 Likes

It’s just so smooth, it is way way way better than the current Scrolling system that roblox uses! Thanks a lot for sharing this for all of us to use!

2 Likes

This module is absolutely amazing! After using this module, I don’t think I can ever go back to standard scrolling.

2 Likes

Update!

Implemented dragging the scrolling bar!

How it was done:
Places a hidden button over the bar (supports X, Y, .ScrollbarThickness and .VerticalScrollBarPosition so all cases should work) and getting the MouseButton1Down event to initiate dragging.

This update should allow touchpad users to still use ScrollingFrames. (Since I can’t actually test that, could someone confirm?)

EDIT:

Fixed the overlapping bar issue by re-purposing the visibilty tracker from @Crazyman32’s incredible MouseOver module (August 18, 2018) to make bar buttons inactive when the frame isn’t visible.

2 Likes

When do you think it will be fixed? I just so happen to want to use the module when this happens.

10 more minutes, tops. Almost done!

Edit: It was done 3 minutes after I said this.

1 Like

There’s an issue I’m having with the scroll bar. When I select a scroll bar and then deselect, it still forces me to scroll. If you select another scroll bar, and the same thing happens. Scrolling with the scroll wheel works fine, however.
https://i.gyazo.com/85a1fe8380a52b52cc8d3a4aa5e453a5.gif