Files
NativeScript/tns-core-modules/ui/list-view/list-view.ios.ts
Hristo Hristov af034089ca iOS Frame, Page and TabView measure/layout methods removed. We now rely on the framework positioning. This will result in a change that width, height, minWidth, minHeight, margins not respected on these controls
iOS layout positioning now respects native properties like automaticallyAdjustsScrollViewInsets, edgesForExtendedLayout, extendedLayoutIncludesOpaqueBars, navigationBar.translucent, tabBar.translucent
Removed frame-tests.ios.ts - those tests are now invalid
Added new layout tests inside page-tests.ios.ts
Commented few asserts in scroll-view-tests
View now expose ios namespace with layoutView method and UILayoutViewController used by page, tab-view and application module
ViewBase now expose viewController property that should be set from all widgets that are using viewcontrollers internally (like Page, Frame, TabView)
ViewBase now sets ios property to either the view returned from createNativeView or to nativeViewProptected
fragment.transitions now use animation/transition start to add fragments to waitingQueue. Before we did it manually in navigate/goBack. This way we can reuse the fragment.transition when calling showDialog. Also when animation/transition ends we check the animation/transition to see if this fragment should be set as current.
Frame expose new loadViewFromEntry method (to load a view from URI)
Frame navigation happens once frame is loaded
Frame now supports Page as a child in XML
Fixed GridLayout row, rowSpan, column, columnSpan properties type
Fixed bug in GridLayout where add/remove of columns/rows won't update the internal state of the grid (backport from android when GridLayout is recycled)
ListView will no longer invalidate layout when cell is removed
Fixed bug in ScrollView ios where effectiveMinWidth/Height was multiplied to density (it is already on device pixels so no need to multiply)
TabView android now calls loaded only on the selected child (not all)
Core refactoring
2017-12-15 13:06:34 +02:00

443 lines
16 KiB
TypeScript

import { ScrollEventData } from "../scroll-view";
import { ItemEventData } from ".";
import {
ListViewBase, View, KeyedTemplate, Length, Observable, Color,
separatorColorProperty, itemTemplatesProperty, layout, EventData
} from "./list-view-common";
import { StackLayout } from "../layouts/stack-layout";
import { ProxyViewContainer } from "../proxy-view-container";
import { ios } from "../../utils/utils";
import { profile } from "../../profiling";
import { device } from "../../platform";
export * from "./list-view-common";
const ITEMLOADING = ListViewBase.itemLoadingEvent;
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
const ITEMTAP = ListViewBase.itemTapEvent;
const DEFAULT_HEIGHT = 44;
const infinity = layout.makeMeasureSpec(0, layout.UNSPECIFIED);
class ListViewCell extends UITableViewCell {
public static initWithEmptyBackground(): ListViewCell {
const cell = <ListViewCell>ListViewCell.new();
// Clear background by default - this will make cells transparent
cell.backgroundColor = null;
return cell;
}
initWithStyleReuseIdentifier(style: UITableViewCellStyle, reuseIdentifier: string): this {
const cell = <this>super.initWithStyleReuseIdentifier(style, reuseIdentifier);
// Clear background by default - this will make cells transparent
cell.backgroundColor = null;
return cell;
}
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 {
return this.owner ? this.owner.get() : null
}
public owner: WeakRef<View>;
}
function notifyForItemAtIndex(listView: ListViewBase, cell: any, view: View, eventName: string, indexPath: NSIndexPath) {
let args = <ItemEventData>{ 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<ListView>;
public static initWithOwner(owner: WeakRef<ListView>): DataSource {
let dataSource = <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 owner = this._owner.get();
let cell: ListViewCell;
if (owner) {
let template = owner._getItemTemplate(indexPath.row);
cell = <ListViewCell>(tableView.dequeueReusableCellWithIdentifier(template.key) || ListViewCell.initWithEmptyBackground());
owner._prepareCell(cell, indexPath);
let cellView: View = cell.view;
if (cellView && cellView.isLayoutRequired) {
// 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 = layout.getMeasureSpecSize(owner.widthMeasureSpec);
let rowHeight = owner._effectiveRowHeight;
let cellHeight = rowHeight > 0 ? rowHeight : owner.getHeight(indexPath.row);
View.layoutChild(owner, cellView, 0, 0, width, cellHeight);
}
}
else {
cell = <ListViewCell>ListViewCell.initWithEmptyBackground();
}
return cell;
}
}
class UITableViewDelegateImpl extends NSObject implements UITableViewDelegate {
public static ObjCProtocols = [UITableViewDelegate];
private _owner: WeakRef<ListView>;
private _measureCellMap: Map<string, ListViewCell>;
public static initWithOwner(owner: WeakRef<ListView>): UITableViewDelegateImpl {
const delegate = <UITableViewDelegateImpl>UITableViewDelegateImpl.new();
delegate._owner = owner;
delegate._measureCellMap = new Map<string, ListViewCell>();
return delegate;
}
public tableViewWillDisplayCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) {
const owner = this._owner.get();
if (owner && (indexPath.row === owner.items.length - 1)) {
owner.notify(<EventData>{ eventName: LOADMOREITEMS, object: owner });
}
}
public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
const cell = <ListViewCell>tableView.cellForRowAtIndexPath(indexPath);
const owner = this._owner.get();
if (owner) {
notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath);
}
return indexPath;
}
public tableViewDidSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
tableView.deselectRowAtIndexPathAnimated(indexPath, true);
return indexPath;
}
public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number {
const owner = this._owner.get();
if (!owner) {
return DEFAULT_HEIGHT;
}
let height: number;
if (ios.MajorVersion >= 8) {
height = owner.getHeight(indexPath.row);
}
if (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.
const template = owner._getItemTemplate(indexPath.row);
let cell = this._measureCellMap.get(template.key);
if (!cell) {
cell = (<any>tableView.dequeueReusableCellWithIdentifier(template.key)) || ListViewCell.initWithEmptyBackground();
this._measureCellMap.set(template.key, cell);
}
height = owner._prepareCell(cell, indexPath);
}
return layout.toDeviceIndependentPixels(height);
}
}
class UITableViewRowHeightDelegateImpl extends NSObject implements UITableViewDelegate {
public static ObjCProtocols = [UITableViewDelegate];
private _owner: WeakRef<ListView>;
public static initWithOwner(owner: WeakRef<ListView>): UITableViewRowHeightDelegateImpl {
let delegate = <UITableViewRowHeightDelegateImpl>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(<EventData>{ eventName: LOADMOREITEMS, object: owner });
}
}
public tableViewWillSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
let cell = <ListViewCell>tableView.cellForRowAtIndexPath(indexPath);
let owner = this._owner.get();
if (owner) {
notifyForItemAtIndex(owner, cell, cell.view, ITEMTAP, indexPath);
}
return indexPath;
}
public tableViewDidSelectRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): NSIndexPath {
tableView.deselectRowAtIndexPathAnimated(indexPath, true);
return indexPath;
}
public tableViewHeightForRowAtIndexPath(tableView: UITableView, indexPath: NSIndexPath): number {
let owner = this._owner.get();
if (!owner) {
return DEFAULT_HEIGHT;
}
return owner._effectiveRowHeight;
}
}
// TODO: Remove this declaration when 'tns-platform-declarations' is update with iOS 11 declarations
declare const enum UIScrollViewContentInsetAdjustmentBehavior {
Automatic = 0,
ScrollableAxes = 1,
Never = 2,
Always = 3
}
export class ListView extends ListViewBase {
public _ios: UITableView;
private _dataSource;
private _delegate;
private _heights: Array<number>;
private _preparingCell: boolean;
private _isDataDirty: boolean;
private _map: Map<ListViewCell, View>;
widthMeasureSpec: number = 0;
constructor() {
super();
this.nativeViewProtected = this._ios = UITableView.new();
this._ios.registerClassForCellReuseIdentifier(ListViewCell.class(), this._defaultTemplate.key);
this._ios.estimatedRowHeight = DEFAULT_HEIGHT;
this._ios.rowHeight = UITableViewAutomaticDimension;
this._ios.dataSource = this._dataSource = DataSource.initWithOwner(new WeakRef(this));
if (parseInt(device.sdkVersion) >= 11) {
// TODO: Remove the cast to 'any' when 'tns-platform-declarations' is update with iOS 11 declarations
(<any>this._ios).contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
}
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
this._heights = new Array<number>();
this._map = new Map<ListViewCell, View>();
this._setNativeClipToBounds();
}
_setNativeClipToBounds() {
// Always set clipsToBounds for list-view
this._ios.clipsToBounds = true;
}
@profile
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;
}
get _childrenCount(): number {
return this._map.size;
}
public eachChildView(callback: (child: View) => boolean): void {
this._map.forEach((view, key) => {
callback(view);
});
}
public scrollToIndex(index: number) {
if (this._ios) {
this._ios.scrollToRowAtIndexPathAtScrollPositionAnimated(NSIndexPath.indexPathForItemInSection(index, 0),
UITableViewScrollPosition.Top, false);
}
}
public refresh() {
// clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate
this._map.forEach((view, nativeView, map) => {
if (!(view.bindingContext instanceof Observable)) {
view.bindingContext = null;
}
});
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(oldValue: Length, newValue: Length) {
const value = this._effectiveRowHeight;
const nativeView = this._ios;
if (value < 0) {
nativeView.rowHeight = UITableViewAutomaticDimension;
nativeView.estimatedRowHeight = DEFAULT_HEIGHT;
this._delegate = UITableViewDelegateImpl.initWithOwner(new WeakRef(this));
}
else {
nativeView.rowHeight = value;
nativeView.estimatedRowHeight = value;
this._delegate = UITableViewRowHeightDelegateImpl.initWithOwner(new WeakRef(this));
}
if (this.isLoaded) {
nativeView.delegate = this._delegate;
}
super._onRowHeightPropertyChanged(oldValue, newValue);
}
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, indexPath: NSIndexPath): number {
if (cellView) {
const rowHeight = this._effectiveRowHeight;
const heightMeasureSpec: number = rowHeight >= 0 ? layout.makeMeasureSpec(rowHeight, layout.EXACTLY) : infinity;
const measuredSize = View.measureChild(this, cellView, this.widthMeasureSpec, heightMeasureSpec);
const height = measuredSize.measuredHeight;
this.setHeight(indexPath.row, height);
return height;
}
return DEFAULT_HEIGHT;
}
public _prepareCell(cell: ListViewCell, indexPath: NSIndexPath): number {
let cellHeight: number;
try {
this._preparingCell = true;
let view = cell.view;
if (!view) {
view = this._getItemTemplate(indexPath.row).createView();
}
let args = notifyForItemAtIndex(this, cell, view, ITEMLOADING, indexPath);
view = args.view || this._getDefaultItemContent(indexPath.row);
// Proxy containers should not get treated as layouts.
// Wrap them in a real layout as well.
if (view instanceof ProxyViewContainer) {
let sp = new StackLayout();
sp.addChild(view);
view = sp;
}
// If cell is reused it have old content - remove it first.
if (!cell.view) {
cell.owner = new WeakRef(view);
} else if (cell.view !== view) {
this._removeContainer(cell);
(<UIView>cell.view.nativeViewProtected).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.nativeViewProtected) {
cell.contentView.addSubview(view.nativeViewProtected);
this._addView(view);
}
cellHeight = this._layoutCell(view, indexPath);
} finally {
this._preparingCell = false;
}
return cellHeight;
}
public _removeContainer(cell: ListViewCell): void {
let view = cell.view;
// This is to clear the StackLayout that is used to wrap ProxyViewContainer instances.
if (!(view.parent instanceof ListView)) {
this._removeView(view.parent);
}
// No need to request layout when we are removing cells.
const preparing = this._preparingCell;
this._preparingCell = true;
view.parent._removeView(view);
this._preparingCell = preparing;
this._map.delete(cell);
}
[separatorColorProperty.getDefault](): UIColor {
return this._ios.separatorColor;
}
[separatorColorProperty.setNative](value: Color | UIColor) {
this._ios.separatorColor = value instanceof Color ? value.ios : value;
}
[itemTemplatesProperty.getDefault](): KeyedTemplate[] {
return null;
}
[itemTemplatesProperty.setNative](value: KeyedTemplate[]) {
this._itemTemplatesInternal = new Array<KeyedTemplate>(this._defaultTemplate);
if (value) {
for (let i = 0, length = value.length; i < length; i++) {
this._ios.registerClassForCellReuseIdentifier(ListViewCell.class(), value[i].key);
}
this._itemTemplatesInternal = this._itemTemplatesInternal.concat(value);
}
this.refresh();
}
}