To integrate a custom grid, begin by organizing your project structure:
📁 CustomGrid
│ ├─ 📄 CustomGrid.xml
│ └─ 📄 CustomGrid.brs
│
📁 ChannelItem
│ ├─ 📄 ChannelItem.xml
│ └─ 📄 ChannelItem.brs
In this structure:
- The
CustomGrid
folder contains two files:customgrid.xml
andcustomgrid.brs
, responsible for managing the custom grid functionality, including translation, scrolling, and key event handling. - The
ChannelItem
folder contains two files:ChannelItem.xml
andChannelItem.brs
, which define the design and behavior of individual items displayed within the grid.
Note: Code for above files (CustomGrid.xml, CustomGrid.brs, ChannelItem.xml, ChannelItem.brs) are attached below👇👇👇.
Next step, to integrate a custom grid into your Roku app, simply add the following snippet code to any UI view or page:
<CustomGrid id="gridItems" clippingRect="[0, 0, 1737, 796]" translation="[10, 50]" itemComponentName="ChannelItem" />
' here itemcomponentName is individual griditem , (this will use ChannelItem node )
This snippet initializes the custom grid component and specifies attributes such as clipping rectangle, translation, and the item component name. You can easily modify these fields to suit your specific needs and design preferences.
IMPORTANT: To populate and display data within the grid, use ContentNode.
Refer to the visual guide below for a seamless integration.
.
.
.
Code
CustomGrid.xml
<?xml version="1.0" encoding="UTF-8"?>
<component name="CustomGrid" extends="Group">
<interface>
<field id="content" type="node" onChange="onContentChanged" />
<field id="itemComponentName" type="string" />
<field id="numColumns" type="integer" value="4" />
<field id="itemSpacings" type="intarray" value="[19, 40]" />
<field id="itemSize" type="intarray" value="[300, 250]" />
<field id="rowItemFocusIndex" type="intarray" value="[0, 0]" onChange="onRowItemFocusIndexChanged" />
<field id="rowItemSelectedIndex" type="intarray" onChange="onRowItemSelected" alwaysNotify="true" />
<field id="selectedItem" type="node" />
</interface>
<script type="text/brightscript" uri="./CustomGrid.brs" />
<children>
<Group id="resultGridWrapper" />
<Animation id="gridScrollAnimation" duration="0.5" easeFunction="inOutQuad" optional="true">
<Vector2DFieldInterpolator id="gridScrollAnimationInterpolator" key="[0.0, 1.0]" fieldToInterp="resultGridWrapper.translation" />
</Animation>
</children>
</component>
CustomGrid.brs
'******************************************************************************************
'* File: CustomGrid.brs
'******************************************************************************************
'***********************************************************************
'* init()
'***********************************************************************
function init() as void
m.resultGridWrapper = m.top.findNode("resultGridWrapper")
m.gridScrollAnimation = m.top.findNode("gridScrollAnimation")
m.gridScrollAnimationInterpolator = m.top.findNode("gridScrollAnimationInterpolator")
m.top.observeField("focusedChild", "onFocusedChildChanged")
m.displayedRows = [0, 1]
m.displayedColumn = 0
m.rowItemFocusIndex = [0, 0]
m.lastTranslation = [0, 0]
end function
'***********************************************************************
'* onContentChanged()
'***********************************************************************
function onContentChanged() as void
m.content = m.top.content
currentCount = m.content.getChildCount()
if (m.content <> invalid)
if (m.resultGridWrapper.getChildCount() > 0 and m.itemCount <> invalid)
endItemIndex = currentCount - 1
startItemIndex = m.itemCount
else
endItemIndex = m.content.getChildCount() - 1
startItemIndex = 0
end if
m.itemCount = m.content.getChildCount()
itemComponentName = m.top.itemComponentName
for i = startItemIndex to endItemIndex
item = m.resultGridWrapper.createChild(itemComponentName)
item.itemContent = m.content.getChild(i)
end for
updateItemLayout(startItemIndex, endItemIndex)
end if
end function
'***********************************************************************
'* updateItemLayout()
'***********************************************************************
function updateItemLayout(startItemIndex = 0 as integer, endItemIndex = m.content.getChildCount() as integer)
itemSpacings = m.top.itemSpacings
itemSize = m.top.itemSize
for i = startItemIndex to endItemIndex
item = m.resultGridWrapper.getChild(i)
item.translation = [((i mod 4) * (itemSpacings[0] + itemSize[0])), caculateRowIndex(i) * (itemSpacings[1] + itemSize[1])]
end for
end function
'***********************************************************************
'* onFocusedChildChanged(data)
'* Should be overridden by inherited view.
'***********************************************************************
function onFocusedChildChanged(data = invalid) as void
if(m.rowItemFocusIndex = invalid)
m.rowItemFocusIndex = m.top.rowItemFocusIndex
end if
if(m.rowItemFocusIndex <> invalid)
itemFocusedIndex = m.rowItemFocusIndex[0] * 4 + m.rowItemFocusIndex[1]
if (m.top.hasFocus() and m.resultGridWrapper.getChildCount() > 0)
m.resultGridWrapper.getChild(itemFocusedIndex).setFocus(true)
end if
end if
end function
'***********************************************************************
'* onKeyRight(isPressed, timeInMs)
'***********************************************************************
function onKeyRight(isPressed as boolean, timeInMs as longinteger) as boolean
isHandled = false
if (isPressed = true)
rowItemFocusIndex = m.rowItemFocusIndex
rowIndex = rowItemFocusIndex[0]
rowItemIndex = rowItemFocusIndex[1]
itemFocusedIndex = m.rowItemFocusIndex[0] * 4 + m.rowItemFocusIndex[1]
if ((rowItemIndex < 3) and (itemFocusedIndex < m.itemCount - 1))
m.top.rowItemFocusIndex = [rowIndex, rowItemIndex + 1]
end if
isHandled = true
end if
return isHandled
end function
'***********************************************************************
'* onKeyLeft(isPressed, timeInMs)
'***********************************************************************
function onKeyLeft(isPressed as boolean, timeInMs as longinteger) as boolean
isHandled = false
if (isPressed = true)
rowItemFocusIndex = m.rowItemFocusIndex
rowIndex = rowItemFocusIndex[0]
rowItemIndex = rowItemFocusIndex[1]
if (rowItemIndex > 0)
m.top.rowItemFocusIndex = [rowIndex, rowItemIndex - 1]
isHandled = true
else
isHandled = false
end if
end if
return isHandled
end function
'***********************************************************************
'* onKeyDown(isPressed, timeInMs)
'***********************************************************************
function onKeyDown(isPressed as boolean, timeInMs as longinteger) as boolean
isHandled = false
if (isPressed = true)
if (m.lastTimeInMs <> invalid)
if (getCurrentTimeInMs() - m.lastTimeInMs < m.gridScrollAnimation.duration * 500)
return true
end if
end if
rowItemFocusIndex = m.rowItemFocusIndex
rowIndex = rowItemFocusIndex[0]
rowItemIndex = rowItemFocusIndex[1]
maxRowIndex = caculateRowIndex(m.itemCount - 1)
if (rowIndex < maxRowIndex)
lastRowItemMaxIndex = (m.itemCount - 1) mod 4
if (rowIndex + 1 = maxRowIndex and rowItemIndex > lastRowItemMaxIndex)
rowItemIndex = lastRowItemMaxIndex
m.displayedColumn = lastRowItemMaxIndex
end if
m.top.rowItemFocusIndex = [rowIndex + 1, rowItemIndex]
end if
m.lastTimeInMs = timeInMs
isHandled = true
end if
return isHandled
end function
'***********************************************************************
'* onKeyLeft(isPressed, timeInMs)
'***********************************************************************
function onKeyUp(isPressed as boolean, timeInMs as longinteger) as boolean
isHandled = false
if (isPressed = true)
rowItemFocusIndex = m.rowItemFocusIndex
rowIndex = rowItemFocusIndex[0]
rowItemIndex = rowItemFocusIndex[1]
if (rowIndex > 0)
if (m.lastTimeInMs <> invalid)
if (getCurrentTimeInMs() - m.lastTimeInMs < m.gridScrollAnimation.duration * 500)
return true
end if
end if
m.top.rowItemFocusIndex = [rowIndex - 1, rowItemIndex]
isHandled = true
m.lastTimeInMs = timeInMs
end if
end if
return isHandled
end function
'***********************************************************************
'* onKeyOK(isPressed, timeInMs)
'***********************************************************************
function onKeyOK(isPressed as boolean, timeInMs as longinteger) as boolean
isHandled = false
if (isPressed = true)
m.top.rowItemSelectedIndex = m.rowItemFocusIndex
isHandled = true
end if
return isHandled
end function
'***********************************************************************
'* onRowItemFocusIndexChanged()
'***********************************************************************
function onRowItemFocusIndexChanged()
m.rowItemFocusIndex = m.top.rowItemFocusIndex
itemFocusedIndex = m.rowItemFocusIndex[0] * 4 + m.rowItemFocusIndex[1]
newFocusedItem = m.resultGridWrapper.getChild(itemFocusedIndex)
if(m.top.isInFocusChain())
newFocusedItem.setFocus(true)
if (m.displayedColumn = m.rowItemFocusIndex[1])
if (m.rowItemFocusIndex[0] > m.displayedRows[0])
m.displayedRows = [m.rowItemFocusIndex[0] - 1, m.rowItemFocusIndex[0]]
handleGridScrollAnimation()
else if (m.rowItemFocusIndex[0] < m.displayedRows[1])
m.displayedRows = [m.rowItemFocusIndex[0], m.rowItemFocusIndex[0] + 1]
handleGridScrollAnimation(true)
end if
end if
m.displayedColumn = m.rowItemFocusIndex[1]
end if
end function
'***********************************************************************
'* handleGridScrollAnimation()
'***********************************************************************
function handleGridScrollAnimation(reverse = false as boolean)
yItemSpacing = m.top.itemSpacings[1]
offsetY = yItemSpacing + m.top.itemSize[1]
m.gridScrollAnimation.duration = 0.5
if (reverse = true) offsetY = -offsetY
nextTranslation = [0, m.lastTranslation[1] - offsetY]
m.gridScrollAnimationInterpolator.keyValue = [m.resultGridWrapper.translation, nextTranslation]
m.gridScrollAnimation.control = "start"
m.lastTranslation = nextTranslation
m.lastReverse = reverse
end function
'*************************************************************
'** caculateRowIndex()
'*************************************************************
function caculateRowIndex(index as integer) as integer
numColumns = m.top.numColumns
if (Int((index + 1) / numColumns) = (index + 1) / numColumns)
rowIndex = (index + 1) / numColumns
else
rowIndex = Int((index + 1) / numColumns) + 1
end if
return rowIndex - 1
end function
'***********************************************************************
'* onRowItemSelected()
'***********************************************************************
function onRowItemSelected() as void
content = m.top.content
numColumns = m.top.numColumns
if (content <> invalid)
rowItemSelected = m.top.rowItemSelectedIndex
item = content.getChild(numColumns * rowItemSelected[0] + rowItemSelected[1])
if (item <> invalid)
m.top.selectedItem = item
m.top.selectedItem = invalid
end if
end if
stop
end function
function onKeyEvent(key as string, isPressed as boolean) as boolean
isHandled = false
if(isPressed = true)
currentTime = getCurrentTimeInMs()
if(key = "up")
return onKeyUp(isPressed, currentTime)
else if(key = "down")
return onKeyDown(isPressed, currentTime)
else if(key = "left")
return onKeyLeft(isPressed, currentTime)
else if(key = "right")
return onKeyRight(isPressed, currentTime)
else if(key = "ok")
return onKeyOK(isPressed, currentTime)
end if
end if
return isHandled
end function
' utility functions
'******************************************************************************************
'* getCurrentTimeInMs()
'* Return the current time in miliseconds
'******************************************************************************************
function getCurrentTimeInMs() as longinteger
dt = getSystemDateTime()
inSec = dt.AsSeconds()
inMs = inSec * 1000 + dt.GetMilliseconds()
return inMs
end function
'******************************************************************************************
'* getSystemDateTime()
'* Return the roDateTime object to use
'******************************************************************************************
function getSystemDateTime() as object
if (m.systemDateTime = invalid)
m.systemDateTime = CreateObject("roDateTime")
else
' Update to current time
m.systemDateTime.Mark()
end if
return m.systemDateTime
end function
ChannelItem.xml
<?xml version="1.0" encoding="utf-8"?>
<component name="ChannelItem" extends="Group">
<interface>
<field id="width" type="float" />
<field id="height" type="float" />
<field id="itemContent" type="node" onChange="onItemContentChanged" />
<field id="itemHasFocus" type="boolean" onChange="onItemHasFocusChanged" />
<field id="scaleFactor" type="vector2d" onChange="onScaleFactorChanged" />
</interface>
<script type="text/brightscript" uri="./ChannelItem.brs" />
<children>
<Group id="maskGroup" scaleRotateCenter="[210, 189]">
<MaskGroup maskUri="pkg://images/item-mask-hd.png" maskSize="[300,250]">
<Poster id="background" uri="pkg://images/swimlane-background.png" blendColor="0x191919" width="300" height="250" />
<Poster id="poster" width="300" height="170" loadWidth="300" loadHeight="170" loadDisplayMode="scaleToZoom" />
</MaskGroup>
</Group>
<Poster id="logo" width="30" height="30" translation="[16, 200]"/>
<Label id="titleLabel" lineSpacing="0" wrap="true" maxLines="2" width="260" translation="[125, 190]" />
</children>
</component>
ChannelItem.brs
'******************************************************************************************
'* File: ChannelItem.brs
'******************************************************************************************
'***********************************************************************
'* init()
'***********************************************************************
function init() as void
m.lastFocusPercent = 0
m.scale = [0, 0]
m.backgroundColor = "0x191919"
m.focusBackgroundColor = "0x385DF7"
m.titleLabel = m.top.findNode("titleLabel")
m.maskGroup = m.top.findNode("maskGroup")
m.background = m.top.findNode("background")
m.poster = m.top.findNode("poster")
m.logo = m.top.findNode("logo")
m.progressBar = m.top.findNode("progressBar")
m.background.blendColor = m.backgroundColor
m.top.observeField("focusedChild", "onFocusedChildChanged")
end function
'***********************************************************************
'* onItemContentChanged()
'***********************************************************************
function onItemContentChanged() as void
content = m.top.itemContent
' here, random image url is used, You can collect url from m.top.itemContent
random = str(Rnd(200)).trim() 'random number 0 to 200
defaultImgUrl = "https://picsum.photos/id/"+random+"/420/219"
if(content.logoUrl <> invalid and content.logoUrl <> "")
m.logo.uri = content.logoUrl
else
m.logo.uri = defaultImgUrl
endif
m.titleLabel.text = content.title
if(content.imageUrl <> invalid and content.imageUrl <> "")
m.poster.uri = content.imageUrl
else
m.poster.uri = defaultImgUrl
endif
end function
'***********************************************************************
'* onItemHasFocusChanged()
'***********************************************************************
function onItemHasFocusChanged() as void
if (m.top.itemHasFocus)
m.background.blendColor = m.focusBackgroundColor
else
m.background.blendColor = m.backgroundColor
end if
end function
'***********************************************************************
'* onFocusedChildChanged()
'***********************************************************************
function onFocusedChildChanged() as void
m.top.itemHasFocus = m.top.hasFocus()
end function
'***********************************************************************
'* onScaleFactorChanged()
'***********************************************************************
function onScaleFactorChanged() as void
m.maskGroup.scale = m.top.scaleFactor
end function