Scrolling List
The scrolling list class attempts to create a new control which behaves similar to the standard list controls found in iOS interfaces. It is a list of items, each one a UDNMobileMenuButton, which can be scrolled up or down using touch input. Swiping will cause the list to continue to move for a bit after the touch ends exhibiting an "inertial" scrolling behavior. These lists must always be fullscreen because of the limitations of clipping text, i.e. it can only be clipped by the extents of the viewport.
Code:
class UDNMobileMenuList extends UDNMobileMenuObject;
/** text to display as the title of the list. */
var string Title;
/** Color to use for drawing the title text */
var Color CaptionColor;
/** If TRUE, center the caption text. otherwise, left align it */
var bool bCenterText;
/** Color to use for filling the list background */
var Color BackgroundColor;
/** Color to use for the list border */
var Color BorderColor;
/** Colors to use for drawing the background of the list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemBackgroundColors[2];
/** Colors to use for drawing the border of the list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemBorderColors[2];
/** Colors to use for drawing the text of list items. [0] is for non-selected items. [1] is for the selected item. */
var LinearColor ItemCaptionColors[2];
/** Font to use for drawing list item text. */
var Font ItemFont;
/** Reference to the Cancel button used to close the list */
var instanced UDNMobileMenuButton Cancel;
/** If TRUE, the list be be cancelable (i.e., the Cancel button will be displayed) */
var bool bHasCancel;
/** The list of items belonging to the menu */
var instanced array<UDNMobileMenuButton> Items;
/** Index of the currently selected item */
var int SelectedIndex;
/** Height of each item cell in the list */
var float ItemHeight;
/** Height of the title bar */
var float TitleBarHeight;
/** Color of the title bar */
var Color TitleBarColor;
/** TRUE when the list is being controlled, i.e. when the user is actively scrolling the list. */
var bool bActive;
/** Holds the cached location of the last touch event for the list */
var vector LastTouchLocation;
/** The amount to scroll the list while active. (This is the delta between touch updates. It keeps the list in sync with the user's finger.) */
var float ScrollAmount;
/** The amount to scroll the list when inactive. (This is how much stored velocity the list has from a swipe.) */
var float ScrollInertia;
/** Holds the cached time of the last render update. */
var float LastRenderTime;
/** Multiplier for the touch movement used to control the ScrollInertia. */
var float SwipeFactor;
/** Multiplier for the speed at which the list snaps back when scrolled past its bounds. */
var float SnapFactor;
/** Distance past the list's bounds it can be actively scrolled. */
var float ScrollLimit;
/****************************************
* Init and Rendering Functions
****************************************/
/**
* Initialize the list
*/
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
//Force list to the size of the device's screen since text can't be easily clipped by the list's bounds
Left = 0;
Top = 0;
Width = ScreenWidth;
Height = ScreenHeight;
//initialize the Cancel button (the menu system has no built-in handling for controls within controls)
Cancel.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
//Position the Cancel button vertically centered on the right of the title bar
Cancel.Top = Top + ((TitleBarHeight - Cancel.Height) / 2);
Cancel.Left = Left + Width - Cancel.Width - 1;
//Grab click events from the button
Cancel.OnClick = HandleCancel;
}
/**
* Draw the list
*/
function RenderObject(canvas Canvas)
{
local int i;
local float DeltaTime;
local float ScrollDelta;
Canvas.SetPos(Left, Top);
//Draw the list's background
Canvas.DrawColor = BackgroundColor;
Canvas.DrawRect(Width, Height);
//Calculate the render delta to use for passively scrolling the list
if(LastRenderTime != 0)
{
DeltaTime = InputOwner.Outer.WorldInfo.RealTimeSeconds - LastRenderTime;
}
lastRenderTime = InputOwner.Outer.WorldInfo.RealTimeSeconds;
if(Items.Length > 0)
{
//Calculate scroll delta for this render pass and snap the list back if outside the list bounds
if(Items[0].Top > Top + TitleBarHeight)
{
ScrollInertia = Top + TitleBarHeight - Items[0].Top;
ScrollDelta = ScrollInertia * DeltaTime * SnapFactor;
}
else if(Items[Items.Length - 1].Top + ItemHeight < Top + Height)
{
ScrollInertia = (Top + Height) - (Items[Items.Length - 1].Top + ItemHeight);
ScrollDelta = ScrollInertia * DeltaTime * SnapFactor;
}
else
{
ScrollDelta = ScrollInertia * DeltaTime;
}
//render each list item
for(i=0; i < Items.Length; i++)
{
//force item size here in case orientation changes
Items[i].Left = left;
Items[i].Top += bActive ? ScrollAmount : ScrollDelta;
Items[i].Width = Width;
Items[i].Height = ItemHeight;
//Only render item if part of it is visible
if(Items[i].Top + ItemHeight > Top && Items[i].Top < Top + Height)
{
Items[i].RenderObject(Canvas);
}
}
}
if(bActive)
{
//Zero out active scroll if active
ScrollAmount = 0;
}
else
{
//decrement scroll inertia if passive
ScrollInertia -= ScrollDelta;
}
//Draw title bar - we draw after the list items so they are clipped by it
Canvas.SetPos(Left, Top);
Canvas.DrawColor = TitleBarColor;
Canvas.DrawRect(Width, TitleBarHeight);
//Draw title
RenderTitle(Canvas);
//Draw list border
Canvas.SetPos(Left, Top + TitleBarHeight);
Canvas.DrawColor = BorderColor;
Canvas.DrawBox(Width, Height - TitleBarHeight);
//Drawn Cancel button if desired
if(bHasCancel)
{
Cancel.RenderObject(Canvas);
}
}
/**
* Draw the list's title text
*/
function RenderTitle(canvas Canvas)
{
local float X,Y,UL,VL;
local FontRenderInfo FRI;
//don't bother if there is no title
if (Title != "")
{
//set font (we're just borrowing the scene font. Could add custom font)
Canvas.Font = OwnerScene.SceneCaptionFont;
Canvas.TextSize(Title,UL,VL);
//Calculate position - Centered or Left-aligned
if(bCenterText)
{
X = Left + (Width / 2) - (UL/2);
Y = Top + (Height /2) - (VL/2);
}
else
{
X = Left + (TitleBarHeight /2) - (VL/2);
Y = Top + (TitleBarHeight /2) - (VL/2);
}
//Draw title text clipped (Could add ability to shorten if it won't fit, i.e. use ...)
Canvas.SetPos(OwnerScene.Left + X - Canvas.OrgX, OwnerScene.Top + Y - Canvas.OrgY);
Canvas.DrawColor = CaptionColor;
FRI.bClipText = true;
Canvas.DrawText(Title, false, 1.0, 1.0, FRI);
}
}
/****************************************
* Item Management Functions
****************************************/
/**
* Add a new item to the list
*
* @param item - the value of the new item to add
*/
function AddItem(string item)
{
local UDNMobileMenuButton NewItem;
local Vector2D ViewportSize;
//Create a new item (a button)
NewItem = new(Outer) class'UDNMobileMenuButton';
if(NewItem != none)
{
//initialize the new item
LocalPlayer(InputOwner.Outer.Player).ViewportClient.GetViewportSize(ViewportSize);
NewItem.InitMenuObject(InputOwner, OwnerScene, ViewportSize.X, ViewportSize.Y);
//Set new item properties
NewItem.Caption = item;
NewItem.CaptionColors[0] = ItemCaptionColors[0];
NewItem.CaptionColors[1] = ItemCaptionColors[1];
NewItem.TextFont = ItemFont;
NewItem.Left = Left;
NewItem.BackgroundColors[0] = ItemBackgroundColors[0];
NewItem.BackgroundColors[1] = ItemBackgroundColors[1];
NewItem.BorderColors[0] = ItemBorderColors[0];
NewItem.BorderColors[1] = ItemBorderColors[1];
NewItem.Top = Top + TitleBarHeight + (Items.Length * ItemHeight);
NewItem.OnClick = OnSelect;
NewItem.bCenterText = false;
NewItem.BorderWidth[2] = 1;
//If this is to be the selected item, highlight it
if(Items.Length == SelectedIndex)
{
NewItem.bIsHighlighted = true;
}
//Add the item to the list
Items.AddItem(NewItem);
}
}
/**
* Remove a value from the list
*
* @param Idx - the index of the item to remove
*/
function RemoveItem(int Idx)
{
local int i;
//remove the item from the list
Items.Remove(Idx, 1);
//shift subsequent items up
for(i=Idx; i < Items.Length; i++)
{
Items[i].Top -= ItemHeight;
}
//If we removed the selected item, reset selection to first item
if(SelectedIndex == Idx)
{
SelectedIndex = 0;
if(Items.Length > 0)
{
Items[0].bIsHighlighted = true;
}
}
}
/**
* Get the value of the list.
*
* Returns the caption of the selected item
*/
function string GetValue()
{
//must have items to access selected index
if(Items.Length > 0)
{
//return the caption (value) of the selected item
return Items[SelectedIndex].Caption;
}
else
{
//no items - return empty string
return "";
}
}
/****************************************
* Input Functions
****************************************/
/**
* Delegate fired when an item is selected in the list
*/
delegate OnChange(int Idx, string Item, float X, float Y);
/**
* Delegate fired when the list is canceled
*/
delegate OnCancel();
/**
* Implementation of the ITouchable interface
*/
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
local UDNMobileMenuButton Label;
local float ActualY;
Super.OnTouch(Type, X, Y);
//Initial touch event
if(Type == ZoneEvent_Touch)
{
//Ignore if touch is outside list or in title bar
if(CheckBounds(X, Y) && Y > Top + TitleBarHeight)
{
//set list active - user is controlling it
bActive = true;
//Cache touch location
LastTouchLocation.X = X;
LastTouchLocation.Y = y;
//reset scroll values
ScrollAmount = 0;
ScrollInertia = 0;
}
}
//Touch in progress
else if(Type == ZoneEvent_Update || Type == ZoneEvent_Stationary)
{
//Ignore if the list is not active - touch must have started outside the list
if(bActive)
{
/************************************************************
* We only update scrolling if:
*
* * There are enough items to require scrolling
* * The list is not scrolled outside the bounds already
*
************************************************************/
if( (Items.Length * ItemHeight > Height) &&
((Items[0].Top - (Top + TitleBarHeight) < ScrollLimit) || (Y < LastTouchLocation.Y)) &&
(((Top + Height) - (Items[Items.Length - 1].Top + ItemHeight) < ScrollLimit) || (Y > LastTouchLocation.Y)))
{
//Actual inertia is scaled by device scaling and custom swipe factor to get used inertia
ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
//Reset scroll inertia if the movement is negligible. Otherwise, increment it with new delta.
if(Abs(ActualY) < UDNMobileMenuScene(OwnerScene).SwipeTolerance)
{
ScrollInertia = 0;
}
else
{
ScrollInertia += ActualY;
}
//Calculate movement delta for active scrolling and increment
ActualY = (Y - LastTouchLocation.Y);
ScrollAmount += ActualY;
}
//Update cached touch location
LastTouchLocation.X = X;
LastTouchLocation.Y = y;
}
}
//touch ending because user lifted finger
else if(Type == ZoneEvent_Untouch)
{
//not a swipe and within the list
if(!CheckSwipe() && CheckBounds(X, Y))
{
//not in title bar - must be a tap on an item
if(Y > Top + TitleBarHeight)
{
//pass tap on to all items
foreach Items(Label)
{
Label.OnTouch(Type, X, Y);
}
}
//tap on title bar
else
{
//if list can be canceled, let the Cancel button handle the tap
if(bHasCancel)
{
Cancel.OnTouch(Type, X, Y);
}
}
}
//update scroll inertia one last time before going passive
if(bActive)
{
ActualY = (Y - LastTouchLocation.Y) * class'MobileMenuScene'.static.GetGlobalScaleY() * SwipeFactor;
ScrollInertia += ActualY;
//set list passive
bActive = false;
}
}
//touch ending prematurely
else if(Type == ZoneEvent_Cancelled)
{
//set passive
bActive = false;
//zero out all scroll values
ScrollAmount = 0;
ScrollInertia = 0;
}
}
/**
* Selects a new item in the list.
* Assigned to the OnClick() of each item in the list.
*/
function OnSelect(UDNMobileMenuObject Sender, float X, float Y)
{
local UDNMobileMenuButton Label;
local int i;
i = 0;
if(UDNMobileMenuButton(Sender) != none)
{
//iterate items to find item matching sender
foreach Items(Label)
{
//found the matching item
if(Label == UDNMobileMenuButton(Sender))
{
//Set new selected index
SelectedIndex = i;
//Highlight the selected item
Sender.bIsHighlighted = true;
//Call OnChange delegate to alert anyone listening
OnChange(i, Label.Caption, X, Y);
//break out of the iterator since we're done here
break;
}
i++;
}
}
//set passive
bActive = false;
//zero out all scroll values
ScrollAmount = 0;
ScrollInertia = 0;
}
/**
* Cancels the list.
* Assigned to the OnClick() of the Cancel button.
*/
function HandleCancel(UDNMobileMenuObject Sender, float X, float Y)
{
//Call the OnCancel delegate to alert anyone listening
OnCancel();
//set passive
bActive = false;
//zero out all scroll values
ScrollAmount = 0;
ScrollInertia = 0;
}
defaultproperties
{
CaptionColor=(R=255,G=255,B=255,A=255)
BackgroundColor=(R=64,G=64,B=64,A=255)
BorderColor=(R=64,G=64,B=64,A=255)
ItemBackgroundColors(0)=(R=0.5,G=0.5,B=0.5,A=1.0)
ItemBackgroundColors(1)=(R=0.75,G=0.75,B=0.75,A=1.0)
ItemBorderColors(0)=(R=0.25,G=0.25,B=0.25,A=1.0)
ItemBorderColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
ItemCaptionColors(0)=(R=1.0,G=1.0,B=1.0,A=1.0)
ItemCaptionColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
ItemFont=Font'EngineFonts.SmallFont'
ItemHeight=48
SwipeFactor=5
SnapFactor=5
ScrollLimit=20
TitleBarHeight=32
TitleBarColor=(R=96,G=96,B=96,A=255)
Begin Object class=UDNMobileMenuButton name=CancelButton
Tag="Cancel"
Width=64
Height=32
Caption="Cancel"
TextFont=Font'EngineFonts.SmallFont'
CaptionColors(0)=(r=1.0,g=1.0,b=1.0,a=1.0)
CaptionColors(1)=(r=0.0,g=0.0,b=0.0,a=1.0)
Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
ImagesUVs(0)=(bCustomCoords=true,U=0,V=896,UL=256,VL=128)
ImagesUVs(1)=(bCustomCoords=true,U=0,V=896,UL=256,VL=128)
End Object
Cancel=CancelButton
}
ComboBox
The combobox control provides a simple way to have settings in a menu whose value can be selected from a list of options. The main visual component of a combobox is a touchable button that toggles open a scrolling list control when tapped. The list displays all the options for the combobox and when one is selected, the list closes, setting the value of the combobox to the new selected option.
Note: The options for the combobox currently require hard coding when the combobox is created. This can easily be changed to work as a per-object-config property or some other method of populating the list of available options.
Code:
class UDNMobileMenuComboBox extends UDNMobileMenuObject;
/** The button that visually represents the combo box */
var instanced UDNMobileMenuButton Label;
/** The list used to select new values for the combo box */
var instanced UDNMobileMenulist list;
/** The title of the combo box - used to set the title of the list as well */
var string Title;
/** If TRUE, show the title. Otherwise show the current value. */
var bool bShowTitle;
/** The list of values used to populate the list */
var array<string> Items;
/** The index of the value to initialize the combo box to */
var int InitialSelectedIndex;
/** TRUE when the list is open for selecting a new value */
var bool bIsOpen;
/****************************************
* Init and Render Functions
****************************************/
/**
* Initializes the combo box
*/
function InitMenuObject(MobilePlayerInput PlayerInput, MobileMenuScene Scene, int ScreenWidth, int ScreenHeight)
{
local string item;
Super.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
//initialize label and list
Label.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
List.InitMenuObject(PlayerInput, Scene, ScreenWidth, ScreenHeight);
//Set position and size of label
Label.Left = Left;
Label.Width = Width;
Label.Top = Top;
Label.height = height;
//initialize label caption
Label.Caption = Items[InitialSelectedIndex];
//Set up delegates
Label.OnClick = OnClick;
List.OnChange = OnSelect;
List.OnCancel = OnCancel;
//initialize list
List.bHasCancel = true;
List.SelectedIndex = InitialSelectedIndex;
List.Title = Title;
//Add item values as items to the list
foreach Items(item)
{
List.AddItem(item);
}
}
/**
* Draw the combo box
*/
function RenderObject(canvas Canvas)
{
//don't render anything if open - the list belonging to the owner scene handles rendering
if(!bIsOpen)
{
//Update label text
if(bShowTitle)
{
Label.Caption = Title;
}
else
{
Label.Caption = GetValue();
}
//Draw the label
Label.RenderObject(Canvas);
}
}
/****************************************
* List Management Functions
****************************************/
/**
* Add an item to the combo box's options
*
* @param item - the value of the item to add
*/
function AddItem(string Item)
{
//Add the value to out internal list
Items.Additem(Item);
//Add the value as a new item in the actual list
List.Additem(Item);
}
/**
* Removes an value from the combo box's options
*
* @param Idx - index of the item to remove
*/
function RemoveItem(int Idx)
{
//remove the item from our internal list
Items.Remove(Idx, 1);
//Remove the value from the actual list
List.RemoveItem(Idx);
}
/**
* Toggle whether the list is open or closed
*/
function ToggleList()
{
//toggle open value
bIsOpen = !bIsOpen;
//update the owner scene's list
if(bIsOpen)
{
//set our list as the active list and make sure it is visible
UDNMobileMenuScene(OwnerScene).List = List;
List.bIsHidden = false;
}
else
{
//Null out the scene's list reference and make our list invisible
UDNMobileMenuScene(OwnerScene).List = none;
List.bIsHidden = true;
}
}
/**
* Returns the value of the combo box (via it's list)
*/
function string GetValue()
{
//let list handle getting the value
return List.GetValue();
}
/****************************************
* Input Functions
****************************************/
/**
* Delegate fired when an item is selected
*/
delegate OnChange(int Idx, string Item, float X, float Y);
/**
* Implementation of the OnTouch() of the ITouchable interface
*/
function OnTouch(EZoneTouchEvent Type, float X, float Y)
{
Super.OnTouch(Type, X, Y);
//the list is open, pass input to the list
if(bIsOpen)
{
List.OnTouch(Type, X, Y);
}
//the list is closed
else
{
//we got a tap, toggle the list open
if(!CheckSwipe())
{
Label.OnTouch(Type, X, Y);
}
}
}
function OnClick(UDNMobileMenuObject Sender, float X, float Y)
{
ToggleList();
}
/**
* Called when an item is selected
* Assigned to the OnChange() of the list
*/
function OnSelect(int Idx, string item, float X, float Y)
{
//close the list
ToggleList();
//fire off our OnChange delegate to anyone listening
OnChange(Idx, Item, X, Y);
//let the owner scene know we got a tap
OwnerScene.OnTouch(self, X, Y, false);
}
/**
* Cancels selection, closing the list
*/
function OnCancel()
{
//Selection was canceled, close the list
ToggleList();
}
defaultproperties
{
Begin Object class=UDNMobileMenuButton name=Label0
Tag="Combo_LabelItem"
bCenterText=false
TextFont=Font'EngineFonts.SmallFont'
CaptionColors(0)=(R=0.125,G=0.125,B=0.125,A=1.0)
CaptionColors(1)=(R=0.125,G=0.125,B=0.125,A=1.0)
Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
ImagesUVs(0)=(bCustomCoords=true,U=0,V=768,UL=1024,VL=128)
ImagesUVs(1)=(bCustomCoords=true,U=0,V=768,UL=1024,VL=128)
End Object
Label=Label0
Begin Object class=UDNMobileMenuList name=List0
Tag="Combo_ItemsList"
End Object
List=List0
}
Example Menu Scene - Putting It All Together
The final step is to create a sample menu using these new controls. It should extend from the base UDNMobileMenuScene class so that it can accept input from the helper scene and pass it to touchable controls. This example has two comboboxes and a label. Changing the value of either combobox updates the label with its new value. Also, one of the comboboxes always displays its "name", while the other shows its current value.
Code:
class UDNMobileMenuExample extends UDNMobileMenuScene;
/**
* Handle basic taps from controls
*/
event OnTouch(MobileMenuObject Sender,float TouchX, float TouchY, bool bCancel)
{
//Combobox 1 changed, set label to its value
if(Sender.Tag == "Combo1")
{
UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo1")).GetValue();
}
//Combobox 2 changed, set label to its value
else if(Sender.Tag == "Combo2")
{
UDNMobileMenuLabel(FindMenuObject("Title")).Caption = UDNMobileMenuComboBox(FindMenuObject("Combo2")).GetValue();
}
}
defaultproperties
{
//Create a combobox
Begin Object class=UDNMobileMenuComboBox name=Combo0
Tag="Combo1"
Title="Setting One"
Height=32
Width=256
Top=20
Left=20
Items(0)="Item 0"
Items(1)="Item 1"
Items(2)="Item 2"
Items(3)="Item 3"
Items(4)="Item 4"
Items(5)="Item 5"
InitialSelectedIndex=3
bShowTitle=true
End Object
MenuObjects.Add(Combo0)
//Create a combobox
Begin Object class=UDNMobileMenuComboBox name=Combo1
Tag="Combo2"
Title="Setting Two"
Height=32
Width=256
Top=60
Left=20
Items(0)="Object 0"
Items(1)="Object 1"
Items(2)="Object 2"
Items(3)="Object 3"
Items(4)="Object 4"
Items(5)="Object 5"
Items(6)="Object 6"
Items(7)="Object 7"
Items(8)="Object 8"
Items(9)="Object 9"
Items(10)="Object 10"
Items(11)="Object 11"
Items(12)="Object 12"
Items(13)="Object 13"
Items(14)="Object 14"
Items(15)="Object 15"
InitialSelectedIndex=9
End Object
MenuObjects.Add(Combo1)
//Create a label
Begin Object class=UDNMobileMenuLabel name=Label0
Tag="Title"
Height=32
Width=160
Left=296
Top=20
TextFont=Font'EngineFonts.SmallFont'
CaptionColors(0)=(R=1.0,G=1.0,B=1.0,A=1.0)
CaptionColors(1)=(R=1.0,G=1.0,B=1.0,A=1.0)
Images(0)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
Images(1)=Texture2D'UDNExampleUI.Menus.T_MenuUI'
ImagesUVs(0)=(bCustomCoords=true,U=576,V=384,UL=192,VL=192)
ImagesUVs(1)=(bCustomCoords=true,U=576,V=384,UL=192,VL=192)
End Object
MenuObjects.Add(Label0)
}
Bookmarks