1
0
KF2-Dev-Scripts/GameFramework/Classes/MobileMenuList.uc

787 lines
20 KiB
Ucode
Raw Permalink Normal View History

2020-12-13 15:01:13 +00:00
/**
* MobileMenuList
* A container of objects that can be scrolled through.
*
* Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
*/
class MobileMenuList extends MobileMenuObject;
struct SelectedMenuItem
{
/** Currently 'Selected' based off of position */
var int Index;
/** The selected item may be not be right on the 'selected' location, this is its offset */
var float Offset;
/** Was the selected limited how far it dragged because it was end of the list **/
var bool bEndOfList;
};
struct DragHistoryData
{
var float TouchTime;
var float TouchCoord;
};
const NumInDragHistory=4;
struct MenuListDragInfo
{
/** Are we currently dragging? If not, still may be used if ScrollSpeed != 0*/
var bool bIsDragging;
/** Item that was initially pressed. If !bIsDragging, then this item will use all input */
var MobileMenuListItem TouchedItem;
/** Saved off to recalculate position */
var SelectedMenuItem OrigSelectedItem;
/** Where did user start to drag at? */
var Vector2D StartTouch;
/** Amount of time for the touch */
var float TouchTime;
/** How far from orig position we are at */
var float ScrollAmount;
/** Tracks if user moved up, then down - ScrollAmount might be 0, but AbsScrollAmount migh have a value*/
var float AbsScrollAmount;
/** To smooth out the release velocity */
var DragHistoryData UpdateHistory[NumInDragHistory];
/** Number of Update calls (not press or release) to index into History */
var int NumUpdates;
/** See if the selected one has changed, if not, treat it as a touch when release */
var bool bHasSelectedChanged;
};
struct MenuListMovementInfo
{
/* Are we automatically moving the list? */
var bool bIsMoving;
/** Saved off to recalculate position */
var SelectedMenuItem OrigSelectedItem;
/** How many pixels total to scroll */
var float FullMovement;
/** Total time until it is done scrolling */
var float TotalTime;
/** How much time we are at */
var float CurrentTime;
};
/** Vertical or horizontal list supported */
var(DefaultInit) bool bIsVerticalList;
/** On short list, might want to disable all scrolling */
var(DefaultInit) bool bDisableScrolling;
/** Offset from Top/Left of list that determines 'selected' item - init as percentage of Width/Height depending on bIsVerticalList */
var(DefaultInit) float SelectedOffset;
/** When user stops moving, should the closest item move to the selected position */
var(DefaultInit) bool bForceSelectedToLineup;
var array<MobileMenuListItem> Items;
/** Current position of our list */
var SelectedMenuItem SelectedItem;
/** User changing position of list */
var MenuListDragInfo Drag;
/** Automatic movement of list (after user drag) */
var MenuListMovementInfo Movement;
/** How fast to Deaccelerate when user releases - cannot be 0*/
var float Deacceleration;
/** How to we slow down while deaccelerate */
var float EaseOutExp;
/** Sometimes rendering item needs this */
var IntPoint ScreenSize;
/** When user taps on an options, should we scroll to it? Typically, when there is no obvious 'selected', you don't want to */
var bool bTapToScrollToItem;
/** Behaves like a slot machine wheel, continually loops. */
var bool bLoops;
/** Index of first and last visible according to what just rendered. */
var int FirstVisible, LastVisible;
/** To not allow scrolling past end of lists */
var int NumShowEndOfList;
/** How much to decrease scroll when at the end of a list. 1.0 is none, 0.5 is half, */
var float EndOfListSupression;
/**
* InitMenuObject - Virtual override from base to init object.
*
* @param PlayerInput - A pointer to the MobilePlayerInput object that owns the UI system
* @param Scene - The scene this object is in
* @param ScreenWidth - The Width of the Screen
* @param ScreenHeight - The Height of the Screen
*/
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight, bool bIsFirstInitialization)
{
ScreenSize.X = ScreenWidth;
ScreenSize.Y = ScreenHeight;
Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight, bIsFirstInitialization);
SelectedOffset *= (bIsVerticalList) ? Height : Width;
}
function AddItem(MobileMenuListItem Item, Int Index=-1)
{
if (Index < 0)
{
Index = Items.length + (Index + 1);
}
Items.InsertItem(Index, Item);
}
function int Num()
{
return Items.length;
}
function MobileMenuListItem GetSelected()
{
local MobileMenuListItem Item;
if ((SelectedItem.Index >= 0) && (SelectedItem.Index < Items.length))
{
Item = Items[SelectedItem.Index];
if (Item != none && !Item.bIsVisible)
Item = none;
return Item;
}
return none;
}
/*
* If item is selected, 0 > RetValue >= 1.0.
* Useful for changing alpha or size of selected item.
*/
function float GetAmountSelected(MobileMenuListItem Item)
{
local MobileMenuListItem Selected;
local float Half;
Selected = GetSelected();
if (Item == Selected)
{
Half = (bIsVerticalList ? Item.Height : Item.Width) * 0.5f;
return FMax(0.0001f, FMin(1.0f, 1.0f - (Abs(SelectedItem.Offset) / Half)));
}
return 0.0f;
}
/**
* Find the visible index of selected item. In other words, number of
* visible items before selected item.
*/
function int GetVisibleIndexOfSelected()
{
local MobileMenuListItem Item, Selected;
local int Index;
Selected = GetSelected();
Index = 0;
foreach Items(Item)
{
if (Item == Selected)
{
return Index;
}
if (Item.bIsVisible)
{
Index++;
}
}
return -1;
}
/**
* Set the selected item to the visible item with VisibleIndex visible items before it
*/
function int SetSelectedToVisibleIndex(int VisibleIndex)
{
local int Index;
for (Index = 0; Index < Items.Length; Index++)
{
if (Items[Index].bIsVisible)
{
if (VisibleIndex <= 0)
{
SelectedItem.Index = Index;
return Index;
}
VisibleIndex--;
}
}
SelectedItem.Index = -1;
return -1;
}
function int GetNumVisible()
{
local int Index, Count;
for (Index = 0; Index < Items.Length; Index++)
{
if (Items[Index].bIsVisible)
{
Count++;
}
}
return Count;
}
// A little hack, forcing all. Perhaps it should always be set to true,
// but this is nearly last bug before we ship.
function bool SetSelectedItem(int ItemIndex, bool bForceAll=false)
{
if ((ItemIndex >= 0) && (ItemIndex < Items.Length))
{
if (Items[ItemIndex].bIsVisible)
{
SelectedItem.Index = ItemIndex;
if (bForceAll)
{
Drag.OrigSelectedItem = SelectedItem;
Movement.OrigSelectedItem = SelectedItem;
}
return true;
}
}
return false;
}
/**
* This event is called when a "touch" event is detected on the object.
* If false is returned (unhanded) event will be passed to scene.
*
* @param EventType - type of event
* @param TouchX - The X location of the touch event
* @param TouchY - The Y location of the touch event
* @param ObjectOver - The Object that mouse is over (NOTE: May be NULL or another object!)
*/
event bool OnTouch(ETouchType EventType, float TouchX, float TouchY, MobileMenuObject ObjectOver, float DeltaTime)
{
local float Velocity, SwipeDelta, FinalScrollDist, CalcScrollDist, SwipeTime;
local MobileMenuListItem Selected;
local int Index, Index0;
local bool bUdpateTouchItem;
TouchX -= Left;
TouchY -= Top;
Drag.TouchTime+= DeltaTime;
//`log("EventType:" $ String(EventType) @ "Y:" $ TouchY @ "Time:" $ DeltaTime @ Drag.TouchTime);
if (EventType == Touch_Began)
{
Movement.bIsMoving = false;
Drag.bIsDragging = true;
Drag.OrigSelectedItem = SelectedItem;
Drag.StartTouch.X = TouchX;
Drag.StartTouch.Y = TouchY;
Drag.ScrollAmount = 0;
Drag.AbsScrollAmount = 0;
Drag.bHasSelectedChanged = false;
Drag.TouchTime = 0;
Drag.NumUpdates = 0;
for (Index = 0; Index < NumInDragHistory; Index++)
{
Drag.UpdateHistory[Index].TouchTime = 0;
}
Drag.TouchedItem = GetItemClickPosition(TouchX, TouchY);
if (Drag.TouchedItem != none)
{
Drag.bIsDragging = !Drag.TouchedItem.OnTouch(EventType, TouchX, TouchY, DeltaTime);
}
}
else if (!Drag.bIsDragging)
{
bUdpateTouchItem = true;
}
else if ((EventType == Touch_Ended) || (EventType == Touch_Cancelled))
{
bUdpateTouchItem = true;
Drag.bIsDragging = false;
Movement.bIsMoving = true;
Movement.CurrentTime = 0;
Movement.OrigSelectedItem = SelectedItem;
if (!Drag.bHasSelectedChanged && (Drag.StartTouch.X == TouchX) && (Drag.StartTouch.Y == TouchY))
{
Selected = GetSelected();
// Fix annoyance issue when you try to swipe, but do so very quick and therefore
// it appears to only be a touch and it moves to the item you touched.
if((Drag.TouchTime > 0.05f) && bTapToScrollToItem)
{
// Force to scroll to item selected.
if (bIsVerticalList)
FinalScrollDist = TouchY - (SelectedOffset + (Selected.Height/2));
else
FinalScrollDist = TouchX - (SelectedOffset + (Selected.Width/2));
}
}
else if (Drag.NumUpdates >= 2)
{
Index = (Drag.NumUpdates - 1) % NumInDragHistory;
Index0 = (Drag.NumUpdates - Min(Drag.NumUpdates, NumInDragHistory)) % NumInDragHistory;
SwipeDelta = -(Drag.UpdateHistory[Index].TouchCoord - Drag.UpdateHistory[Index0].TouchCoord);
SwipeTime = Drag.UpdateHistory[Index].TouchTime - Drag.UpdateHistory[Index0].TouchTime;
// Find the final velocity.
Velocity = (SwipeTime > 0) ? (SwipeDelta / SwipeTime) : 0.0f;
// Using acceleration formulas - find how far it should take to stop and how long that will take.
FinalScrollDist = Square(Velocity) / (2.0 * Deacceleration);
//`log("Delta:" $ SwipeDelta @ "Vel:" $ Velocity @ "Dist:" $ FinalScrollDist);
}
if (bDisableScrolling)
FinalScrollDist = 0;
// See how far we will really go since we want to lock in selected position (no adjust)
if (SwipeDelta < 0)
CalcScrollDist = CalculateSelectedItem(SelectedItem, -FinalScrollDist, true);
else
CalcScrollDist = CalculateSelectedItem(SelectedItem, FinalScrollDist, true);
// If we don't have to scroll to selected, then use our original scroll dist.
// We still have to call CalculateSelectedItem() because it will tell us if bEndOfList.
// In that case, allow to auto scroll back to selected item.
if (!bForceSelectedToLineup && !SelectedItem.bEndOfList)
{
if (SwipeDelta < 0)
CalcScrollDist = -FinalScrollDist;
else
CalcScrollDist = FinalScrollDist;
}
// Restore...since we were looking into the future.
SelectedItem = Movement.OrigSelectedItem;
// Given this desired distance, and a little algebra on D = (D/T)**2/(2A) -> T = sqrt(D /(2A))
Movement.TotalTime = Sqrt(Abs(CalcScrollDist) / (2.0 * Deacceleration));
Movement.FullMovement = CalcScrollDist;
//`log("FinalDist:" $ CalcScrollDist @ "Time:" $ Movement.TotalTime);
}
else
{
Drag.UpdateHistory[Drag.NumUpdates % NumInDragHistory].TouchTime = Drag.TouchTime;
Drag.UpdateHistory[Drag.NumUpdates % NumInDragHistory].TouchCoord = (bIsVerticalList) ? TouchY : TouchX;
Drag.NumUpdates++;
if (Drag.OrigSelectedItem.Index != SelectedItem.Index)
{
Drag.bHasSelectedChanged = true;
}
Drag.ScrollAmount = (bIsVerticalList) ? (Drag.StartTouch.Y - TouchY) : (Drag.StartTouch.X - TouchX);
Index = (Drag.NumUpdates - 1) % NumInDragHistory;
Index0 = (Drag.NumUpdates - Min(Drag.NumUpdates, NumInDragHistory)) % NumInDragHistory;
SwipeDelta = abs(Drag.UpdateHistory[Index].TouchCoord - Drag.UpdateHistory[Index0].TouchCoord);
if (bDisableScrolling)
{
Drag.ScrollAmount = 0;
SwipeDelta = 0;
}
Drag.AbsScrollAmount += SwipeDelta;
//`log(Drag.AbsScrollAmount);
}
if (bUdpateTouchItem)
{
if (Drag.TouchedItem != none)
{
// If user has moved off of item, still update it, but indicate that we are no longer over it with -1.
if (Drag.TouchedItem == GetItemClickPosition(TouchX, TouchY))
{
Drag.TouchedItem.OnTouch(EventType, TouchX, TouchY, DeltaTime);
}
else
{
Drag.TouchedItem.OnTouch(EventType, -1, -1, DeltaTime);
}
}
}
return true;
}
function MobileMenuListItem GetItemClickPosition(out float MouseX, out float MouseY)
{
local int ScrollAmount, CurIndex, ScrollSize;
local MobileMenuListItem Item;
ScrollAmount = (bIsVerticalList) ? MouseY : MouseX;
ScrollAmount -= SelectedOffset;
// First attempt to scroll list up/left (if user swiped down/right)
// ScrollSize needs to be size of SelectedIndex after loop...
CurIndex = fMax(0, SelectedItem.Index); // Avoid [] out of bounds.
if (CurIndex >= Items.Length)
return none;
Item = Items[CurIndex];
ScrollSize = ItemScrollSize(Item);
while (ScrollAmount < 0)
{
if (CurIndex > 0)
CurIndex--;
else if (bLoops)
CurIndex = Items.Length - 1;
else
break;
Item = Items[CurIndex];
if (Item.bIsVisible)
{
ScrollSize = ItemScrollSize(Item);
ScrollAmount += ScrollSize;
}
}
// Now see if we need to go other way (because user swiped up/left)
while (ScrollAmount > ScrollSize)
{
if (CurIndex < (Items.length - 1))
CurIndex++;
else if (bLoops)
CurIndex = 0;
else
break;
Item = Items[CurIndex];
if (Item.bIsVisible)
{
ScrollAmount -= ScrollSize;
ScrollSize = ItemScrollSize(Item);
}
}
if (bIsVerticalList)
{
MouseY = ScrollAmount;
if (ScrollAmount < 0 || ScrollAmount > Item.Height)
{
Item = none;
}
}
else
{
MouseX = ScrollAmount;
if (ScrollAmount < 0 || ScrollAmount > Item.Width)
{
Item = none;
}
}
return Item;
}
function float CalculateSelectedItem(out SelectedMenuItem Selected, float ScrollAmount, bool bForceZeroAdjustment)
{
local float AdjustValue, ScrollSize, Scrolled, HalfScroll;
local int CurIndex;
local MobileMenuListItem Item;
AdjustValue = Selected.Offset;
// First scroll so that selected item is even.
Scrolled = AdjustValue;
ScrollAmount -= AdjustValue;
// First attempt to scroll list up/left (if user swiped down/right)
// ScrollSize needs to be size of SelectedIndex after loop...
CurIndex = fMax(0, Selected.Index); // Avoid [] out of bounds.
if (CurIndex >= Items.Length)
return 0;
Item = Items[CurIndex];
ScrollSize = ItemScrollSize(Item);
Selected.bEndOfList = false;
while (ScrollAmount < 0)
{
if (CurIndex > 0)
{
CurIndex--;
}
else if (bLoops)
{
CurIndex = Items.Length - 1;
}
else
{
// We are at top item - cause dragging to be less effective.
ScrollAmount *= EndOfListSupression;
Selected.bEndOfList = true;
break;
}
Item = Items[CurIndex];
if (Item.bIsVisible)
{
ScrollSize = ItemScrollSize(Item);
ScrollAmount += ScrollSize;
Scrolled -= ScrollSize;
Selected.Index = CurIndex;
}
}
// Now see if we need to go other way (because user swiped up/left or we went too far above)
HalfScroll = (ScrollSize/2);
while (ScrollAmount > HalfScroll)
{
if (CurIndex < (Items.length - (NumShowEndOfList + 1)))
{
CurIndex++;
}
else if (bLoops)
{
CurIndex = 0;
}
else
{
// We are at bottom item - cause dragging to be less effective.
// Need to take out the half scroll so it does not jump on transition from when
// this code is not executed, and when it is.
ScrollAmount -= HalfScroll;
ScrollAmount *= EndOfListSupression;
ScrollAmount += HalfScroll;
Selected.bEndOfList = true;
break;
}
Item = Items[CurIndex];
if (Item.bIsVisible)
{
ScrollAmount -= ScrollSize;
Scrolled += ScrollSize;
Selected.Index = CurIndex;
ScrollSize = ItemScrollSize(Item);
}
}
if (bForceZeroAdjustment)
{
Selected.Offset = 0;
}
else
{
Selected.Offset = -ScrollAmount;
Scrolled -= ScrollAmount;
}
return Scrolled;
}
function UpdateScroll(float DeltaTime)
{
local float ScrollAmount;
if (Drag.bIsDragging)
{
SelectedItem = Drag.OrigSelectedItem;
ScrollAmount = Drag.ScrollAmount;
}
else if (Movement.bIsMoving)
{
SelectedItem = Movement.OrigSelectedItem;
Movement.CurrentTime += DeltaTime;
if (Movement.CurrentTime < Movement.TotalTime)
{
ScrollAmount = FInterpEaseOut(0, Movement.FullMovement, Movement.CurrentTime/Movement.TotalTime, EaseOutExp);
//`log(ScrollAmount $ "=" $ Movement.FullMovement @ "Fraction:" $ Movement.CurrentTime/Movement.TotalTime );
}
else
{
ScrollAmount = Movement.FullMovement;
Movement.bIsMoving = false;
}
}
else
{
return;
}
CalculateSelectedItem(SelectedItem, ScrollAmount, false);
}
/**
* Render the widget
*
* @param Canvas - the canvas object for drawing
*/
function RenderObject(canvas Canvas, float DeltaTime)
{
local MobileMenuListItem Item;
local float OrgX, OrgY; //, ClipX, ClipY;
local int VpEnd, CurIndex, First, Last, SelectedIdx, NumItems, RealIndex;
local Vector2D VpPos, VpSize;
NumItems = Items.Length;
if (NumItems == 0)
return;
UpdateScroll(DeltaTime);
VpSize.X = Width;
VpSize.Y = Height;
// Find top displayed visible item.
SelectedIdx = fMax(0, SelectedItem.Index); // Avoid [] out of bounds.
// If we loop, then we add NumItems for 0 compares but always mod to get real index.
if (bLoops)
SelectedIdx += NumItems;
First = SelectedIdx;
if (bIsVerticalList)
{
VpPos.X = Left;
VpPos.Y = Top + SelectedOffset + SelectedItem.Offset;
VpEnd = Top + Height;
while ((First > 0) && (VpPos.Y > Top))
{
First--;
Item = Items[First % NumItems];
if (Item.bIsVisible)
VpPos.Y -= Item.Height;
}
}
else
{
VpPos.X = Left + SelectedOffset + SelectedItem.Offset;
VpPos.Y = Top;
VpEnd = Left + Width;
while ((First > 0) && (VpPos.X > Left))
{
First--;
Item = Items[First % NumItems];
if (Item.bIsVisible)
VpPos.X -= Item.Width;
}
}
// Make sure our First is actually visible.
while ((First + 1) < NumItems)
{
Item = Items[First];
if (Item.bIsVisible)
break;
First++;
};
// Calculate viewports for everyone
Last = First;
for (CurIndex = 0; CurIndex < NumItems; CurIndex++)
{
RealIndex = (bLoops) ? ((First + CurIndex) % NumItems) : (First + CurIndex);
if (RealIndex >= NumItems)
{
break;
}
Item = Items[RealIndex];
if (Item.bIsVisible)
{
Last = First + CurIndex;
if (bIsVerticalList)
{
VpSize.Y = Item.Height;
Item.VpPos = VpPos;
Item.VpSize = VpSize;
VpPos.Y += VpSize.Y;
if (VpPos.Y >= VpEnd)
break;
}
else
{
VpSize.X = Item.Width;
Item.VpPos = VpPos;
Item.VpSize = VpSize;
VpPos.X += VpSize.X;
if (VpPos.X >= VpEnd)
break;
}
}
}
OrgX = Canvas.OrgX;
OrgY = Canvas.OrgY;
// Does not good :(
//ClipX = Canvas.ClipX;
//ClipY = Canvas.ClipY;
//Canvas.ClipX = Left + Width;
//Canvas.ClipY = Top + Height;
// Now render up to (not including) selected, then backwards to and including selected.
// This is so if we render larger that our VP, the selected on will be top.
// Normally these loop conditions do not kick it out, it is the check with RealIndex, the
// conditions are just safety checks (like a small list)
for (CurIndex = First; CurIndex < SelectedIdx; CurIndex++)
{
Item = Items[CurIndex % NumItems];
if (Item.bIsVisible)
{
Canvas.SetOrigin(Item.VpPos.X, Item.VpPos.Y);
Item.RenderItem(self, Canvas, DeltaTime);
}
}
for (CurIndex = Last; CurIndex >= SelectedIdx; CurIndex--)
{
Item = Items[CurIndex % NumItems];
if (Item.bIsVisible)
{
Canvas.SetOrigin(Item.VpPos.X, Item.VpPos.Y);
Item.RenderItem(self, Canvas, DeltaTime);
}
}
FirstVisible = First;
LastVisible = Last;
// Restore to not mess up next scene.
Canvas.OrgX = OrgX;
Canvas.OrgY = OrgY;
//Canvas.ClipX = ClipX;
//Canvas.ClipY = ClipY;
}
/** Amount of space item takes up in scroll direction */
function int ItemScrollSize(MobileMenuListItem Item)
{
return (bIsVerticalList) ? Item.Height : Item.Width;
}
defaultproperties
{
NumShowEndOfList=0
bIsActive=true
bIsVerticalList=true
bTapToScrollToItem=true
Deacceleration = 1500
EaseOutExp=4.0
EndOfListSupression=0.4f
SelectedItem=(Index=0, Offset=0)
SelectedOffset = 0
bForceSelectedToLineup=true
}