Clients/Client2018/content/internal/AppShell/Modules/Shell/Components/Common/WindowedScrollingFrame.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