'xCustomListView v1.50
#Event: ItemClick (Index As Int, Value As Object)
#Event: ReachEnd
#Event: VisibleRangeChanged (FirstIndex As Int, LastIndex as Int)
#Event: ScrollChanged (Offset As Int)
#DesignerProperty: Key: DividerColor, DisplayName: Divider Color, FieldType: Color, DefaultValue: 0xFFD9D7DE
#DesignerProperty: Key: DividerHeight, DisplayName: Divider Height, FieldType: Int, DefaultValue: 2
#DesignerProperty: Key: PressedColor, DisplayName: Pressed Color, FieldType: Color, DefaultValue: 0xFF7EB4FA
#DesignerProperty: Key: InsertAnimationDuration, DisplayName: Insert Animation Duration, FieldType: Int, DefaultValue: 300
#DesignerProperty: Key: ListOrientation, DisplayName: List Orientation, FieldType: String, DefaultValue: Vertical, List: Vertical|Horizontal
Sub Class_Globals
Private mBase As B4XView
Public sv As B4XView
Type CLVItem(Panel As B4XView, Size As Int, Value As Object, _
Color As Int, TextItem As Boolean, Offset As Int)
Private items As List
Private DividerSize As Float
Private EventName As String
Private CallBack As Object
Public DefaultTextColor As Int
Public DefaultTextBackgroundColor As Int
Public AnimationDuration As Int = 300
Private LastReachEndEvent As Long
Private PressedColor As Int
Private xui As XUI
Private DesignerLabel As Label
Private horizontal As Boolean
#if B4J
Private fx As JFX
#else if B4A
Private su As StringUtils
#else if B4i
#end if
Private FirstVisibleIndex = -1, LastVisibleIndex = 0x7FFFFFFF As Int
Private MonitorVisibleRange As Boolean
Private FireScrollChanged As Boolean
End Sub
Public Sub Initialize (vCallBack As Object, vEventName As String)
EventName = vEventName
CallBack = vCallBack
items.Initialize
DefaultTextBackgroundColor = xui.Color_White
DefaultTextColor = xui.Color_Black
MonitorVisibleRange = xui.SubExists(CallBack, EventName & "_VisibleRangeChanged", 2)
FireScrollChanged = xui.SubExists(CallBack, EventName & "_ScrollChanged", 1)
End Sub
Public Sub DesignerCreateView (Base As Object, Lbl As Label, Props As Map)
mBase = Base
Dim o As String = Props.GetDefault("ListOrientation", "Vertical")
horizontal = o = "Horizontal"
sv = CreateScrollView
mBase.AddView(sv, 0, 0, mBase.Width, mBase.Height)
sv.ScrollViewInnerPanel.Color = xui.PaintOrColorToColor(Props.Get("DividerColor"))
DividerSize = DipToCurrent(Props.Get("DividerHeight")) 'need to scale the value
PressedColor = xui.PaintOrColorToColor(Props.Get("PressedColor"))
AnimationDuration = Props.GetDefault("InsertAnimationDuration", AnimationDuration)
DefaultTextColor = xui.PaintOrColorToColor(Lbl.TextColor)
DefaultTextBackgroundColor = mBase.Color
DesignerLabel = Lbl
Base_Resize(mBase.Width, mBase.Height)
End Sub
Private Sub Base_Resize (Width As Double, Height As Double)
sv.SetLayoutAnimated(0, 0, 0, Width, Height)
Dim scrollbar As Int
If xui.IsB4J Then scrollbar = 20
If horizontal Then
sv.ScrollViewContentHeight = Height - scrollbar
For Each it As CLVItem In items
it.Panel.Height = sv.ScrollViewContentHeight
it.Panel.GetView(0).Height = it.Panel.Height
Next
Else
sv.ScrollViewContentWidth = Width - scrollbar
For Each it As CLVItem In items
it.Panel.Width = sv.ScrollViewContentWidth
it.Panel.GetView(0).Width = it.Panel.Width
If it.TextItem Then
Dim lbl As B4XView = it.Panel.GetView(0).GetView(0)
lbl.SetLayoutAnimated(0, lbl.Left, lbl.Top, it.Panel.Width - lbl.Left, lbl.Height)
End If
Next
End If
If items.Size > 0 Then UpdateVisibleRange
End Sub
Public Sub AsView As B4XView
Return mBase
End Sub
'Clears all items.
Public Sub Clear
items.Clear
sv.ScrollViewInnerPanel.RemoveAllViews
SetScrollViewContentSize(0)
End Sub
Public Sub GetBase As B4XView
Return mBase
End Sub
'Returns the number of items.
Public Sub getSize As Int
Return items.Size
End Sub
Private Sub GetItem(Index As Int) As CLVItem
Return items.Get(Index)
End Sub
'Returns the Panel stored at the specified index.
Public Sub GetPanel(Index As Int) As B4XView
Return GetItem(Index).Panel.GetView(0)
End Sub
'Returns the value stored at the specified index.
Public Sub GetValue(Index As Int) As Object
Return GetItem(Index).Value
End Sub
'Removes the item at the specified index.
Public Sub RemoveAt(Index As Int)
If getSize <= 1 Then
Clear
Return
End If
Dim RemoveItem As CLVItem = items.Get(Index)
Dim p As B4XView
For i = Index + 1 To items.Size - 1
Dim item As CLVItem = items.Get(i)
p = item.Panel
p.Tag = i - 1
Dim NewOffset As Int = item.Offset - item.Size - DividerSize
SetItemOffset(item, NewOffset)
Next
SetScrollViewContentSize(GetScrollViewContentSize - RemoveItem.Size - DividerSize)
items.RemoveAt(Index)
RemoveItem.Panel.RemoveViewFromParent
End Sub
'Adds a text item. The item height will be adjusted based on the text.
Public Sub AddTextItem(Text As Object, Value As Object)
InsertAtTextItem(items.Size, Text, Value)
End Sub
'Inserts a text item at the specified index.
Public Sub InsertAtTextItem(Index As Int, Text As Object, Value As Object)
If horizontal Then
Log("AddTextItem is only supported in vertical orientation.")
Return
End If
Dim pnl As B4XView = CreatePanel("")
Dim lbl As B4XView = CreateLabel(Text)
lbl.Height = Max(50dip, lbl.Height)
pnl.AddView(lbl, 5dip, 2dip, sv.ScrollViewContentWidth - 10dip, lbl.Height)
lbl.TextColor = DefaultTextColor
pnl.Color = DefaultTextBackgroundColor
pnl.Height = lbl.Height + 2dip
InsertAt(Index, pnl, Value)
Dim item As CLVItem = GetItem(Index)
item.TextItem = True
End Sub
'Adds a custom item at the specified index.
Public Sub InsertAt(Index As Int, pnl As B4XView, Value As Object)
Dim size As Float
If horizontal Then
size = pnl.Width
Else
size = pnl.Height
End If
InsertAtImpl(Index, pnl, size, Value, 0)
End Sub
Private Sub InsertAtImpl(Index As Int, Pnl As B4XView, ItemSize As Int, Value As Object, InitialSize As Int)
'create another panel to handle the click event
Dim p As B4XView = CreatePanel("Panel")
p.Color = Pnl.Color
Pnl.Color = xui.Color_Transparent
If horizontal Then
p.AddView(Pnl, 0, 0, ItemSize, sv.ScrollViewContentHeight)
Else
p.AddView(Pnl, 0, 0, sv.ScrollViewContentWidth, ItemSize)
End If
p.Tag = Index
Dim IncludeDividierHeight As Int
If InitialSize = 0 Then IncludeDividierHeight = DividerSize Else IncludeDividierHeight = 0
Dim NewItem As CLVItem
NewItem.Panel = p
NewItem.Size = ItemSize
NewItem.Value = Value
NewItem.Color = p.Color
If Index = items.Size And InitialSize = 0 Then
items.Add(NewItem)
Dim offset As Int
If Index = 0 Then
offset = DividerSize
Else
offset = GetScrollViewContentSize
End If
NewItem.Offset = offset
If horizontal Then
sv.ScrollViewInnerPanel.AddView(p, offset, 0, ItemSize, sv.Height)
Else
sv.ScrollViewInnerPanel.AddView(p, 0, offset, sv.Width, ItemSize)
End If
Else
Dim offset As Int
If Index = 0 Then
offset = DividerSize
Else
Dim PrevItem As CLVItem = items.Get(Index - 1)
offset = PrevItem.Offset + PrevItem.Size + DividerSize
End If
For i = Index To items.Size - 1
Dim It As CLVItem = items.Get(i)
Dim NewOffset As Int = It.Offset + ItemSize - InitialSize + IncludeDividierHeight
If Min(NewOffset, It.Offset) - GetScrollViewOffset < GetScrollViewVisibleSize Then
It.Offset = NewOffset
If horizontal Then
It.Panel.SetLayoutAnimated(AnimationDuration, NewOffset, 0, It.Size, It.Panel.Height)
Else
It.Panel.SetLayoutAnimated(AnimationDuration, 0, NewOffset, It.Panel.Width, It.Size)
End If
Else
SetItemOffset(It, NewOffset)
End If
It.Panel.Tag = i + 1
Next
items.InsertAt(Index, NewItem)
NewItem.Offset = offset
If horizontal Then
sv.ScrollViewInnerPanel.AddView(p, offset, 0, InitialSize, sv.Height)
p.SetLayoutAnimated(AnimationDuration, offset, 0, ItemSize, p.Height)
Else
sv.ScrollViewInnerPanel.AddView(p, 0, offset, sv.Width, InitialSize)
p.SetLayoutAnimated(AnimationDuration, 0, offset, p.Width, ItemSize)
End If
End If
SetScrollViewContentSize(GetScrollViewContentSize + ItemSize - InitialSize + IncludeDividierHeight)
If items.Size = 1 Then SetScrollViewContentSize(GetScrollViewContentSize + IncludeDividierHeight)
If Index < LastVisibleIndex Or GetItem(LastVisibleIndex).Offset + _
GetItem(LastVisibleIndex).Size + DividerSize < GetScrollViewVisibleSize Then
UpdateVisibleRange
End If
End Sub
Private Sub UpdateVisibleRange
If MonitorVisibleRange = False Then Return
Dim first As Int = getFirstVisibleIndex
Dim last As Int = getLastVisibleIndex
If first <> FirstVisibleIndex Or last <> LastVisibleIndex Then
FirstVisibleIndex = first
LastVisibleIndex = last
CallSubDelayed3(CallBack, EventName & "_VisibleRangeChanged", FirstVisibleIndex, LastVisibleIndex)
End If
End Sub
Private Sub GetScrollViewVisibleSize As Float
If horizontal Then
Return sv.Width
Else
Return sv.Height
End If
End Sub
Private Sub GetScrollViewOffset As Float
If horizontal Then
Return sv.ScrollViewOffsetX
Else
Return sv.ScrollViewOffsetY
End If
End Sub
Private Sub SetScrollViewOffset(offset As Float)
If horizontal Then
sv.ScrollViewOffsetX = offset
Else
sv.ScrollViewOffsetY = offset
End If
End Sub
Private Sub GetScrollViewContentSize As Float
If horizontal Then
Return sv.ScrollViewContentWidth
Else
Return sv.ScrollViewContentHeight
End If
End Sub
Private Sub SetScrollViewContentSize(f As Float)
If horizontal Then
sv.ScrollViewContentWidth = f
Else
sv.ScrollViewContentHeight = f
End If
End Sub
Private Sub SetItemOffset (item As CLVItem, offset As Float)
item.Offset = offset
If horizontal Then
item.Panel.Left = offset
Else
item.Panel.Top = offset
End If
End Sub
'Changes the height of an existing item.
Public Sub ResizeItem(Index As Int, ItemHeight As Int)
Dim p As B4XView = GetPanel(Index)
Dim value As Object = items.Get(Index)
Dim parent As B4XView = p.Parent
p.Color = parent.Color
p.RemoveViewFromParent
ReplaceAt(Index, p, ItemHeight, value)
End Sub
'Replaces the item at the specified index with a new item.
Public Sub ReplaceAt(Index As Int, pnl As B4XView, ItemHeight As Int, Value As Object)
Dim RemoveItem As CLVItem = items.Get(Index)
items.RemoveAt(Index)
RemoveItem.Panel.RemoveViewFromParent
InsertAtImpl(Index, pnl, ItemHeight, Value, RemoveItem.Size)
End Sub
'Adds a custom item.
Public Sub Add(Pnl As B4XView, Value As Object)
InsertAt(items.Size, Pnl, Value)
End Sub
'Scrolls the list to the specified item (without animating the scroll).
Public Sub JumpToItem(Index As Int)
SetScrollViewOffset(FindItemOffset(Index))
End Sub
'Smoothly scrolls the list to the specified item.
Public Sub ScrollToItem(Index As Int)
#if B4J or B4I
JumpToItem(Index)
#Else If B4A
Dim offset As Float = FindItemOffset(Index)
If horizontal Then
Dim hv As HorizontalScrollView = sv
hv.ScrollPosition = offset 'smooth scroll
Else
Dim nsv As ScrollView = sv
nsv.ScrollPosition = offset
End If
#End If
End Sub
Private Sub FindItemOffset(Index As Int) As Int
Return GetItem(Index).Offset
End Sub
'Finds the index of the item (+ divider) based on the offset
Public Sub FindIndexFromOffset(Offset As Int) As Int
'the next divider is added to the current item
Dim Position As Int = items.Size / 2
Dim StepSize As Int = Ceil(Position / 2)
Do While True
Dim CurrentItem As CLVItem = items.Get(Position)
Dim NextOffset As Int
If Position < items.Size - 1 Then
NextOffset = GetItem(Position + 1).Offset - 1
Else
NextOffset = 0x7FFFFFFF
End If
Dim PrevOffset As Int
If Position = 0 Then
PrevOffset = 0x80000000
Else
PrevOffset = CurrentItem.Offset
End If
If Offset > NextOffset Then
Position = Min(Position + StepSize, items.Size - 1)
Else if Offset < PrevOffset Then
Position = Max(Position - StepSize, 0)
Else
Return Position
End If
StepSize = Ceil(StepSize / 2)
Loop
Return -1
End Sub
'Gets the index of the first visible item.
Public Sub getFirstVisibleIndex As Int
Return FindIndexFromOffset(GetScrollViewOffset + DividerSize)
End Sub
'Gets the index of the last visible item.
Public Sub getLastVisibleIndex As Int
Return FindIndexFromOffset(GetScrollViewOffset + GetScrollViewVisibleSize)
End Sub
Private Sub PanelClickHandler(SenderPanel As B4XView)
Dim clr As Int = GetItem(SenderPanel.Tag).Color
SenderPanel.SetColorAnimated(50, clr, PressedColor)
If xui.SubExists(CallBack, EventName & "_ItemClick", 2) Then
CallSub3(CallBack, EventName & "_ItemClick", SenderPanel.Tag, GetItem(SenderPanel.Tag).Value)
End If
Sleep(200)
SenderPanel.SetColorAnimated(200, PressedColor, clr)
End Sub
'Returns the index of the item that holds the given view.
Public Sub GetItemFromView(v As B4XView) As Int
Dim parent = v As Object, current As B4XView
Do While sv.ScrollViewInnerPanel <> parent
current = parent
parent = current.Parent
Loop
v = current
Return v.Tag
End Sub
Private Sub ScrollHandler
Dim position As Int = GetScrollViewOffset
If position + GetScrollViewVisibleSize >= GetScrollViewContentSize - 5dip And DateTime.Now > LastReachEndEvent + 100 Then
If xui.SubExists(CallBack, EventName & "_ReachEnd", 0) Then
LastReachEndEvent = DateTime.Now
CallSubDelayed(CallBack, EventName & "_ReachEnd")
Else
LastReachEndEvent = DateTime.Now + 1000 * DateTime.TicksPerDay 'disable
End If
End If
UpdateVisibleRange
If FireScrollChanged Then
CallSub2(CallBack, EventName & "_ScrollChanged", position)
End If
End Sub
Private Sub CreatePanel (PanelEventName As String) As B4XView
Return xui.CreatePanel(PanelEventName)
End Sub
'******************************* platform specific ***************************************************
#If B4A
Private Sub CreateScrollView As B4XView
If horizontal Then
Dim hv As HorizontalScrollView
hv.Initialize(0, "sv")
Return hv
Else
Dim nsv As ScrollView
nsv.Initialize2(0, "sv")
Return nsv
End If
End Sub
Private Sub Panel_Click
PanelClickHandler(Sender)
End Sub
Private Sub sv_ScrollChanged(Position As Int)
ScrollHandler
End Sub
Private Sub CreateLabel(txt As Object) As B4XView
Dim lbl As Label
lbl.Initialize("")
lbl.Gravity = DesignerLabel.Gravity
lbl.TextSize = DesignerLabel.TextSize
lbl.SingleLine = False
lbl.Text = txt
lbl.Width = sv.ScrollViewContentWidth - 10dip
lbl.Height = su.MeasureMultilineTextHeight(lbl, txt)
Return lbl
End Sub
#else If B4i
Private Sub CreateScrollView As B4XView
Dim nsv As ScrollView
nsv.Initialize("sv", 0, 0)
Return nsv
End Sub
Private Sub Panel_Click
PanelClickHandler(Sender)
End Sub
Sub sv_ScrollChanged (OffsetX As Int, OffsetY As Int)
ScrollHandler
End Sub
Private Sub CreateLabel(txt As String) As B4XView
Dim lbl As Label
lbl.Initialize("")
lbl.TextAlignment = DesignerLabel.TextAlignment
lbl.Font = DesignerLabel.Font
lbl.Multiline = True
lbl.Text = txt
lbl.Width = sv.ScrollViewContentWidth - 10dip
lbl.SizeToFit
Return lbl
End Sub
#else If B4J
Private Sub CreateScrollView As B4XView
Dim nsv As ScrollPane
nsv.Initialize("sv")
If horizontal Then
nsv.SetVScrollVisibility("NEVER")
Else
nsv.SetHScrollVisibility("NEVER")
End If
Return nsv
End Sub
Private Sub Panel_MouseClicked (EventData As MouseEvent)
PanelClickHandler(Sender)
End Sub
Private Sub sv_VScrollChanged (Position As Double)
ScrollHandler
End Sub
Private Sub sv_HScrollChanged (Position As Double)
ScrollHandler
End Sub
Private Sub CreateLabel(txt As String) As B4XView
Dim lbl As Label
lbl.Initialize("")
lbl.Alignment = DesignerLabel.Alignment
lbl.Style = DesignerLabel.Style
lbl.Font = DesignerLabel.Font
lbl.WrapText = True
lbl.Text = txt
Dim jo As JavaObject = Me
Dim width As Double = sv.ScrollViewContentWidth - 10dip
lbl.PrefHeight = 20dip + jo.RunMethod("MeasureMultilineTextHeight", Array(lbl.Font, txt, width))
Return lbl
End Sub
#if Java
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javafx.scene.text.Font;
import javafx.scene.text.TextBoundsType;
public double MeasureMultilineTextHeight(Font f, String text, double width) throws Exception {
Method m = Class.forName("com.sun.javafx.scene.control.skin.Utils").getDeclaredMethod("computeTextHeight",
Font.class, String.class, double.class, TextBoundsType.class);
m.setAccessible(true);
return (Double)m.invoke(null, f, text, width, TextBoundsType.LOGICAL);
}
#End If
#End If