ListView
API Reference: UI.ListView
ListView is a View that displays given items as a one-column vertical list. Interaction with each item in the list is possible.
- If ListViewItem's are created from UI Editor, you shouldn't be returning 0 in onRowType method.
Define your Listview calback functions before Page.onShow
You can define them in Page.onLoad
Usage of ListView
There are two ways to use ListView on Smartface
- Create the component skeleton in code, like creating your elements in Javascript
- [Recommended] Using Smartface UI Editor to create component skeleton and reuse it.
For more information about the difference of two, refer to the document below:
Using UI Editor and ClassesFor ease of showcase, the code examples here creates the necessary components directly from code. Smartface recommends to use Smartface UI Editor to use ListView. More information is located at the document below:
Using ListView and ListViewItem from Code
This method is not recommended when using 1 type of ListViewItem.
It is possible to create your ListView and ListViewItem using Page code. For this approach, you should use UI.ListView.onRowCreate method to create your layouts and UI.ListView.onRowBind to assign your data.
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import Font from "@smartface/native/ui/font";
import TextAlignment from "@smartface/native/ui/textalignment";
import PageTitleLayout from "components/PageTitleLayout";
import ListViewItem from "@smartface/native/ui/listviewitem";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
type DatasetType = { title: String; backgroundColor: Color };
const SPAN_COUNT: number = 1;
const COLORS: string[] = [
"#ffffff",
"#e6f7ff",
"#cceeff",
"#b3e6ff",
"#99ddff",
"#80d4ff",
"#66ccff",
"#4dc3ff",
"#33bbff",
"#1ab2ff",
"#00aaff",
"#0099e6",
"#0088cc",
"#0077b3",
"#006699",
];
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
myDataSet: DatasetType[] = Array.from({ length: 10 }).map((_, index: number) => ({
title: `Smartface Title ${index}`,
}));
constructor(private router?: Router, private route?: Route) {
super({});
}
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
const { myDataSet } = this;
this.headerBar.titleLayout = new PageTitleLayout();
this.myListView = new StyleableListView({
itemCount: myDataSet.length,
});
this.myListView.onRowCreate = () => {
let myListViewItem = new StyleableListViewItem();
let myLabelTitle = new StyleableLabel();
myLabelTitle.font = Font.create(Font.DEFAULT, 15, Font.BOLD);
myLabelTitle.textAlignment = TextAlignment.MIDCENTER;
myLabelTitle.textColor = Color.WHITE;
myLabelTitle.borderRadius = 10;
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `listViewItem${++this.index}`
});
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-listViewItem",
{
marginTop: 10,
marginBottom: 10,
marginLeft: 50,
marginRight: 50,
width: null,
flexProps: {
flexGrow: 1,
},
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
return myListViewItem;
};
this.myListView.onRowBind = (listViewItem, index) => {
// @ts-ignore
let myLabelTitle = listViewItem.myLabelTitle;
myLabelTitle.text = myDataSet[index].title;
myLabelTitle.backgroundColor = myDataSet[index].backgroundColor;
};
this.myListView.onRowSelected = (listViewItem, index) => {
console.log("selected index = " + index);
};
this.myListView.onPullRefresh = () => {
myDataSet.push({
title: "Smartface Title " + (myDataSet.length + 1),
backgroundColor: Color.RED,
});
this.myListView.itemCount = myDataSet.length;
this.myListView.refreshData();
this.myListView.stopRefresh();
};
this.addChild(
this.myListView,
"myListView",
".sf-listView",
(userProps) => {
userProps.height = 350;
userProps.rowHeight = 70;
return userProps;
}
);
this.dispatch({
type: "updateUserStyle",
userStyle: {
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
},
});
}
}
Scrolling to a ListViewItem Programmatically
API Reference: scrollTo
When using scrollTo method, you should check if ListView has enough data in it.
this.data = [{ text: "Smartface" }];
/**
* This may cause unexpected behavior since the listView doesn't have
enough length to scroll to.
*/
this.listView1.scrollTo(3);
// You should do it like this
if (this.data.length > 3) {
this.listView1.scrollTo(3);
}
Sample ListView Implementations
For ease of showcase, the code examples here creates the necessary components directly from code. Smartface recommends to use Smartface UI Editor to use ListView. More information is located at the document below:
Using Multiple ListViewItem's
For multiple ListViewItems, you cannot use Smartface UI Editor to drag\&drop your ListViewItem elements. You should specify which elements to choose from code. However, you can create your skeleton component from UI Editor and reference it by importing it.
import LviElements from "components/LviElements";
import LviItems from "components/LviItems";
// ...your page code.
this.listViewItemIndex = 0; // Initialize this in constructor
this.listView1.onRowCreate = (type) => {
let listViewItem = type === 0 ? new LviElements() : new LviItems();
this.listView1.dispatch({
type: 'addChild',
component: listViewItem,
name: `myListViewItem${++this.listViewItemIndex}`
});
// ...other necessary logic
return listViewItem;
};
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import Font from "@smartface/native/ui/font";
import TextAlignment from "@smartface/native/ui/textalignment";
import PageTitleLayout from "components/PageTitleLayout";
import ListViewItem from "@smartface/native/ui/listviewitem";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
type RowDataType = string[][];
type DataType = { isHeader: boolean; data: string };
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
headerData: string[] = [
"Complementary",
"Analogous",
"Tetradic",
"Monochromatic",
];
rowData: string[][] = [
["#ffb8c9", "#b8ffee"],
["#ffb8ed", "#ffb8c9", "#ffcbb8"],
["#eeb8ff", "#ffb8c9", "#c9ffb8", "#b8ffee"],
["#ff6c8f", "#ff85a2", "#ff9fb6", "#ffb8c9", "#ffd2dc", "#ffebf0"],
];
dataArray: DataType[] = this.pushDataToArray(this.headerData, this.rowData);
constructor(private router?: Router, private route?: Route) {
super({});
}
pushDataToArray(headerData: string[], rowData: RowDataType): DataType[] {
let dataArray: DataType[] = new Array<DataType>();
for (var i = 0; i < headerData.length; i++) {
dataArray.push({ isHeader: true, data: headerData[i] });
for (var j = 0; j < rowData[i].length; j++) {
dataArray.push({ isHeader: false, data: rowData[i][j] });
}
}
return dataArray;
}
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
const { dataArray } = this;
this.headerBar.titleLayout = new PageTitleLayout();
this.myListView = new StyleableListView({
itemCount: dataArray.length,
});
this.myListView.onRowCreate = (type) => {
let myListViewItem = new StyleableListViewItem();
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `myListViewItem${++this.index}`
});
if (type == 1) {
let myLabelTitle = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
flexProps: {
flexGrow: 1,
},
textAlignment: "MIDCENTER",
borderRadius: 10,
margin: 10,
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
} else {
// Header
let myLabelTitle = new Label();
myLabelTitle.font = Font.create(Font.DEFAULT, 30, Font.BOLD);
myLabelTitle.backgroundColor = Color.WHITE;
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
flexProps: {
flexGrow: 1,
},
borderRadius: 10,
margin: 10,
font: {
size: 30,
bold: true,
family: "SFProText",
style: "Semibold",
},
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
}
return myListViewItem;
};
this.myListView.onRowHeight = function (index) {
if (dataArray[index].isHeader) {
return 100;
}
return 70;
};
this.myListView.onRowBind = function (listViewItem, index) {
// @ts-ignore
const { myLabelTitle } = listViewItem;
if (dataArray[index].isHeader) {
myLabelTitle.text = dataArray[index].data;
} else {
myLabelTitle.backgroundColor = Color.create(dataArray[index].data);
myLabelTitle.text = dataArray[index].data;
}
};
this.myListView.onRowType = function (index) {
if (dataArray[index].isHeader) {
return 2;
} else {
return 1;
}
};
this.myListView.onPullRefresh = () => {
let header = ["Triadic"];
let row = [["#b8c9ff", "#ffb8c9", "#c9ffb8"]];
this.pushDataToArray(header, row);
this.myListView.itemCount = dataArray.length;
this.myListView.refreshData();
this.myListView.stopRefresh();
};
this.addChild(
this.myListView,
"myListView",
".sf-listView",
(userProps) => {
userProps.width = null;
userProps.height = null;
userProps.flexGrow = 1;
userProps.marginLeft = 20;
userProps.marginRight = 20;
userProps.marginTop = 50;
userProps.marginBottom = 50;
return userProps;
}
);
this.dispatch({
type: "updateUserStyle",
userStyle: {
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
},
});
}
}
Lazy Loading (Pagination)
Lazy loading of ListView is easy on Smartface Native Framework. It is possible with:
- onScroll event
- onRowBind event
Please note that onScroll event will be fired while user scrolling the ListView. This means while you downloading your content for lazy loading, onScroll event will continue to firing. You should check that. We recommend to use onRowBind for lazy loading to get over from this problem.
Lazy Loading Using onRowBind (recommended)
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import PageTitleLayout from "components/PageTitleLayout";
import ListViewItem from "@smartface/native/ui/listviewitem";
import Timer from "@smartface/native/global/timer";
import ActivityIndicator from "@smartface/native/ui/activityindicator";
import FlexLayout from "@smartface/native/ui/flexlayout";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableActivityIndicator extends styleableComponentMixin(ActivityIndicator) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
class StyleableFlexLayout extends styleableContainerComponentMixin(FlexLayout) {}
type DatasetType = {
title: String;
subtitle?: String;
backgroundColor?: Color;
};
const COLORS: string[] = [
"#ffffff",
"#e6f7ff",
"#cceeff",
"#b3e6ff",
"#99ddff",
"#80d4ff",
"#66ccff",
"#4dc3ff",
"#33bbff",
"#1ab2ff",
"#00aaff",
"#0099e6",
"#0088cc",
"#0077b3",
"#006699",
];
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
isLoading: boolean = false;
myDataSet: DatasetType[] = Array.from({ length: 10 }).map((_, index: number) => ({
title: `Smartface Title ${index}`,
subtitle: `Smartface Subtitle ${index}`,
}));
constructor(private router?: Router, private route?: Route) {
super({});
}
pushMoreToDataset(count: number): void {
for (let i = 0; i < count; i++) {
this.myDataSet.push({
title: "Smartface Title " + (this.myDataSet.length + 1),
subtitle: "Smartface Subitle " + (this.myDataSet.length + 1),
});
}
}
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
let { myDataSet, isLoading } = this;
this.headerBar.titleLayout = new PageTitleLayout();
this.myListView = new StyleableListView({
itemCount: myDataSet.length + 1,
onRowCreate: (type) => {
let myListViewItem = new StyleableListViewItem();
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `myListViewItem${++this.index}`,
classNames: ".sf-listViewItem",
userStyle: {
paddingTop: 5,
paddingBottom: 5
}
});
if (type == 2) {
// Loading
let loadingIndicator = new StyleableActivityIndicator();
//@ts-ignore
myListViewItem.loadingIndicator = loadingIndicator;
//@ts-ignore
myListViewItem.addChild(
loadingIndicator,
`loadingIndicator${this.index}`,
".sf-activityIndicator",
{
width: 35,
height: 35,
color: "#008000",
flexProps: {
positionType: "ABSOLUTE",
},
}
);
//@ts-ignore
myListViewItem.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
justifyContent: "CENTER",
alignItems: "CENTER",
},
},
});
} else {
let titleLayout = new StyleableFlexLayout();
let titleLabel = new StyleableLabel();
let subtitleLabel = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
titleLayout,
`titleLayout${this.index}`,
".sf-flexLayout",
{
flexGrow: 1,
backgroundColor: "#00A1F1",
}
);
//@ts-ignore
titleLayout.addChild(
titleLabel,
`titleLabel${this.index}`,
"sf-label",
{
textAlignment: "MIDCENTER",
flexGrow: 1,
textColor: "#FFFFFF",
}
);
//@ts-ignore
titleLayout.titleLabel = titleLabel;
//@ts-ignore
titleLayout.addChild(
subtitleLabel,
`subtitleLabel${this.index}`,
"sf-label",
{
textAlignment: "MIDCENTER",
flexGrow: 1,
textColor: "#FFFFFF",
}
);
//@ts-ignore
titleLayout.subtitleLabel = subtitleLabel;
//@ts-ignore
myListViewItem.titleLayout = titleLayout;
}
return myListViewItem;
},
onRowBind: (listViewItem, index) => {
if (index === myDataSet.length) {
listViewItem.loadingIndicator.visible = true;
} else {
listViewItem.titleLayout.titleLabel.text =
myDataSet[index % myDataSet.length].title;
listViewItem.titleLayout.subtitleLabel.text =
myDataSet[index % myDataSet.length].subtitle;
}
if (index > myDataSet.length - 3 && !isLoading) {
isLoading = true;
Timer.setTimeout({
task: () => {
// Loading completed
this.pushMoreToDataset(10);
this.myListView.itemCount = myDataSet.length + 1;
this.myListView.refreshData();
isLoading = false;
},
delay: 1500,
});
}
},
onPullRefresh: () => {
this.pushMoreToDataset(1);
this.myListView.itemCount = myDataSet.length + 1;
this.myListView.refreshData();
this.myListView.stopRefresh();
},
onRowType: (index) => {
if (myDataSet.length === index) {
// Loading
return 2;
} else {
return 1;
}
},
});
this.addChild(
this.myListView,
"myListView",
".sf-listView",
(userProps: any) => {
userProps.rowHeight = 100;
userProps.alignSelf = "CENTER";
userProps.width = null;
userProps.height = null;
userProps.flexGrow = 1;
userProps.marginLeft = 75;
userProps.marginRight = 75;
userProps.marginTop = 100;
userProps.marginBottom = 100;
return userProps;
}
);
this.dispatch({
type: "updateUserStyle",
userStyle: {
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
flexProps: {
justifyContent: "CENTER",
},
},
});
}
}
Lazy Loading Using onScroll
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import PageTitleLayout from "components/PageTitleLayout";
import ListViewItem from "@smartface/native/ui/listviewitem";
import Timer from "@smartface/native/global/timer";
import ActivityIndicator from "@smartface/native/ui/activityindicator";
import FlexLayout from "@smartface/native/ui/flexlayout";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableActivityIndicator extends styleableComponentMixin(ActivityIndicator) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
class StyleableFlexLayout extends styleableContainerComponentMixin(FlexLayout) {}
type DatasetType = {
title: String;
subtitle?: String;
backgroundColor?: Color;
};
const COLORS: string[] = [
"#ffffff",
"#e6f7ff",
"#cceeff",
"#b3e6ff",
"#99ddff",
"#80d4ff",
"#66ccff",
"#4dc3ff",
"#33bbff",
"#1ab2ff",
"#00aaff",
"#0099e6",
"#0088cc",
"#0077b3",
"#006699",
];
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
isLoading: boolean = false;
myDataSet: DatasetType[] = Array.from({ length: 10 }).map((_, index: number) => ({
title: `Smartface Title ${index}`,
subtitle: `Smartface Subtitle ${index}`,
}));
constructor(private router?: Router, private route?: Route) {
super({});
}
pushMoreToDataset(count: number): void {
for (let i = 0; i < count; i++) {
this.myDataSet.push({
title: "Smartface Title " + (this.myDataSet.length + 1),
subtitle: "Smartface Subitle " + (this.myDataSet.length + 1),
});
}
}
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
let { myDataSet } = this;
this.headerBar.titleLayout = new PageTitleLayout();
this.myListView = new StyleableListView({
itemCount: myDataSet.length + 1,
onRowCreate: (type) => {
let myListViewItem = new StyleableListViewItem();
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `myListViewItem${++this.index}`,
classNames: ".sf-listViewItem",
userStyle: {
paddingTop: 5,
paddingBottom: 5
}
});
if (type == 2) {
// Loading
let loadingIndicator = new StyleableActivityIndicator();
//@ts-ignore
myListViewItem.loadingIndicator = loadingIndicator;
//@ts-ignore
myListViewItem.addChild(
loadingIndicator,
`loadingIndicator${this.index}`,
".sf-activityIndicator",
{
width: 35,
height: 35,
color: "#008000",
flexProps: {
positionType: "ABSOLUTE",
},
}
);
//@ts-ignore
myListViewItem.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
justifyContent: "CENTER",
alignItems: "CENTER",
},
},
});
} else {
let titleLayout = new StyleableFlexLayout();
let titleLabel = new StyleableLabel();
let subtitleLabel = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
titleLayout,
`titleLayout${this.index}`,
".sf-flexLayout",
{
flexGrow: 1,
backgroundColor: "#00A1F1",
}
);
//@ts-ignore
titleLayout.addChild(
titleLabel,
`titleLabel${this.index}`,
"sf-label",
{
textAlignment: "MIDCENTER",
flexGrow: 1,
textColor: "#FFFFFF",
}
);
//@ts-ignore
titleLayout.titleLabel = titleLabel;
//@ts-ignore
titleLayout.addChild(
subtitleLabel,
`subtitleLabel${this.index}`,
"sf-label",
{
textAlignment: "MIDCENTER",
flexGrow: 1,
textColor: "#FFFFFF",
}
);
//@ts-ignore
titleLayout.subtitleLabel = subtitleLabel;
//@ts-ignore
myListViewItem.titleLayout = titleLayout;
}
return myListViewItem;
},
onRowBind: (listViewItem, index) => {
if (index === myDataSet.length) {
listViewItem.loadingIndicator.visible = true;
} else {
listViewItem.titleLayout.titleLabel.text =
myDataSet[index % myDataSet.length].title;
listViewItem.titleLayout.subtitleLabel.text =
myDataSet[index % myDataSet.length].subtitle;
}
},
onPullRefresh: () => {
this.pushMoreToDataset(1);
this.myListView.itemCount = myDataSet.length + 1;
this.myListView.refreshData();
this.myListView.stopRefresh();
},
onRowType: (index) => {
if (myDataSet.length === index) {
// Loading
return 2;
} else {
return 1;
}
},
onScroll: () => {
if (
this.myListView.getLastVisibleIndex() > myDataSet.length - 3 &&
!this.isLoading
) {
// Simulate loading like downloading internet.
this.isLoading = true;
setTimeout(() => {
console.log(" myListView onScroll loading 11");
// Loading completed
this.pushMoreToDataset(10);
this.myListView.itemCount = myDataSet.length + 1;
this.myListView.refreshData();
this.isLoading = false;
}, 1500);
}
},
});
this.addChild(
this.myListView,
"myListView",
".sf-listView",
(userProps: any) => {
userProps.rowHeight = 100;
userProps.alignSelf = "CENTER";
userProps.width = null;
userProps.height = null;
userProps.flexGrow = 1;
userProps.marginLeft = 75;
userProps.marginRight = 75;
userProps.marginTop = 100;
userProps.marginBottom = 100;
return userProps;
}
);
this.dispatch({
type: "updateUserStyle",
userStyle: {
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
flexProps: {
justifyContent: "CENTER",
},
},
});
}
}
Sticky Items
Some components within the page will appear constantly at the bottom of the page. As the user scrolls down, those items will remain within the same absolute position.
This is a simple example of Sticky Items made with ListView and FlexLayout.
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import PageTitleLayout from "components/PageTitleLayout";
import ListViewItem from "@smartface/native/ui/listviewitem";
import FlexLayout from "@smartface/native/ui/flexlayout";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
class StyleableFlexLayout extends styleableContainerComponentMixin(FlexLayout) {}
type RowDataType = Array<Array<string>>;
type DataType = { isHeader: boolean; data: string };
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
headerData: string[] = [
"Complementary",
"Analogous",
"Tetradic",
"Monochromatic",
];
rowData: Array<Array<string>> = [
["#ffb8c9", "#b8ffee"],
["#ffb8ed", "#ffb8c9", "#ffcbb8"],
["#eeb8ff", "#ffb8c9", "#c9ffb8", "#b8ffee"],
["#ff6c8f", "#ff85a2", "#ff9fb6", "#ffb8c9", "#ffd2dc", "#ffebf0"],
];
dataArray: DataType[] = this.pushDataToArray(this.headerData, this.rowData);
constructor(private router?: Router, private route?: Route) {
super({});
}
pushDataToArray(headerData: string[], rowData: RowDataType): DataType[] {
let dataArray: DataType[] = new Array<DataType>();
for (var i = 0; i < headerData.length; i++) {
dataArray.push({ isHeader: true, data: headerData[i] });
for (var j = 0; j < rowData[i].length; j++) {
dataArray.push({ isHeader: false, data: rowData[i][j] });
}
}
return dataArray;
}
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
const { dataArray } = this;
this.headerBar.titleLayout = new PageTitleLayout();
this.myListView = new StyleableListView({
itemCount: dataArray.length,
refreshEnabled: false,
});
this.myListView.onRowCreate = (type: number) => {
let myListViewItem = new StyleableListViewItem();
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `myListViewItem${++this.index}`
});
if (type == 1) {
let myLabelTitle = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
flexProps: {
flexGrow: 1,
},
textAlignment: "MIDCENTER",
borderRadius: 10,
margin: 10,
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
} else {
// Header
let myLabelTitle = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
flexProps: {
flexGrow: 1,
},
borderRadius: 10,
margin: 10,
backgroundColor: "#FFFFFF",
font: {
size: 30,
bold: true,
family: "SFProText",
style: "Semibold",
},
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
}
return myListViewItem;
};
this.myListView.onRowHeight = function (index: number) {
if (dataArray[index].isHeader) {
return 100;
}
return 70;
};
this.myListView.onRowBind = function (listViewItem: any, index: number) {
const { myLabelTitle } = listViewItem;
if (dataArray[index].isHeader) {
myLabelTitle.text = dataArray[index].data;
} else {
myLabelTitle.backgroundColor = Color.create(dataArray[index].data);
myLabelTitle.text = dataArray[index].data;
}
};
this.myListView.onRowType = (index: number): number => {
if (dataArray[index].isHeader) {
return 2;
} else {
return 1;
}
};
this.myListView.onScroll = (params: any): void => {
if (params.contentOffset.y >= 240) {
headerSticky.visible = true;
} else {
headerSticky.visible = false;
}
if (params.contentOffset.y + this.myListView.height >= 1030) {
footerSticky.visible = false;
} else {
footerSticky.visible = true;
}
};
this.addChild(
this.myListView,
"myListView",
".sf-listView",
(userProps) => {
userProps.width = null;
userProps.height = null;
userProps.flexGrow = 1;
userProps.marginLeft = 20;
userProps.marginRight = 20;
return userProps;
}
);
let headerSticky = new StyleableFlexLayout();
this.addChild(
headerSticky,
"headerSticky",
".sf-flexlayout",
(userProps: any) => {
userProps.visible = false;
userProps.height = 100;
userProps.left = 0;
userProps.right = 0;
userProps.top = 0;
userProps.positionType = "ABSOLUTE";
return userProps;
}
);
let myLabelTitle = new StyleableLabel({
text: "Analogous",
});
//@ts-ignore
headerSticky.addChild(
myLabelTitle,
"myLabelTitle",
".sf-label",
(userProps: any) => {
userProps.flexGrow = 1;
userProps.marginLeft = 30;
userProps.marginRight = 10;
userProps.font = {
size: 30,
bold: true,
family: "SFProText",
style: "Semibold",
};
userProps.backgroundColor = "#FFFFFF";
return userProps;
}
);
let footerSticky = new StyleableFlexLayout();
this.addChild(
footerSticky,
"footerSticky",
".sf-flexlayout",
(userProps: any) => {
userProps.visible = true;
userProps.height = 100;
userProps.left = 0;
userProps.right = 0;
userProps.bottom = 0;
userProps.positionType = "ABSOLUTE";
return userProps;
}
);
let myLabelTitle2 = new Label({
text: "Monochromatic",
});
//@ts-ignore
footerSticky.addChild(
myLabelTitle2,
"myLabelTitle2",
".sf-label",
(userProps: any) => {
userProps.flexGrow = 1;
userProps.marginLeft = 30;
userProps.marginRight = 10;
userProps.font = {
size: 30,
bold: true,
family: "SFProText",
style: "Semibold",
};
userProps.backgroundColor = "#FFFFFF";
return userProps;
}
);
this.dispatch({
type: "updateUserStyle",
userStyle: {
marginTop: 0,
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
},
});
}
}
The ListViewItem with Monochromatic text is a Sticky View.
Right and Left Swipe List Items
This is a simple example of creating a swipe items in ListView.
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basics- Page Code
- SwipeItem Style
import Page1Design from 'generated/pages/page1';
import { Route, Router } from '@smartface/router';
import { withDismissAndBackButton } from '@smartface/mixins';
import MyListViewItem from 'components/MyListViewItem';
import ListView from '@smartface/native/ui/listview';
import { themeService } from 'theme';
type DatasetType = { title: string };
export default class Page1 extends withDismissAndBackButton(Page1Design) {
private disposeables: (() => void)[] = [];
myDataSet: DatasetType[] = Array.from({ length: 10 }).map((_, index: number) => ({
title: `Smartface Title ${index}`,
}));
constructor(private router?: Router, private route?: Route) {
super({});
}
deleteAndRefresh(e: { index: number }): void {
this.myDataSet.splice(e.index, 1);
this.myListView.itemCount = this.myDataSet.length;
this.myListView.deleteRowRange({
itemCount: 1,
positionStart: e.index,
ios: {
animation: ListView.iOS.RowAnimation.FADE
}
});
this.myListView.refreshRowRange({
itemCount: 1,
positionStart: this.myDataSet.length - 1
});
}
/**
* @event onShow
* This event is called when a page appears on the screen (everytime).
*/
onShow() {
super.onShow();
this.myListView.itemCount = this.myDataSet.length;
this.myListView.refreshData();
}
/**
* @event onLoad
* This event is called once when page is created.
*/
onLoad() {
super.onLoad();
this.headerBar.leftItemEnabled = false;
this.myListView.onRowBind = (ListViewItem: MyListViewItem, index) => {
ListViewItem.myLabel.text = this.myDataSet[index].title;
};
this.myListView.onRowSwipe = (e) => {
const deleteItem = new ListView.SwipeItem() as StyleContextComponentType<ListView.SwipeItem>;
const editItem = new ListView.SwipeItem() as StyleContextComponentType<ListView.SwipeItem>;
const items: ListView.SwipeItem[] = [];
if (e.direction === ListView.SwipeDirection.RIGHTTOLEFT) {
deleteItem.text = 'Delete';
themeService.addGlobalComponent(deleteItem as any, `deleteItem-${e.index}`);
deleteItem.onPress = () => {
this.deleteAndRefresh(e);
};
deleteItem.dispatch({
type: 'pushClassNames',
classNames: '.swipeItem.delete'
});
items.push(deleteItem);
} else if (e.direction === ListView.SwipeDirection.LEFTTORIGHT) {
themeService.addGlobalComponent(editItem as any, `editItem-${e.index}`);
editItem.text = 'Edit';
editItem.onPress = () => {
// edit code here
};
editItem.dispatch({
type: 'pushClassNames',
classNames: '.swipeItem.edit'
});
items.push(editItem);
}
e.ios.expansionSettings.buttonIndex = 0;
return items;
};
this.myListView.onRowCanSwipe = (index: number) => {
return [ListView.SwipeDirection.RIGHTTOLEFT, ListView.SwipeDirection.LEFTTORIGHT];
};
this.myListView.swipeEnabled = true;
this.myListView.rowHeight = 70;
}
onHide(): void {
this.dispose();
}
dispose(): void {
this.disposeables.forEach((item) => item());
}
}
{
".swipeItem": {
"textColor": "#FFFFFF",
"font": {
"family": "SFProText",
"style": "Regular",
"bold": false,
"italic": false,
"size": 14
},
"ios": {
"iconTextSpacing": 5,
"padding": 30
},
".delete": {
"backgroundColor": "#FF0000"
},
".edit": {
"backgroundColor": "#0000FF"
}
}
}
Be sure that you set the backgroundColor of the ListViewItem to any color other than transparent. If you don't, the swipe item backgroundColor will overlap with the ListViewItem backgroundColor.
Drag and Drop ListViewItems
ListViewItems can be selected and moved to a different location.
A simple example for Drag & Drop feature.
The components in the example are added from the code for better showcase purposes. To learn more about the subject you can refer to:
Adding Component From CodeAs a best practice, Smartface recommends using the WYSIWYG editor in order to add components and styles to your page or library. To learn how to use UI Editor better, please refer to this documentation
UI Editor Basicsimport PageSampleDesign from "generated/pages/pageSample";
import Application from "@smartface/native/application";
import { Route, Router } from "@smartface/router";
import { styleableContainerComponentMixin, styleableComponentMixin } from '@smartface/styling-context';
import Label from "@smartface/native/ui/label";
import Color from "@smartface/native/ui/color";
import ListView from "@smartface/native/ui/listview";
import ListViewItem from "@smartface/native/ui/listviewitem";
class StyleableListView extends styleableComponentMixin(ListView) {}
class StyleableLabel extends styleableComponentMixin(Label) {}
class StyleableListViewItem extends styleableContainerComponentMixin(ListViewItem) {}
type RowDataType = Array<Array<string>>;
type DataType = { isHeader: boolean; data: string };
const HEADER_TYPE: number = 2;
const GENERAL_TYPE: number = 1;
//You should create new Page from UI-Editor and extend with it.
export default class Sample extends PageSampleDesign {
myListView: StyleableListView;
index: number = 0;
headerData: string[] = ["Complementary", "Analogous"];
rowData: Array<Array<string>> = [
["#eeb8ff", "#ffb8c9", "#c9ffb8", "#b8ffee"],
["#ff6c8f", "#ff85a2", "#ff9fb6", "#ffb8c9", "#ffd2dc", "#ffebf0"],
];
dataArray: DataType[] = this.pushDataToArray(this.headerData, this.rowData);
constructor(private router?: Router, private route?: Route) {
super({});
}
array_move(
arr: DataType[],
old_index: number,
new_index: number
): DataType[] {
if (new_index >= arr.length) {
var k = new_index - arr.length + 1;
while (k--) {
arr.push(undefined);
}
}
arr.splice(new_index, 0, arr.splice(old_index, 1)[0]);
return arr;
}
pushDataToArray(headerData: string[], rowData: RowDataType): DataType[] {
let dataArray: DataType[] = new Array<DataType>();
for (var i = 0; i < headerData.length; i++) {
dataArray.push({ isHeader: true, data: headerData[i] });
for (var j = 0; j < rowData[i].length; j++) {
dataArray.push({ isHeader: false, data: rowData[i][j] });
}
}
return dataArray;
}
indexOfSecondHeader = () =>
this.dataArray.findIndex((data) => data.data === "Analogous");
// The page design has been made from the code for better
// showcase purposes. As a best practice, remove this and
// use WYSIWYG editor to style your pages.
centerizeTheChildrenLayout() {
this.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: 'ROW',
justifyContent: 'CENTER',
alignItems: 'CENTER'
}
}
})
}
onShow() {
super.onShow();
const { headerBar } = this;
Application.statusBar.visible = false;
headerBar.visible = false;
}
onLoad() {
super.onLoad();
this.centerizeTheChildrenLayout();
this.myListView = new StyleableListView({
itemCount: this.dataArray.length,
rowMoveEnabled: true,
});
this.myListView.onRowCreate = (type: number) => {
let myListViewItem = new StyleableListViewItem();
this.myListView.dispatch({
type: 'addChild',
component: myListViewItem,
name: `myListViewItem${++this.index}`
});
if (type === GENERAL_TYPE) {
let myLabelTitle = new StyleableLabel();
let myEditableLbl = new StyleableLabel({
text: "-",
});
//@ts-ignore
myListViewItem.addChild(
myEditableLbl,
`myEditableLbl${this.index}`,
".sf-label",
{
width: 30,
height: 30,
backgroundColor: "#cc3300",
borderRadius: 15,
textColor: "#FFFFFF",
textAlignment: "MIDCENTER",
}
);
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
width: null,
height: null,
flexGrow: 1,
margin: 10,
textColor: "#FFFFFF",
borderRadius: 10,
textAlignment: "MIDCENTER",
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
//@ts-ignore
myListViewItem.myEditableLbl = myEditableLbl;
} else {
// Header
let myLabelTitle = new StyleableLabel();
//@ts-ignore
myListViewItem.addChild(
myLabelTitle,
`myLabelTitle${this.index}`,
".sf-label",
{
width: null,
height: null,
flexGrow: 1,
margin: 10,
backgroundColor: "#FFFFFF",
borderRadius: 15,
textAlignment: "MIDCENTER",
}
);
//@ts-ignore
myListViewItem.myLabelTitle = myLabelTitle;
}
//@ts-ignore
myListViewItem.dispatch({
type: "updateUserStyle",
userStyle: {
flexProps: {
flexDirection: "ROW",
alignItems: "CENTER",
},
},
});
return myListViewItem;
};
this.myListView.onRowHeight = (index: number): number => {
if (this.dataArray[index].isHeader) {
return 70;
}
return 40;
};
this.myListView.onRowBind = (listViewItem: ListViewItem, index: number) => {
//@ts-ignore
const { myLabelTitle, myEditableLbl } = listViewItem;
if (this.dataArray[index].isHeader) {
myLabelTitle.text = this.dataArray[index].data;
} else {
myLabelTitle.backgroundColor = Color.create(this.dataArray[index].data);
myLabelTitle.text = this.dataArray[index].data;
if (index > this.indexOfSecondHeader()) {
myEditableLbl.text = "+";
myEditableLbl.backgroundColor = Color.GREEN;
myEditableLbl.onTouch = (): boolean => {
console.info("TOUCH LABEL");
let index = this.myListView.indexByListViewItem(listViewItem);
let prevData = this.dataArray[index];
this.dataArray.splice(index, 1);
this.myListView.itemCount = this.dataArray.length;
this.myListView.deleteRowRange({
positionStart: index,
itemCount: 1,
ios: {
animation: ListView.iOS.RowAnimation.FADE,
},
});
this.dataArray.splice(this.indexOfSecondHeader(), 0, prevData);
this.myListView.itemCount = this.dataArray.length;
this.myListView.insertRowRange({
positionStart: this.indexOfSecondHeader() - 1,
itemCount: 1,
ios: {
animation: ListView.iOS.RowAnimation.FADE,
},
});
return false;
};
} else {
myEditableLbl.text = "-";
myEditableLbl.backgroundColor = Color.RED;
myEditableLbl.onTouch = (): boolean => {
let index = this.myListView.indexByListViewItem(listViewItem);
let prevData = this.dataArray[index];
this.dataArray.splice(index, 1);
this.myListView.itemCount = this.dataArray.length;
this.myListView.deleteRowRange({
positionStart: index,
itemCount: 1,
ios: {
animation: ListView.iOS.RowAnimation.FADE,
},
});
this.dataArray.splice(this.indexOfSecondHeader() + 1, 0, prevData);
this.myListView.itemCount = this.dataArray.length;
this.myListView.insertRowRange({
positionStart: this.indexOfSecondHeader() + 1,
itemCount: 1,
ios: {
animation: ListView.iOS.RowAnimation.FADE,
},
});
return false;
};
}
}
};
this.myListView.onRowType = (index: number): number => {
if (this.dataArray[index].isHeader) {
return HEADER_TYPE;
} else {
return GENERAL_TYPE;
}
};
this.myListView.onRowMoved = (source: number, dest: number) => {
this.dataArray = this.array_move(this.dataArray, source, dest);
};
this.myListView.onRowMove = (
sourceIndex: number,
desIndex: number
): boolean => {
if (this.dataArray[desIndex].isHeader || desIndex > this.indexOfSecondHeader()) {
return false;
}
return true;
};
this.myListView.onRowCanMove = (index: number) => {
if (!this.dataArray[index].isHeader && this.indexOfSecondHeader() > index) {
return true;
}
return false;
};
this.myListView.onPullRefresh = () => {
this.myListView.itemCount = this.dataArray.length;
this.myListView.refreshData();
this.myListView.stopRefresh();
};
this.addChild(this.myListView, "myListView", ".sf-listView", {
width: null,
height: null,
flexGrow: 1,
marginLeft: 20,
marginRight: 20,
marginTop: 15,
marginBottom: 15,
});
this.dispatch({
type: "updateUserStyle",
userStyle: {
paddingTop: 0,
paddingLeft: 0,
paddingBottom: 0,
paddingRight: 0,
},
});
}
}
Zebra Design
If you need to apply a Zebra Pattern, you should change the background of the ListViewItem in every odd(or even) index, depending on your design.
First, you should create a component on Smartface UI Editor to for your element. For this, a simple ListViewItem with one label inside is enough.
For Component Theme, baseTheme was used in this documentation. It is highly recommended to create your own theme and define your styles there. Refer to the document below for more information:
- Component Code
- Component UI
- Component Theme
import LviElementDesign from "generated/my-components/LviElement";
import { themeService } from "theme";
const wrapperClassName = ".lviElement-wrapper";
export default class LviElement extends LviElementDesign {
pageName?: string | undefined;
constructor(props?: any, pageName?: string) {
super(props);
this.pageName = pageName;
}
get keyText(): string {
return this.lblKey.text;
}
set keyText(key: string) {
this.lblKey.text = key;
}
get valueText(): string {
return this.lblValue.text;
}
set valueText(value: string) {
this.lblValue.text = value;
}
toggleZebra(isZebra = false) {
this.flElementWrapper.dispatch({
type: "pushClassNames",
classNames: isZebra ? `${wrapperClassName}-zebra` : `${wrapperClassName}`
});
}
static getHeight(): number {
return themeService.getStyle(".lviElement").height || 0;
}
}
{
"components": [
{
"className": ".sf-listViewItem .lviElement",
"hiddenComponent": false,
"id": "1841-ff9a-b968-028d",
"initialized": true,
"props": {
"children": [
"615a-baa9-4281-7fc7"
],
"name": "lviElement",
"parent": "57f4-201f-4bfc-5fc6"
},
"source": {
"page": "__library__"
},
"type": "ListViewItem",
"userProps": {}
},
{
"className": ".sf-flexLayout .lviElement-wrapper",
"id": "615a-baa9-4281-7fc7",
"props": {
"children": [
"ff05-f55e-a604-ed03",
"24ca-f21f-dfb6-5dc9",
"bd8f-5e00-4110-7fd7"
],
"name": "flElementWrapper",
"parent": "1841-ff9a-b968-028d",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "FlexLayout",
"userProps": {
"testId": "29yw8eN1o",
"usePageVariable": true
}
},
{
"className": ".sf-label .lviElement-title",
"id": "ff05-f55e-a604-ed03",
"props": {
"autoHeight": 51.086956521739125,
"autoWidth": 134.91847826086956,
"children": [],
"name": "lblKey",
"parent": "615a-baa9-4281-7fc7",
"text": "ListView Cell Title",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "Label",
"userProps": {
"autoHeight": 51.086956521739125,
"autoWidth": 134.91847826086956,
"text": "ListView Cell Title",
"usePageVariable": true
}
},
{
"className": ".sf-label .lviElement-title.value",
"id": "24ca-f21f-dfb6-5dc9",
"props": {
"autoHeight": 51.086956521739125,
"autoWidth": 134.91847826086956,
"children": [],
"name": "lblValue",
"parent": "615a-baa9-4281-7fc7",
"text": "ListView Cell Title",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "Label",
"userProps": {
"autoHeight": 51.086956521739125,
"autoWidth": 134.91847826086956,
"text": "ListView Cell Title"
}
},
{
"className": ".sf-flexLayout .lviElement-line",
"id": "bd8f-5e00-4110-7fd7",
"props": {
"children": [],
"name": "flLine",
"parent": "615a-baa9-4281-7fc7"
},
"source": {
"page": "__library__"
},
"type": "FlexLayout",
"userProps": {}
}
]
}
{
".lviElement": {
"height": 54,
"flexProps": {
"alignItems": "CENTER",
"flexDirection": "ROW"
},
"&-wrapper": {
"flexProps": {
"flexDirection": "ROW",
"flexGrow": 1
},
"backgroundColor": "rgba( 255, 255, 255, 1 )",
"height": 54,
"&-zebra": {
"backgroundColor": "rgba( 242, 242, 242, 0.8 )"
},
"paddingLeft": 16,
"paddingRight": 16,
"marginLeft": 0
},
"&-title": {
"flexProps": {
"positionType": "RELATIVE",
"alignSelf": "AUTO",
"flexGrow": 1
},
"font": {
"size": 15,
"bold": true,
"italic": false,
"family": "SFProText",
"style": "Semibold"
},
"height": 20,
"marginBottom": null,
"marginTop": 13,
"textAlignment": "MIDLEFT",
"textColor": "rgba( 0, 0, 0, 1 )",
"marginLeft": 0,
".value": {
"marginLeft": 16,
"textAlignment": "MIDRIGHT"
}
},
"&-line": {
"backgroundColor": "rgba( 235, 235, 235, 1 )",
"bottom": 0,
"flexProps": {
"positionType": "ABSOLUTE"
},
"height": 1,
"left": 0,
"marginBottom": null,
"marginTop": null,
"right": 0
}
}
}
After we have created our component, let's add it to the listView on our page
- Page Code
- Page UI
import PgZebraDesign from "generated/pages/pgZebra";
import LviElement from "components/LviElement";
import { Route, Router } from "@smartface/router";
export default class PgZebra extends PgZebraDesign {
elements = [...Array(20)].map((_, index) => ({
key: `Element${index}`,
value: `Element${index} Value`,
}));
constructor(private router?: Router, private route?: Route) {
super({});
}
initListView() {
this.lvElements.rowHeight = LviElement.getHeight();
this.lvElements.onRowBind = (listViewItem: LviElement, index: number) => {
listViewItem.keyText = this.elements[index].key;
listViewItem.valueText = this.elements[index].value;
listViewItem.toggleZebra(index % 2 !== 0);
};
this.lvElements.refreshEnabled = false;
}
refreshListView() {
this.lvElements.itemCount = this.elements.length;
this.lvElements.refreshData();
}
onShow() {
super.onShow();
this.refreshListView();
this.headerBar.title = "Zebra ListView";
}
onLoad() {
super.onLoad();
this.initListView();
}
}
{
"components": [
{
"className": ".sf-page",
"id": "1426-daf5-b788-d67c",
"initialized": true,
"props": {
"children": [
"f30c-1ac5-43e1-874f",
"2e36-e309-2da1-3a06",
"21b8-34c4-5bc7-6904"
],
"name": "pgZebra",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {}
},
{
"className": ".sf-statusBar",
"id": "f30c-1ac5-43e1-874f",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "1426-daf5-b788-d67c"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "2e36-e309-2da1-3a06",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "1426-daf5-b788-d67c",
"title": "pgZebra"
},
"type": "HeaderBar",
"userProps": {
"title": "pgZebra"
}
},
{
"className": ".sf-listView",
"id": "21b8-34c4-5bc7-6904",
"props": {
"children": [
"c27a-5b6b-d6b1-5b50"
],
"name": "lvElements",
"parent": "1426-daf5-b788-d67c",
"usePageVariable": true
},
"type": "ListView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "zF-RG7WAth",
"usePageVariable": true
}
},
{
"className": ".sf-listViewItem .lviElement",
"hiddenComponent": false,
"id": "c27a-5b6b-d6b1-5b50",
"initialized": true,
"props": {
"children": [],
"name": "lviElement",
"parent": "21b8-34c4-5bc7-6904"
},
"source": {
"page": "__library__",
"type": "lviElement",
"id": "1841-ff9a-b968-028d"
},
"type": "ListViewItem",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "f11PXCPU4",
"top": 0
}
}
]
}
Do not forget to add your new page to your router.
Smooth Search - iOS Search Behavior
Since Smartface is Native Cross-Platform Framework, you can directly use iOS search behavior but you need to access Native-API-Access and your code will only work for iOS. To make it work for both platforms, we need to mimic it using various UI manipulations.
Let's create our Smooth Search component first:
- Component Code
- Component UI
- Component Style
import SmoothSearchDesign from "generated/my-components/SmoothSearch";
import { themeService } from "theme";
import ListView from "@smartface/native/ui/listview";
export default class SmoothSearch extends SmoothSearchDesign {
pageName?: string | undefined;
static MaxSearchHeight: number =
themeService.getStyle(".smoothSearch").height || 0;
static InnerMaxSearchFlexHeight: number =
themeService.getStyle(".smoothSearch-inner").height || 0;
static InnerMaxSearchFlexBorderRadius: number =
themeService.getStyle(".smoothSearch-inner").borderRadius || 0;
private layoutHeight = SmoothSearch.MaxSearchHeight;
private innerHeight = SmoothSearch.InnerMaxSearchFlexHeight;
constructor(props?: any, pageName?: string) {
super(props);
this.pageName = pageName;
}
get searchBarHint(): string {
return this.searchBar.hint || "";
}
set searchBarHint(value: string) {
this.searchBar.hint = value || "";
}
/**
* This will be used on the page.
*/
onScroll(params: Parameters<ListView["onScroll"]>[0]) {
let alpha = 1;
let borderRadius = SmoothSearch.InnerMaxSearchFlexBorderRadius;
/**
* Normal scroll to bottom
*/
if (
params.contentOffset.y >= 0 &&
params.contentOffset.y < SmoothSearch.MaxSearchHeight
) {
this.layoutHeight = SmoothSearch.MaxSearchHeight - params.contentOffset.y;
this.innerHeight =
SmoothSearch.InnerMaxSearchFlexHeight - params.contentOffset.y;
borderRadius =
(this.innerHeight * SmoothSearch.InnerMaxSearchFlexBorderRadius) /
SmoothSearch.InnerMaxSearchFlexHeight;
} else if (params.contentOffset.y < 0) {
/**
* Overscroll to top
*/
if (this.layoutHeight !== SmoothSearch.MaxSearchHeight) {
this.layoutHeight = SmoothSearch.MaxSearchHeight;
this.innerHeight = SmoothSearch.InnerMaxSearchFlexHeight;
}
} else if (this.layoutHeight !== 0) {
/**
* Scrolled enough to cover the height of search
*/
this.layoutHeight = 0;
this.innerHeight = 0;
alpha = 0;
}
this.dispatch({
type: "updateUserStyle",
userStyle: {
height: this.layoutHeight,
alpha,
},
});
this.flInnerSearch.dispatch({
type: "updateUserStyle",
userStyle: {
height: this.innerHeight,
borderRadius,
},
});
this.searchBar.dispatch({
type: "updateUserStyle",
userStyle: {
alpha: (this.layoutHeight - SmoothSearch.InnerMaxSearchFlexHeight) / 10,
},
});
}
}
{
"components": [
{
"className": ".sf-flexLayout .smoothSearch",
"id": "27ad-7411-0826-c0ed",
"initialized": true,
"props": {
"children": [
"8409-eec3-8789-6461"
],
"name": "smoothSearch",
"parent": "57f4-201f-4bfc-5fc6"
},
"source": {
"page": "__library__"
},
"type": "FlexLayout",
"userProps": {}
},
{
"className": ".sf-flexLayout .smoothSearch-inner",
"id": "8409-eec3-8789-6461",
"props": {
"children": [
"baa5-2887-cf62-057b"
],
"name": "flInnerSearch",
"parent": "27ad-7411-0826-c0ed",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "FlexLayout",
"userProps": {
"usePageVariable": true
}
},
{
"className": ".sf-searchView .smoothSearch-searchView",
"id": "baa5-2887-cf62-057b",
"props": {
"children": [],
"hint": "Search",
"name": "searchBar",
"parent": "8409-eec3-8789-6461",
"usePageVariable": true
},
"source": {
"page": "__library__"
},
"type": "SearchView",
"userProps": {
"hint": "Search",
"usePageVariable": true
}
}
]
}
{
".smoothSearch": {
"flexProps": {
"positionType": "ABSOLUTE"
},
"backgroundColor": "rgba( 0, 161, 241, 1 )",
"height": 45,
"&-inner": {
"backgroundColor": "rgba( 245, 245, 245, 1 )",
"borderRadius": 10,
"height": 35,
"marginTop": 5
},
"paddingLeft": 10,
"paddingRight": 10,
"&-searchView": {
"flexProps": {
"positionType": "ABSOLUTE"
},
"left": 0,
"right": 0,
"bottom": -20,
"top": -20,
"ios": {
"searchViewStyle": "PROMINENT"
},
"backgroundColor": "rgba( 245, 245, 245, 1 )",
"textFieldBackgroundColor": "rgba( 245, 245, 245, 1 )",
"height": null
},
"alpha": 1,
"left": 0,
"right": 0,
"marginTop": 0
}
}
- Page Code
- Page UI
import PgSmoothScrollDesign from "generated/pages/pgSmoothScroll";
import LviElement from "components/LviElement";
import SmoothSearch from "components/SmoothSearch";
import System from "@smartface/native/device/system";
import { Route, Router } from "@smartface/router";
export default class pgSmoothScroll extends PgSmoothScrollDesign {
elements = [...Array(30)].map((_, i) => ({
key: `Smartface ${i}`,
value: ``,
}));
parentController: any;
constructor(private router?: Router, private route?: Route) {
super({});
}
initSearchView() {
this.smoothSearch.searchBarHint = "Search";
}
removeHeaderBorder() {
//Removes black line on iOS and sets elevation to 0 on android
if (System.OS === System.OSType.IOS) {
this.parentController.headerBar.borderVisibility = false;
} else {
this.headerBar.android.elevation = 0;
}
}
initListView() {
this.lvElements.rowHeight = LviElement.getHeight();
this.lvElements.refreshEnabled = false;
//contentInset can be set from UI edtior as well.
this.lvElements.contentInset = {
top: SmoothSearch.MaxSearchHeight,
bottom: 0,
};
this.lvElements.onRowBind = (listViewItem: LviElement, index: number) => {
listViewItem.keyText = this.elements[index].key;
listViewItem.valueText = this.elements[index].value;
};
this.lvElements.onScroll = (params) => this.smoothSearch.onScroll(params);
//@ts-ignore
this.lvElements.ios.onScrollEndDecelerating = (contentOffset) => {
if (
contentOffset.y > 0 &&
contentOffset.y < SmoothSearch.MaxSearchHeight
) {
const contentOffsetY =
contentOffset.y / SmoothSearch.MaxSearchHeight <= 0.5
? -SmoothSearch.MaxSearchHeight
: 0;
this.lvElements.nativeObject.setContentOffsetAnimated(
{ x: 0, y: contentOffsetY },
true
);
}
};
//@ts-ignore
this.lvElements.ios.onScrollEndDraggingWillDecelerate = (
contentOffset,
decelerate
) => {
if (!decelerate) {
if (
contentOffset.y > 0 &&
contentOffset.y < SmoothSearch.MaxSearchHeight
) {
const contentOffsetY =
contentOffset.y / SmoothSearch.MaxSearchHeight <= 0.5
? -SmoothSearch.MaxSearchHeight
: 0;
this.lvElements.nativeObject.setContentOffsetAnimated(
{ x: 0, y: contentOffsetY },
true
);
}
}
};
}
refreshListView() {
this.lvElements.itemCount = this.elements.length;
this.lvElements.refreshData();
}
onShow() {
super.onShow();
this.headerBar.title = "Smooth Scroll";
this.removeHeaderBorder();
this.refreshListView();
}
onLoad() {
super.onLoad();
this.initSearchView();
this.initListView();
}
}
{
"components": [
{
"className": ".sf-page",
"id": "0f08-29dc-e38d-7236",
"initialized": true,
"props": {
"children": [
"adb2-475e-6350-bed7",
"8f5d-c5b6-c991-7ac0",
"f4a4-5d27-c25b-d509",
"9282-28b3-d844-f412"
],
"name": "pgSmoothScroll",
"orientation": "PORTRAIT",
"parent": null
},
"type": "Page",
"userProps": {
"paddingLeft": 0,
"paddingRight": 0,
"paddingTop": 0
}
},
{
"className": ".sf-statusBar",
"id": "adb2-475e-6350-bed7",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "0f08-29dc-e38d-7236"
},
"type": "StatusBar",
"userProps": {
"visible": true
}
},
{
"className": ".sf-headerBar",
"id": "8f5d-c5b6-c991-7ac0",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "0f08-29dc-e38d-7236",
"title": "pgSmoothScroll"
},
"type": "HeaderBar",
"userProps": {
"title": "pgSmoothScroll"
}
},
{
"className": ".sf-listView",
"id": "f4a4-5d27-c25b-d509",
"props": {
"children": [
"d1f4-6f62-c98c-c660"
],
"name": "lvElements",
"parent": "0f08-29dc-e38d-7236",
"usePageVariable": true
},
"type": "ListView",
"userProps": {
"flexProps": {
"flexGrow": 1
},
"testId": "q0Ls07o-G",
"usePageVariable": true
}
},
{
"className": ".sf-listViewItem .sf-listViewItem-simple",
"hiddenComponent": false,
"id": "d1f4-6f62-c98c-c660",
"initialized": true,
"props": {
"children": [],
"name": "lviElement",
"parent": "f4a4-5d27-c25b-d509"
},
"source": {
"page": "__library__",
"type": "lviElement",
"id": "1841-ff9a-b968-028d"
},
"type": "ListViewItem",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "GCYhrFDlM",
"top": 0
}
},
{
"className": ".sf-flexLayout .smoothSearch",
"hiddenComponent": false,
"id": "9282-28b3-d844-f412",
"initialized": true,
"props": {
"children": [],
"name": "smoothSearch",
"parent": "0f08-29dc-e38d-7236",
"usePageVariable": true
},
"source": {
"page": "__library__",
"type": "smoothSearch",
"id": "27ad-7411-0826-c0ed"
},
"type": "FlexLayout",
"userProps": {
"testId": "29gzvt5T7",
"usePageVariable": true
}
}
]
}
After creating the component, you can drag & drop it to your newly created page.
Note that for ListView element, the LviElement component from previous Zebra Design was used. You can use any ListViewItem component, it is not related to Smooth Search implementation.
If you also want to use Zebra design together with smooth scroll, you can copy the onRowBind method from the previous example page code.
ListView Index (iOS Only)
With list view index, you can navigate to a specific item in the list.
import PgListViewIndexDesign from 'generated/pages/pgListViewIndex';
import FlexLayout from '@smartface/native/ui/flexlayout';
import ListView from '@smartface/native/ui/listview';
import ListViewItem from '@smartface/native/ui/listviewitem';
import Label from '@smartface/native/ui/label';
import Color from '@smartface/native/ui/color';
import Font from '@smartface/native/ui/font';
import System from '@smartface/native/device/system';
import { Route } from '@smartface/router';
import { withDismissAndBackButton } from '@smartface/mixins';
import { Router } from '@smartface/router';
import ListViewIndex from '@smartface/native/ui/listview/listviewindex';
export default class PgListViewIndex extends withDismissAndBackButton(PgListViewIndexDesign) {
listViewItemArray: any[] = [];
listViewItemIndexItems = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase().split('');
myListView: ListView;
headerIndex: number[] = [];
listViewIndex = new ListViewIndex();
constructor(private router?: Router, private route?: Route) {
super({});
this.layout.flexDirection = FlexLayout.FlexDirection.ROW;
}
initListView() {
var _rowData = [];
for (var i = 0; i < this.listViewItemIndexItems.length; i++) {
_rowData.push(['#ff6c8f', '#ff85a2', '#ff9fb6', '#ffb8c9', '#ffd2dc', '#ffebf0']);
}
const pushDataToArray = (headerData, rowData) => {
for (var i = 0; i < headerData.length; i++) {
this.listViewItemArray.push({ isHeader: true, data: headerData[i] });
this.headerIndex.push(this.listViewItemArray.length - 1);
for (var j = 0; j < rowData[i].length; j++) {
this.listViewItemArray.push({ isHeader: false, data: rowData[i][j] });
}
}
};
pushDataToArray(this.listViewItemIndexItems, _rowData);
this.myListView = new ListView({
flexGrow: 1,
marginLeft: 20,
itemCount: this.listViewItemArray.length
});
this.layout.addChild(this.myListView);
this.myListView.onRowCreate = (type) => {
var myListViewItem: ListViewItem & { myLabelTitle?: Label } = new ListViewItem();
if (type == 1) {
var myLabelTitle = new Label({
flexGrow: 1,
margin: 10
});
myLabelTitle.textColor = Color.WHITE;
myLabelTitle.borderRadius = 10;
myListViewItem.addChild(myLabelTitle);
myListViewItem.myLabelTitle = myLabelTitle;
} else {
// Header
var myLabelTitle = new Label({
flexGrow: 1,
margin: 10
});
myLabelTitle.font = Font.create(Font.DEFAULT, 30, Font.BOLD);
myLabelTitle.backgroundColor = Color.WHITE;
myListViewItem.addChild(myLabelTitle);
myListViewItem.myLabelTitle = myLabelTitle;
}
return myListViewItem;
};
this.myListView.onRowHeight = (index) => {
if (this.listViewItemArray[index].isHeader) {
return 100;
}
return 70;
};
this.myListView.onRowBind = (listViewItem, index) => {
//@ts-ignore
var myLabelTitle = listViewItem.myLabelTitle;
if (this.listViewItemArray[index].isHeader) {
myLabelTitle.text = typeof this.listViewItemArray[index].data === 'string' ? this.listViewItemArray[index].data : 'Image';
} else {
myLabelTitle.backgroundColor = Color.create(this.listViewItemArray[index].data);
myLabelTitle.text = this.listViewItemArray[index].data;
}
};
this.myListView.onRowType = (index) => (this.listViewItemArray[index].isHeader ? 2 : 1);
}
initListViewIndex() {
if (System.OS === System.OSType.ANDROID) {
return;
}
this.listViewIndex.width = 20;
this.listViewIndex.items = this.listViewItemIndexItems;
this.listViewIndex.indexDidSelect = (index) => {
this.myListView.scrollTo(this.headerIndex[index], true);
return true; //haptic
};
System.OS === System.OSType.IOS && this.listViewIndex.reloadData();
this.layout.addChild(this.listViewIndex);
}
onShow() {
super.onShow();
this.initBackButton(this.router);
}
onLoad() {
super.onLoad();
this.initListView();
this.initListViewIndex();
}
}