It can sometimes be a pain to make your UI’s tween nicely and without bugs. I have always found it kind of tedious to do. While working on one of my projects I made this module to handle all my frame tweening, and I decided to share it.
ContextFrame will handle all your menu tweening needs and prevent any pesky tweening related issues. It works by allowing you to create different context groups for frames. Within each context only one frame is allowed to be open.
When you add a frame to a context it will give you a few frame tweening methods to play with.
Source code:
--[[
ContextFrame: A module to handle all your menu tweening needs!
Prevents menu overlap and general tweening related issues.
--------------------------------------------------------------
ContextFrame Methods:
---------------------
ContextFrame.NewContext(contextName) -> ContextObj | Creates and returns a new context object.
ContextFrame:GetContext(contextName) -> ContextObj | Retrieves an existing context from it's name.
ContextFrame:GetContextFromInstance(instance) -> ContextObj | Retrieves an existing context from an instance.
ContextObj Methods:
-------------------
ContextObj:AddToContext(instance, openPosition, startPos) -> Frame | Adds instance to the context.
openPosition is a UDim2 value which will be the frame's position when it is opened. start pos can be "open" or "closed", it is closed by default.
ContextObj:RemoveFromContext(instance) | Removes instance from the context.
ContextObj:CloseAll(ignoreDefault) -> boolean | Closes all frames in ContextObj except those set as default. (set ignoreDefault to true to override).
ContextObj:AddDefault(instance) -> boolean | Adds instance to the list of default frames (these frames will remain open when CloseAll() is called).
ContextObj:RemoveDefault(instance) -> boolean | Removes instance from the list of default frames.
ContextObj:GetOpenFrame() -> Frame | Returns the frame that is currently open in the ContextObj.
Frame Methods:
--------------
Frame:SetStyle(length, easingStyle) | Adjusts the tween animation. length is the length of the tween. easing style is an enum.
Frame:Open() | Tweens the frame to it's open position, any frame within it's context will be closed prior.
Frame:Close() | Tweens the frame to it's closed position.
Frame:Handle() | Tweens the frame to whatever position it isn't in. i.e if the frame is closed it will open it and vice versa.
Frame:Lock() | Locks the frame, when this frame is opened no other frame may open until it is closed.
Frame:Unlock() | Unlocks the frame.
]]
local tweenService = game:GetService("TweenService")
local UIHandler = {}
local contextFunc = {}
UIHandler.Context = {}
UIHandler.__index = UIHandler
contextFunc.__index = contextFunc
local framePositions = {
"closed",
"open",
"opening",
"closing",
}
function UIHandler.NewContext(name)
if not UIHandler.Context[name] then
local newEnv = {}
newEnv.Frames = {}
newEnv.DefaultOpen = {}
setmetatable(newEnv, UIHandler)
UIHandler.Context[name] = newEnv
setmetatable(newEnv, UIHandler)
return newEnv
else
warn("Context name is not original")
return
end
end
function UIHandler:GetContext(name)
return UIHandler.Context[name]
end
function UIHandler:GetContextFromInstance(frame)
for contextName, contextObj in pairs(UIHandler.Context) do
if contextObj.Frames[frame] then
return contextObj
end
end
return
end
function UIHandler:GetContextFrameFromInstance(frame)
for contextName, contextObj in pairs(UIHandler.Context) do
local contextFrame = contextObj.Frames[frame]
if contextFrame then
return contextFrame
end
end
return
end
function UIHandler:AddDefault(frame)
local foundFrame = self.Frames[frame]
if foundFrame then
table.insert(self.DefaultOpen, foundFrame)
return true
end
warn("Attempt to add frame out of context")
return
end
function UIHandler:RemoveDefault(frame)
local foundFrame = self.Frames[frame]
if foundFrame then
local foundDefault = table.find(self.DefaultOpen, foundFrame)
if foundDefault then
table.remove(self.DefaultOpen, foundDefault)
else
return false
end
else
warn("Frame is not in context")
return
end
end
function UIHandler:CloseAll(ignoreDefault)
for index, object in pairs(self.Frames) do
if not table.find(self.DefaultOpen, object) or ignoreDefault then
print("Closing", object.Object)
object:Close()
elseif not ignoreDefault and (self.FrameStatus ~= "open" or self.FrameStatus ~= "opening") then
print("Open", object.Object)
object:Open()
end
end
end
function UIHandler:AddToContext(frame, openPos, startPos)
if not self.Frames[frame] then
local proxy = {}
if startPos and not table.find(framePositions, startPos) then
warn("invalid start position, default has been set")
startPos = "closed"
end
--Data set--
proxy.FrameStatus = startPos or "closed"
proxy.Context = self
proxy.Object = frame
proxy.OpenPos = openPos or UDim2.new(0.5, 0, 0.5, 0)
proxy.ClosePos = proxy.OpenPos + UDim2.new(0, 0, 1, 0)
proxy.Locked = false
proxy.TweenInInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.In)
proxy.TweenOutInfo = TweenInfo.new(0.25, Enum.EasingStyle.Quad, Enum.EasingDirection.Out)
proxy.CurrentTween = nil
self.Frames[frame] = proxy
setmetatable(proxy, contextFunc)
return proxy
else
warn("Attempted to add duplicate frame to context")
return self.Frames[frame]
end
end
function UIHandler:RemoveFromContext(frame)
if self.Frames[frame] then
self:RemoveDefault(frame)
self.Frames[frame] = nil
end
end
function UIHandler:GetOpenFrame()
for _, object in pairs(self.Frames) do
if object.FrameStatus == "open" or object.FrameStatus == "opening" then
return object
end
end
return nil
end
---------------------------
--Context frame functions--
---------------------------
function contextFunc:SetStyle(length, easingStyle)
self.tweenInInfo = TweenInfo.new(length, (easingStyle or Enum.EasingStyle.Quad), Enum.EasingDirection.In)
self.tweenOutInfo = TweenInfo.new(length, (easingStyle or Enum.EasingStyle.Quad), Enum.EasingDirection.Out)
end
function contextFunc:Lock()
self.Locked = true
end
function contextFunc:Unlock()
self.Locked = false
end
function contextFunc:Close()
if self.FrameStatus == "opening" then
self.CurrentTween:Cancel()
elseif self.FrameStatus ~= "closed" or self.FrameStatus ~= "closing" then
local tween = tweenService:Create(self.Object, self.TweenInInfo, {Position = self.ClosePos})
self.CurrentTween = tween
self.FrameStatus = "closing"
tween:Play()
spawn(function()
tween.Completed:Wait()
tween:Destroy()
self.FrameStatus = "closed"
end)
end
end
function contextFunc:Open()
local openFrame = self.Context:GetOpenFrame()
if openFrame ~= self then
if openFrame then
if openFrame.Locked then
return
else
openFrame:Close()
end
end
local tween = tweenService:Create(self.Object, self.TweenOutInfo, {Position = self.OpenPos})
self.CurrentTween = tween
self.FrameStatus = "opening"
tween:Play()
spawn(function()
tween.Completed:Wait()
tween:Destroy()
self.FrameStatus = "open"
end)
end
end
function contextFunc:Handle()
if self.Context:GetOpenFrame() == self then
self:Close()
else
self:Open()
end
end
return UIHandler