List control
Version 1.0 - 3. February 2017
Sourcecode
Please see the UgListControlDemo project for the source code created in this chapter. See Installing examples for information on how to download the examples from https://github.com/scadedoc/UgExamples/tree/master/UgListControlDemo
Introduction
Our list control is very powerful because it's very versatile. You can configure it to do many things.
- Its can contain list elements of different types
- Each list element can be composed of many other elements
- You can hide and show certain elements programmatically
- You can change a lot of the characteristics by simply binding it to control variables
- The list control uses native graphics as well as platform specific behaviour, i.e
- Controls within the list control such as label or button are mapped to its respective native iOS and Android platform
- Drag and drop & slide or any touch movement use the algorithms of the respective native iOS and Android platform and are native
Displaying an array in a list control
In this example, we want to display the first 100 most popular dog names (from 2020) together with a breed and id.
Add list control to page
- We use the contraints box to make sure it uses all the space available
Add controls to list template
We add the controls we want to see in each list element
- Each list control contains a list of list elements
- Each list element contains the same visual controls to display the data
- The list element is designed using the the list controls's list element template
- In this example, we added 3 test labels into a GridView
Define Data Model
- We use the Dog class to display a list of dog names and breeds
- We inherit from EObject
import ScadeKit
class Dog : EObject {
// please make sure you annotate each variable with the type
// for SCADE to more identify and leverage the type information
// currently, you must inherit from EObject
var id: String
var name: String
var ageInYears: Int
var breed: String
init(id: String, name: String, breed: String, ageInYears: Int) {
self.id = id
self.name = name
self.breed = breed
self.ageInYears = ageInYears
}
}
Bind data to controls
Bind the list elements visual control to the data
- In order to bind the view to the model, we use the SCDWidgetsElementProvider class
- Its set using .elementProvider
- You set an instance of SCDWidgetsElementProvider and add the logic that binds the data to the list element's visual controls
- The template parameter is of type SCDWidgetsGridView. Use the getWidgetByName method to retrieve a reference to the visual control and set the data from the Dog class instance:
class MainPageAdapter: SCDLatticePageAdapter {
var dogs: [Dog] = []
// page adapter initialization
override func load(_ path: String) {
super.load(path)
self.list1.elementProvider = SCDWidgetsElementProvider { (dog: Dog, template) in
(template.getWidgetByName("dogName") as! SCDWidgetsLabel).text = dog.name
(template.getWidgetByName("dogBreed") as! SCDWidgetsLabel).text = dog.breed
(template.getWidgetByName("dogId") as! SCDWidgetsLabel).text = dog.id
}
}
}
Set the data source of the control
- Define the source of data by setting the items property
- Its a collection of data objects
self.list1.items = Dogs().dogs
List Control lists data successfully
Add handler to process row click / item selected event
To add logic whenever a row is clicked,
- get a handle on the list control and cast the object to SCDWidgetsList
- add the event handler to the .onItemSelected
- The event handler is called SCDWidgetsItemSelectedEventHandler
// listen to itemSelected events
let listControl = self.page!.getWidgetByName("list1") as! SCDWidgetsList
listControl.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{ ev in self.rowClicked(event: ev!)})
- The event it fires is SCDWidgetsItemEvent
- The item is contained in a field called item
- Just cast it to the target structure
func rowClicked(event:SCDWidgetsItemEvent) {
if let dog = event.item as? Dog {
print("Hello \(dog.name)")
}
}
Entire class looks like this
import ScadeKit
class MainPageAdapter: SCDLatticePageAdapter {
@objc dynamic var dogs : [Dog] = []
// page adapter initialization
override func load(_ path: String) {
super.load(path)
self.setupDogs()
// listen to itemSelected events
let listControl = self.page!.getWidgetByName("list1") as! SCDWidgetsList
listControl.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{ ev in self.rowClicked(event: ev!)})
// Listen to action buttons
let listControlRowActionButtonRight = self.page!.getWidgetByName("btnListRowInfo") as! SCDWidgetsButton
listControlRowActionButtonRight.onClick.append(SCDWidgetsEventHandler{_ in print("Info button Clicked")})
// wire toolbar buttons
let groupedByButton = self.page!.getWidgetByName("itmGroupedByBreed") as! SCDWidgetsClickable
groupedByButton.onClick.append(SCDWidgetsEventHandler{_ in self.navigation!.go("groupedByBreed.page")})
}
func setupDogs() {
dogs.append(Dog(id:"d100",name:"Hector",breed:"Boxer",ageInYears:2))
dogs.append(Dog(id:"d101",name:"Max",breed:"Labrador", ageInYears:3))
dogs.append(Dog(id:"d102",name:"Bailey",breed:"St.Bernard", ageInYears:3))
}
func rowClicked(event:SCDWidgetsItemEvent) {
if let dog = event.item as? Dog {
print("Hello \(dog.name)")
}
}
}
Works nicely:
Add swipe support
Swipe support allows you to display custom areas containing buttons etc when you swipe the list row left and right. We describe adding a button to the right hand side of the listelement:
- Press the > arrow to open up the panel on the right
- You then can add any control, for instance a button that we will call btnListRowInfo with the title Info. We change background and font color:
All you need to do is to wire up the button. That's pretty easy:
let listControlRowActionButtonRight = self.page!.getWidgetByName("btnListRowInfo") as! SCDWidgetsButton
listControlRowActionButtonRight.onClick.append(SCDWidgetsEventHandler{_ in print("Info button Clicked")})
Add grouping separator to list control
Adding a separator row to indicate the start of a new set of data is a common use case:
We use the power and flexibility of the list control to add a seperator row. We insert two horizontal layout grids to the list element, and then add the labels indicating the group value and the data details in the respective grid:
The way to achieve this is to hide the elements that form the separator line when the dog's data need to be displayed, and hide the Dog data when the group view needs to be displayed. Therefore, we put all group separator related elements in viewGroup and all detail related elements into viewDetails.
In order to avoid having to code this using Swift code, we add two variables isGroup and isDetail that indicate if the item is a group separator or if it is a detail row:
import ScadeKit
class DogView : EObject {
// please make sure you annotate each variable with the type
// for SCADE to more identify and leverage the type information
let id : String
let name : String
let ageInYears : Int
let breed:String
// use to indicate if its a separator item or detail item
let isGroup:Bool
let isDetail:Bool
init(id:String,name:String,breed:String,ageInYears:Int) {
self.id = id
self.name = name
self.breed = breed
self.ageInYears = ageInYears
self.isGroup = false
self.isDetail = !self.isGroup
}
init(breed:String) {
self.id = ""
self.name = ""
self.breed = breed
self.ageInYears = -1
self.isGroup = true
self.isDetail = !self.isGroup
}
}
To hide any layout grid, we need to set two attributes, Layout.exclude and Layout.visible:
- we set Layout.exlude to true and
- we set Layout.visible to false
We do this using our binding editor:
Finally, we only need to supply the data item:
import ScadeKit
class GroupedByBreedPageAdapter: SCDLatticePageAdapter {
@objc dynamic var dogs : [DogView] = []
// page adapter initialization
override func load(_ path: String) {
super.load(path)
setupDogs()
let backbutton = self.page!.getWidgetByName("itmDoglist") as! SCDWidgetsClickable
backbutton.onClick.append(SCDWidgetsEventHandler{_ in self.navigation!.go("main.page")})
}
func setupDogs() {
dogs.append(DogView(breed:"Boxer"))
dogs.append(DogView(id:"d100",name:"Hector",breed:"Boxer",ageInYears:2))
dogs.append(DogView(breed:"Labrador"))
dogs.append(DogView(id:"d101",name:"Max",breed:"Labrador", ageInYears:3))
dogs.append(DogView(id:"d102",name:"Moritz",breed:"Labrador", ageInYears:3))
dogs.append(DogView(breed:"St.Bernard"))
dogs.append(DogView(id:"d103",name:"Bailey",breed:"St.Bernard", ageInYears:3))
}
}
Now, we grouped our dogs by breed, with full design control of the grouping row.
Create clicked animation effect on list element
This chapter explains on how to add a fade animation effect that visualizes that a row has been clicked. After clicking a row, the background color with turn to gray and that gray color with slowly fade:
- Create a new project and add a list control into the main page. Add a label into the list
- Create a reference to the list control and add an event handler when the list row is clicked
// create reference to list
let list1 = self.page!.getWidgetByName("list1") as! SCDWidgetsList
// populate with some items so that the list is not empty
list1.items = [1,2,3]
// add event handler when list row is clicked
list1.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{event in ... })
- Each list consists of a list of SCDWidgetsListElement that represent an individual list item. The element variable in the event contains the reference to the element
// reference the SCDWidgetsListElement that has been clicked
let listElement = event!.element as! SCDWidgetsListElement
- Now we change the list element's background color. Once the code is executed, the list element becomes gray
// set background color (yes, this API is wordy, we will improve it)
let backgroundStyleClass = SCDWidgetsBackgroundStyle()
let style = listElement.getStyle(backgroundStyleClass.eClass()) as! SCDWidgetsBackgroundStyle
style.type = SCDWidgetsBackgroundType.color
style.color = self.grayColor // previously defined
- Now the most important part. Get background rectangle inside list element:. Each list element is represented as a SVGGroup, which in turns contains a box which holds a rectangle. The rectangle is the correct element to modify to change the background:
// find the graphical area (the rectangle) that represents tha background
let drawingGroup = (listElement.drawing) as! SCDSvgGroup
let box = drawingGroup.drawableChilds[0] as! SCDSvgBox
let rect = box.drawableChilds[0] as! SCDSvgRect
- Finally, we add an animation that changes the opacity, as such giving a fading effect. (Alternatively, you could have changed the color itself). The animation guide contains a lot more details on animation:
// create animation to change the opacity. See animation guide for more details
let anim = SCDSvgPropertyAnimation("fillOpacity", values: [1, 0.5, 0])
anim.duration = 0.4
anim.repeatCount = 1
anim.delay = 0.2
Please see the full source code below
import ScadeKit
class MainPageAdapter: SCDLatticePageAdapter {
let grayColor = SCDGraphicsRGB(red: 211, green: 211, blue: 211)
// page adapter initialization
override func load(_ path: String) {
super.load(path)
// create reference to list
let list1 = self.page!.getWidgetByName("list1") as! SCDWidgetsList
// populate with some items so that the list is not empty
list1.items = [1,2]
// add event handler when list row is clicked
list1.onItemSelected.append(SCDWidgetsItemSelectedEventHandler{event in
// reference the SCDWidgetsListElement that has been clicked
let listElement = event!.element as! SCDWidgetsListElement
// set background color (yes, this API is wordy, we will improve it)
let backgroundStyleClass = SCDWidgetsBackgroundStyle()
let style = listElement.getStyle(backgroundStyleClass.eClass()) as! SCDWidgetsBackgroundStyle
style.type = SCDWidgetsBackgroundType.color
style.color = self.grayColor
// find the graphical area (the rectangle) that represents tha background
let drawingGroup = (listElement.drawing) as! SCDSvgGroup
let box = drawingGroup.drawableChilds[0] as! SCDSvgBox
let rect = box.drawableChilds[0] as! SCDSvgRect
// create animation to change the opacity. See animation guide for more details
let anim = SCDSvgPropertyAnimation("fillOpacity", values: [1, 0.5, 0])
anim.duration = 0.4
anim.repeatCount = 1
anim.delay = 0.2
// adding the animation starts it
rect.animations.append(anim)
})
}
}
Result:
Updated over 3 years ago