253 lines
10 KiB
Lua
253 lines
10 KiB
Lua
--[[
|
|
Creates a Roact component for inifinite scrolling
|
|
Props:
|
|
scrollingFrameProps : dictionary - props for the scrolling frame
|
|
.Selectable : bool - Whether or not this object should be selectable using joysticks (controller).
|
|
.ClipsDescendants : bool - Determines whether Roblox will render any portions of its GUI descendants that are outside of its own borders.
|
|
items : Array - An array of the input item data which is used to construct item component.
|
|
itemSize : Vector2 - The size for each item in the scrolling frame.
|
|
itemsPaddingOffset : int - The padding between each item.
|
|
scrollingDirection : Enum.ScrollingDirection - The scrolling direction of the scrolling frame, can't be Enum.ScrollingDirection.XY.
|
|
itemOffsetStart: int - The minimum distance of the selected guiobject to the window start border.
|
|
itemOffsetEnd: int - The minimum distance of the selected guiobject to the window end border.
|
|
customScrollDist: dictionary - Custom distances to trigger the scroll.
|
|
generateKey : function() - Used to generate a name for the item.
|
|
renderItem : function() -
|
|
Input: item data, item index and an onSelectionGained callback
|
|
Output: a Roact Component
|
|
State:
|
|
viewStart: int - The item start index.
|
|
viewSize: int - The number of items can be put in the window.
|
|
paddingStart: int - The padding to apply on the top / left side of the scrolling frame.
|
|
]]
|
|
|
|
local GuiService = game:GetService("GuiService")
|
|
local Modules = game:GetService("CoreGui").RobloxGui.Modules
|
|
|
|
local Utility = require(Modules.Shell.Utility)
|
|
local Roact = require(Modules.Common.Roact)
|
|
local WindowedScrollingFrame = Roact.PureComponent:extend("WindowedScrollingFrame")
|
|
|
|
|
|
local function clipCanvasPosition(scrollingFrame, canvasPos)
|
|
local canvasPosX = canvasPos.X
|
|
local canvasPosY = canvasPos.Y
|
|
canvasPosX = math.min(canvasPosX, scrollingFrame.CanvasSize.X.Offset - scrollingFrame.AbsoluteWindowSize.X)
|
|
canvasPosX = math.max(0, canvasPosX)
|
|
canvasPosY = math.min(canvasPosY, scrollingFrame.CanvasSize.Y.Offset - scrollingFrame.AbsoluteWindowSize.Y)
|
|
canvasPosY = math.max(0, canvasPosY)
|
|
return Vector2.new(canvasPosX, canvasPosY)
|
|
end
|
|
|
|
function WindowedScrollingFrame:init()
|
|
self.state = {
|
|
viewStart = 0,
|
|
viewSize = 0,
|
|
paddingStart = 0,
|
|
}
|
|
|
|
self.scrollingFrameRef = function(rbx)
|
|
self.scrollingFrame = rbx
|
|
end
|
|
end
|
|
|
|
function WindowedScrollingFrame:onSelectionChanged(selectedItem)
|
|
if not self.scrollingFrame then
|
|
return
|
|
end
|
|
if selectedItem == nil or selectedItem == self.savedSelectedObject or not selectedItem:IsDescendantOf(self.scrollingFrame) then
|
|
return
|
|
end
|
|
self.savedSelectedObject = selectedItem
|
|
|
|
local scrollingFrame = self.scrollingFrame
|
|
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
|
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
|
|
local canvasPosition = scrollingFrame.CanvasPosition
|
|
|
|
local axisKey = "X"
|
|
if scrollingDirection == Enum.ScrollingDirection.Y then
|
|
axisKey = "Y"
|
|
end
|
|
|
|
-- If our scrolling frame has zero height / width, let's not bother trying to
|
|
-- recompute our sizing
|
|
if absoluteWindowSize[axisKey] == 0 then
|
|
return
|
|
end
|
|
|
|
local itemOffsetStart = self.props.itemOffsetStart or 0
|
|
local itemOffsetEnd = self.props.itemOffsetEnd or 0
|
|
local customScrollDist = self.props.customScrollDist or {}
|
|
--If the selected guiobject is off-window, we move it back into the window instantly
|
|
--Then make the motion
|
|
local instantPos;
|
|
local tweenTargetPos;
|
|
if scrollingDirection == Enum.ScrollingDirection.Y then
|
|
local topDistance = selectedItem.AbsolutePosition.Y - scrollingFrame.AbsolutePosition.Y
|
|
local bottomDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).Y
|
|
|
|
local minDistTop = itemOffsetStart
|
|
local minDistBottom = itemOffsetEnd
|
|
|
|
if topDistance < (customScrollDist.Top or minDistTop) then
|
|
if topDistance < 0 then
|
|
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y + topDistance)
|
|
end
|
|
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y - (minDistTop - topDistance))
|
|
elseif bottomDistance < (customScrollDist.Bottom or minDistBottom) then
|
|
if bottomDistance < 0 then
|
|
instantPos = Vector2.new(canvasPosition.X, canvasPosition.Y - bottomDistance)
|
|
end
|
|
tweenTargetPos = Vector2.new(canvasPosition.X, canvasPosition.Y + minDistBottom - bottomDistance)
|
|
end
|
|
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
|
local leftDistance = selectedItem.AbsolutePosition.X - scrollingFrame.AbsolutePosition.X
|
|
local rightDistance = (scrollingFrame.AbsolutePosition + scrollingFrame.AbsoluteWindowSize - selectedItem.AbsolutePosition - selectedItem.AbsoluteSize).X
|
|
|
|
local minDistLeft = itemOffsetStart
|
|
local minDistRight = itemOffsetEnd
|
|
|
|
if leftDistance < (customScrollDist.Left or minDistLeft) then
|
|
if leftDistance < 0 then
|
|
instantPos = Vector2.new(canvasPosition.X + leftDistance, canvasPosition.Y)
|
|
end
|
|
tweenTargetPos = Vector2.new(canvasPosition.X - (minDistLeft - leftDistance), canvasPosition.Y)
|
|
elseif rightDistance < (customScrollDist.Right or minDistRight) then
|
|
if rightDistance < 0 then
|
|
instantPos = Vector2.new(canvasPosition.X - rightDistance, canvasPosition.Y)
|
|
end
|
|
tweenTargetPos = Vector2.new(canvasPosition.X + minDistRight - rightDistance, canvasPosition.Y)
|
|
end
|
|
end
|
|
if instantPos then
|
|
instantPos = clipCanvasPosition(scrollingFrame, instantPos)
|
|
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, instantPos, 0, Utility.EaseOutQuad, true, function()
|
|
if tweenTargetPos then
|
|
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
|
|
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", instantPos, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
|
|
end
|
|
end)
|
|
end
|
|
if not instantPos and tweenTargetPos then
|
|
tweenTargetPos = clipCanvasPosition(scrollingFrame, tweenTargetPos)
|
|
Utility.PropertyTweener(scrollingFrame, "CanvasPosition", canvasPosition, tweenTargetPos, 0.2, Utility.EaseOutQuad, true)
|
|
end
|
|
end
|
|
|
|
function WindowedScrollingFrame:updateViewBounds()
|
|
if not self.scrollingFrame then
|
|
return
|
|
end
|
|
|
|
local scrollingFrame = self.scrollingFrame
|
|
local itemSize = self.props.itemSize
|
|
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
|
|
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
|
local absoluteWindowSize = scrollingFrame.AbsoluteWindowSize
|
|
local canvasPosition = scrollingFrame.CanvasPosition
|
|
|
|
local axisKey = "X"
|
|
if scrollingDirection == Enum.ScrollingDirection.Y then
|
|
axisKey = "Y"
|
|
end
|
|
|
|
-- If our scrolling frame has zero height / width, let's not bother trying to
|
|
-- recompute our sizing
|
|
if absoluteWindowSize[axisKey] == 0 then
|
|
return
|
|
end
|
|
|
|
canvasPosition = clipCanvasPosition(scrollingFrame, canvasPosition)
|
|
local itemTotalSize = (itemSize[axisKey] + itemsPaddingOffset)
|
|
local viewSize = math.ceil(absoluteWindowSize[axisKey] / itemTotalSize) + 1
|
|
local viewStart = math.floor(canvasPosition[axisKey] / itemTotalSize)
|
|
local paddingStart = math.max(0, (viewStart - 1) * itemTotalSize)
|
|
local shouldUpdate = viewSize ~= self.state.viewSize or viewStart ~= self.state.viewStart or paddingStart ~= self.state.paddingStart
|
|
if shouldUpdate then
|
|
self:setState({
|
|
viewStart = viewStart,
|
|
viewSize = viewSize,
|
|
paddingStart = paddingStart,
|
|
})
|
|
end
|
|
end
|
|
|
|
function WindowedScrollingFrame:render()
|
|
local items = self.props.items
|
|
local generateKey = self.props.generateKey
|
|
local renderItem = self.props.renderItem
|
|
local itemSize = self.props.itemSize
|
|
local itemsPaddingOffset = self.props.itemsPaddingOffset or 0
|
|
local scrollingDirection = self.props.scrollingDirection or Enum.ScrollingDirection.Y
|
|
assert(scrollingDirection ~= Enum.ScrollingDirection.XY, "Can't set ScrollingDirection as XY.")
|
|
|
|
local children = {}
|
|
|
|
children.UIListLayout = Roact.createElement("UIListLayout", {
|
|
Padding = UDim.new(0, itemsPaddingOffset),
|
|
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
FillDirection = scrollingDirection == Enum.ScrollingDirection.Y and Enum.FillDirection.Vertical or Enum.FillDirection.Horizontal
|
|
})
|
|
|
|
if scrollingDirection == Enum.ScrollingDirection.Y then
|
|
children.UIPadding = Roact.createElement("UIPadding", {
|
|
PaddingTop = UDim.new(0, self.state.paddingStart)
|
|
})
|
|
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
|
children.UIPadding = Roact.createElement("UIPadding", {
|
|
PaddingLeft = UDim.new(0, self.state.paddingStart)
|
|
})
|
|
end
|
|
|
|
local lowerBound = math.max(1, self.state.viewStart)
|
|
local upperBound = math.min(#items, self.state.viewStart + self.state.viewSize)
|
|
|
|
for i = lowerBound, upperBound do
|
|
local key = generateKey and generateKey(i) or i
|
|
children[key] = renderItem(items[i], i)
|
|
end
|
|
|
|
local scrollingFrameProps = self.props.scrollingFrameProps or {}
|
|
local canvasSize = nil
|
|
if scrollingDirection == Enum.ScrollingDirection.Y then
|
|
canvasSize = UDim2.new(1, 0, 0, #items * itemSize.Y + (#items - 1) * itemsPaddingOffset)
|
|
elseif scrollingDirection == Enum.ScrollingDirection.X then
|
|
canvasSize = UDim2.new(0, #items * itemSize.X + (#items - 1) * itemsPaddingOffset, 1, 0)
|
|
end
|
|
|
|
return Roact.createElement("ScrollingFrame", {
|
|
Size = UDim2.new(1, 0, 1, 0),
|
|
ScrollingEnabled = false, --Don't let the default select logic affect canvas position
|
|
CanvasSize = canvasSize,
|
|
Selectable = scrollingFrameProps.selectable or false,
|
|
ScrollBarThickness = 0,
|
|
ClipsDescendants = scrollingFrameProps.clipsDescendants,
|
|
BackgroundTransparency = 1,
|
|
ScrollingDirection = scrollingDirection,
|
|
[Roact.Ref] = self.scrollingFrameRef,
|
|
[Roact.Change.CanvasPosition] = function() self:updateViewBounds() end,
|
|
[Roact.Change.AbsoluteSize] = function() self:updateViewBounds() end,
|
|
}, children)
|
|
end
|
|
|
|
function WindowedScrollingFrame:didMount()
|
|
self:updateViewBounds()
|
|
end
|
|
|
|
function WindowedScrollingFrame:didUpdate(prevProps, prevState)
|
|
if not prevProps.inFocus and self.props.inFocus then
|
|
self.conn = GuiService:GetPropertyChangedSignal("SelectedCoreObject"):connect(function()
|
|
self:onSelectionChanged(GuiService.SelectedCoreObject)
|
|
end)
|
|
elseif prevProps.inFocus and not self.props.inFocus then
|
|
Utility.DisconnectEvent(self.conn)
|
|
end
|
|
|
|
if self.props ~= prevProps then
|
|
self:updateViewBounds()
|
|
end
|
|
end
|
|
|
|
return WindowedScrollingFrame
|