import observable = require("data/observable"); import definition = require("ui/list-view"); import common = require("./list-view-common"); import utils = require("utils/utils"); import view = require("ui/core/view"); import proxy = require("ui/core/proxy"); import dependencyObservable = require("ui/core/dependency-observable"); import color = require("color"); var CELLIDENTIFIER = "cell"; var ITEMLOADING = common.ListView.itemLoadingEvent; var LOADMOREITEMS = common.ListView.loadMoreItemsEvent; var ITEMTAP = common.ListView.itemTapEvent; var DEFAULT_HEIGHT = 44; global.moduleMerge(common, exports); var infinity = utils.layout.makeMeasureSpec(0, utils.layout.UNSPECIFIED); class ListViewCell extends UITableViewCell { public willMoveToSuperview(newSuperview: UIView): void { let parent: ListView = (this.view ? this.view.parent : null); // When inside ListView and there is no newSuperview this cell is // removed from native visual tree so we remove it from our tree too. if (parent && !newSuperview) { parent._removeContainer(this); } } public get view(): view.View { return this.owner ? this.owner.get() : null } public owner: WeakRef; } function notifyForItemAtIndex(listView: definition.ListView, cell: any, view: view.View, eventName: string, indexPath: NSIndexPath) { let args = { eventName: eventName, object: listView, index: indexPath.row, view: view, ios: cell, android: undefined }; listView.notify(args); return args; } class DataSource extends NSObject implements UITableViewDataSource { public static ObjCProtocols = [UITableViewDataSource]; private _owner: WeakRef; public static initWithOwner(owner: WeakRef): DataSource { let dataSource = DataSource.new(); dataSource._owner = owner; return dataSource; } public tableViewNumberOfRowsInSection(tableView: UITableView, section: number) { let owner = this._owner.get(); return (owner && owner.items) ? owner.items.length : 0; } public tableViewCellForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): UITableViewCell { // We call this method because ...ForIndexPath calls tableViewHeightForRowAtIndexPath immediately (before we can prepare and measure it). let cell = (tableView.dequeueReusableCellWithIdentifier(CELLIDENTIFIER) || ListViewCell.new()); let owner = this._owner.get(); if (owner) { owner._prepareCell(cell, indexPath); let cellView: view.View = cell.view; if (cellView) { // Arrange cell views. We do it here instead of _layoutCell because _layoutCell is called // from 'tableViewHeightForRowAtIndexPath' method too (in iOS 7.1) and we don't want to arrange the fake cell. let width = utils.layout.getMeasureSpecSize(owner.widthMeasureSpec); let rowHeight = owner._nativeView.rowHeight; let cellHeight = rowHeight > 0 ? rowHeight : owner.getHeight(indexPath.row); view.View.layoutChild(owner, cellView, 0, 0, width, cellHeight); } } return cell; } } class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate { public static ObjCProtocols = [UITableViewDelegate]; private _owner: WeakRef; private _measureCell: ListViewCell; public static initWithOwner(owner: WeakRef): UITableViewDelegateImpl { let delegate = UITableViewDelegateImpl.new(); delegate._owner = owner; return delegate; } public tableViewWillDisplayCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) { let owner = this._owner.get(); if (owner && (indexPath.row === owner.items.length - 1)) { owner.notify({ eventName: LOADMOREITEMS, object: owner }); } } public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath { let cell = tableView.cellForRowAtIndexPath(indexPath); let owner = this._owner.get(); if (owner) { notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath); } cell.highlighted = false; return indexPath; } public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number { let owner = this._owner.get(); if (!owner) { return 44; } let height = undefined; if (utils.ios.MajorVersion >= 8) { height = owner.getHeight(indexPath.row); } if (utils.ios.MajorVersion < 8 || height === undefined) { // in iOS 7.1 (or iOS8+ after call to scrollToRowAtIndexPath:atScrollPosition:animated:) this method is called before tableViewCellForRowAtIndexPath so we need fake cell to measure its content. let cell = this._measureCell; if (!cell) { this._measureCell = tableView.dequeueReusableCellWithIdentifier(CELLIDENTIFIER) || ListViewCell.new(); cell = this._measureCell; } height = owner._prepareCell(cell, indexPath); } return height; } } class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDelegate { public static ObjCProtocols = [UITableViewDelegate]; private _owner: WeakRef; public static initWithOwner(owner: WeakRef): UITableViewRowHeightDelegateImpl { let delegate = UITableViewRowHeightDelegateImpl.new(); delegate._owner = owner; return delegate; } public tableViewWillDisplayCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) { let owner = this._owner.get(); if (owner && (indexPath.row === owner.items.length - 1)) { owner.notify({ eventName: LOADMOREITEMS, object: owner }); } } public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath { let cell = tableView.cellForRowAtIndexPath(indexPath); let owner = this._owner.get(); if (owner) { notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath); } cell.highlighted = false; return indexPath; } public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number { let owner = this._owner.get(); if (!owner) { return DEFAULT_HEIGHT; } return owner.rowHeight; } } function onSeparatorColorPropertyChanged(data: dependencyObservable.PropertyChangeData) { var bar = data.object; if (!bar.ios) { return; } if (data.newValue instanceof color.Color) { bar.ios.separatorColor = (data.newValue).ios; } } // register the setNativeValue callbacks (common.ListView.separatorColorProperty.metadata).onSetNativeValue = onSeparatorColorPropertyChanged; export class ListView extends common.ListView { private _ios: UITableView; private _dataSource; private _delegate; private _heights: Array; private _preparingCell: boolean = false; private _isDataDirty: boolean = false; private _map: Map; widthMeasureSpec: number = 0; constructor() { super(); this._ios = new UITableView(); this._ios.registerClassForCellReuseIdentifier(ListViewCell.class(), CELLIDENTIFIER); this._ios.autoresizingMask = UIViewAutoresizing.UIViewAutoresizingNone; this._ios.estimatedRowHeight = DEFAULT_HEIGHT; this._ios.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this)); this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this)); this._heights = new Array(); this._map = new Map(); } public onLoaded() { super.onLoaded(); if (this._isDataDirty) { this.refresh(); } this._ios.delegate = this._delegate; } public onUnloaded() { this._ios.delegate = null; super.onUnloaded(); } get ios(): UITableView { return this._ios; } public scrollToIndex(index: number) { if (this._ios) { this._ios.scrollToRowAtIndexPathAtScrollPositionAnimated(NSIndexPath.indexPathForItemInSection(index, 0), UITableViewScrollPosition.UITableViewScrollPositionTop, false); } } public refresh() { if (this.isLoaded) { this._ios.reloadData(); this.requestLayout(); this._isDataDirty = false; } else { this._isDataDirty = true; } } public getHeight(index: number): number { return this._heights[index]; } public setHeight(index: number, value: number): void { this._heights[index] = value; } public _onRowHeightPropertyChanged(data: dependencyObservable.PropertyChangeData) { if (data.newValue < 0) { this._nativeView.rowHeight = UITableViewAutomaticDimension; this._nativeView.estimatedRowHeight = DEFAULT_HEIGHT; this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this)); } else { this._nativeView.rowHeight = data.newValue; this._nativeView.estimatedRowHeight = data.newValue; this._delegate = UITableViewRowHeightDelegateImpl.initWithOwner(new WeakRef(this)); } if (this.isLoaded) { this._nativeView.delegate = this._delegate; } super._onRowHeightPropertyChanged(data); } public requestLayout(): void { // When preparing cell don't call super - no need to invalidate our measure when cell desiredSize is changed. if (!this._preparingCell) { super.requestLayout(); } } public measure(widthMeasureSpec: number, heightMeasureSpec: number): void { this.widthMeasureSpec = widthMeasureSpec; var changed = this._setCurrentMeasureSpecs(widthMeasureSpec, heightMeasureSpec); super.measure(widthMeasureSpec, heightMeasureSpec); if (changed) { this._ios.reloadData(); } } private _layoutCell(cellView: view.View, indexPath: NSIndexPath): number { if (cellView) { var measuredSize = view.View.measureChild(this, cellView, this.widthMeasureSpec, infinity); var height = measuredSize.measuredHeight; this.setHeight(indexPath.row, height); return height; } return 0; } public _prepareCell(cell: ListViewCell, indexPath: NSIndexPath): number { let cellHeight: number; try { this._preparingCell = true; let view = cell.view; if (!view) { view = this._getItemTemplateContent(indexPath.row); } let args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath); view = args.view || this._getDefaultItemContent(indexPath.row); // If cell is reused be have old content - remove it first. if (!cell.view) { cell.owner = new WeakRef(view); } else if (cell.view !== view) { this._removeContainer(cell); (cell.view._nativeView).removeFromSuperview(); cell.owner = new WeakRef(view); } this._prepareItem(view, indexPath.row); this._map.set(cell, view); // We expect that views returned from itemLoading are new (e.g. not reused). if (view && !view.parent && view._nativeView) { cell.contentView.addSubview(view._nativeView); this._addView(view); } cellHeight = this._layoutCell(view, indexPath); } finally { this._preparingCell = false; } return cellHeight; } public _removeContainer(cell: ListViewCell): void { this._removeView(cell.view) this._map.delete(cell); } }