Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion apps/automated/src/ui/list-view/list-view-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ export class ListViewTest extends UITest<ListView> {

if (isAndroid) {
// simulates Angular way of removing views
(<any>listView)._realizedItems.forEach((view, nativeView, map) => {
(<any>listView)._realizedItems.forEach(({ view }, nativeView, map) => {
//console.log("view: " + view);
listView._removeView(view);
});
Expand Down Expand Up @@ -902,6 +902,29 @@ export class ListViewTest extends UITest<ListView> {
TKUnit.assertEqual(secondNativeElementText, 'green', 'second element text');
}

public test_ItemTemplateSelector_DoesNotThrowWhenItemsChangeMidRender() {
let listView = this.testView;
listView.height = 200;

listView.itemTemplates = this._itemTemplatesString;
listView.itemTemplateSelector = "age === 0 ? 'red' : 'green'";
listView.items = ListViewTest.generateItemsForMultipleTemplatesTests(4);
listView.items[0].age = 0;
listView.once('itemLoading', () => {
if (listView.items && listView.items.length) {
listView.items[0].age = listView.items[0].age !== 0 ? 0 : 1;
}
});
listView.refresh();
TKUnit.wait(0.1);
listView.scrollToIndex(1);
TKUnit.wait(0.1);
listView.scrollToIndex(2);
TKUnit.wait(0.1);
listView.scrollToIndex(3);
TKUnit.wait(0.1);
}

public test_ItemTemplateSelector_TestVirtualization() {
let listView = this.testView;
listView.height = 300;
Expand Down
119 changes: 89 additions & 30 deletions packages/core/ui/list-view/index.android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function initializeItemClickListener(): void {

onItemClick<T extends android.widget.Adapter>(parent: android.widget.AdapterView<T>, convertView: android.view.View, index: number, id: number) {
const owner = this.owner;
const view = owner._realizedTemplates.get(owner._getItemTemplate(index).key).get(convertView);
const view = owner._realizedItems.get(convertView).view;
owner.notify({
eventName: ITEMTAP,
object: owner,
Expand All @@ -53,11 +53,71 @@ function initializeItemClickListener(): void {

export class ListView extends ListViewBase {
nativeViewProtected: android.widget.ListView;
private _androidViewId: number = -1;
private _androidViewId = -1;

public _realizedItems = new Map<android.view.View, View>();
public _realizedItems = new Map<
android.view.View,
{
view: View;
templateKey: string;
}
>();
public _availableViews = new Map<string, Set<android.view.View>>();
public _realizedTemplates = new Map<string, Map<android.view.View, View>>();

private _ensureAvailableViews(templateKey: string) {
if (!this._availableViews.has(templateKey)) {
this._availableViews.set(templateKey, new Set());
}
}

public _registerViewToTemplate(templateKey: string, nativeView: android.view.View, view: View) {
this._realizedItems.set(nativeView, {
view,
templateKey,
});
if (!this._realizedTemplates.has(templateKey)) {
this._realizedTemplates.set(templateKey, new Map());
}
this._realizedTemplates.get(templateKey).set(nativeView, view);
this._ensureAvailableViews(templateKey);
const availableViews = this._availableViews.get(templateKey);
availableViews.add(nativeView);
}

public _markViewUsed(nativeView: android.view.View) {
const viewData = this._realizedItems.get(nativeView);
if (!viewData) {
throw new Error('View not registered');
}
this._ensureAvailableViews(viewData.templateKey);
this._availableViews.get(viewData.templateKey).delete(nativeView);
}
public _markViewUnused(nativeView: android.view.View) {
const viewData = this._realizedItems.get(nativeView);
if (!viewData) {
throw new Error('View not registered');
}
this._ensureAvailableViews(viewData.templateKey);
this._availableViews.get(viewData.templateKey).add(nativeView);
}
public _getKeyFromView(nativeView: android.view.View) {
return this._realizedItems.get(nativeView).templateKey;
}
public _hasAvailableView(templateKey: string) {
this._ensureAvailableViews(templateKey);
return this._availableViews.get(templateKey).size > 0;
}
public _getAvailableView(templateKey: string) {
this._ensureAvailableViews(templateKey);
if (!this._hasAvailableView(templateKey)) {
return null;
}
const view: android.view.View = this._availableViews.get(templateKey).values().next().value;
this._markViewUsed(view);
return view;
}

@profile
public createNativeView() {
const listView = new android.widget.ListView(this._context);
Expand Down Expand Up @@ -112,7 +172,7 @@ export class ListView extends ListViewBase {
}

// clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate
this._realizedItems.forEach((view, nativeView) => {
this._realizedItems.forEach(({ view }, nativeView) => {
if (!(view.bindingContext instanceof Observable)) {
view.bindingContext = null;
}
Expand Down Expand Up @@ -140,7 +200,7 @@ export class ListView extends ListViewBase {
}

public eachChildView(callback: (child: View) => boolean): void {
this._realizedItems.forEach((view, nativeView) => {
this._realizedItems.forEach(({ view }, nativeView) => {
if (view.parent instanceof ListView) {
callback(view);
} else {
Expand All @@ -165,7 +225,7 @@ export class ListView extends ListViewBase {

private clearRealizedCells(): void {
// clear the cache
this._realizedItems.forEach((view, nativeView) => {
this._realizedItems.forEach(({ view }, nativeView) => {
if (view.parent) {
// This is to clear the StackLayout that is used to wrap non LayoutBase & ProxyViewContainer instances.
if (!(view.parent instanceof ListView)) {
Expand All @@ -176,11 +236,12 @@ export class ListView extends ListViewBase {
});

this._realizedItems.clear();
this._availableViews.clear();
this._realizedTemplates.clear();
}

public isItemAtIndexVisible(index: number): boolean {
let nativeView = this.nativeViewProtected;
const nativeView = this.nativeViewProtected;
const start = nativeView.getFirstVisiblePosition();
const end = nativeView.getLastVisiblePosition();

Expand All @@ -191,7 +252,7 @@ export class ListView extends ListViewBase {
dividerHeight: number;
divider: android.graphics.drawable.Drawable;
} {
let nativeView = this.nativeViewProtected;
const nativeView = this.nativeViewProtected;

return {
dividerHeight: nativeView.getDividerHeight(),
Expand All @@ -206,7 +267,7 @@ export class ListView extends ListViewBase {
divider: android.graphics.drawable.Drawable;
}
) {
let nativeView = this.nativeViewProtected;
const nativeView = this.nativeViewProtected;
if (value instanceof Color) {
nativeView.setDivider(new android.graphics.drawable.ColorDrawable(value.android));
nativeView.setDividerHeight(1);
Expand Down Expand Up @@ -250,7 +311,7 @@ function ensureListViewAdapterClass() {

public getItem(i: number) {
if (this.owner && this.owner.items && i < this.owner.items.length) {
let getItem = (<ItemsSource>this.owner.items).getItem;
const getItem = (<ItemsSource>this.owner.items).getItem;

return getItem ? getItem.call(this.owner.items, i) : this.owner.items[i];
}
Expand All @@ -259,7 +320,7 @@ function ensureListViewAdapterClass() {
}

public getItemId(i: number) {
let item = this.getItem(i);
const item = this.getItem(i);
let id = i;
if (this.owner && item && this.owner.items) {
id = this.owner.itemIdGenerator(item, i, this.owner.items);
Expand All @@ -277,8 +338,8 @@ function ensureListViewAdapterClass() {
}

public getItemViewType(index: number) {
let template = this.owner._getItemTemplate(index);
let itemViewType = this.owner._itemTemplatesInternal.indexOf(template);
const template = this.owner._getItemTemplate(index);
const itemViewType = this.owner._itemTemplatesInternal.indexOf(template);

return itemViewType;
}
Expand All @@ -291,7 +352,7 @@ function ensureListViewAdapterClass() {
return null;
}

let totalItemCount = this.owner.items ? this.owner.items.length : 0;
const totalItemCount = this.owner.items ? this.owner.items.length : 0;
if (index === totalItemCount - 1) {
this.owner.notify({
eventName: LOADMOREITEMS,
Expand All @@ -300,18 +361,22 @@ function ensureListViewAdapterClass() {
}

// Recycle an existing view or create a new one if needed.
let template = this.owner._getItemTemplate(index);
const template = this.owner._getItemTemplate(index);
let view: View;
// convertView is of the wrong type
if (convertView && this.owner._getKeyFromView(convertView) !== template.key) {
this.owner._markViewUnused(convertView); // release this view
convertView = this.owner._getAvailableView(template.key); // get a view from the right type or null
}
if (convertView) {
view = this.owner._realizedTemplates.get(template.key).get(convertView);
if (!view) {
throw new Error(`There is no entry with key '${convertView}' in the realized views cache for template with key'${template.key}'.`);
}
} else {
view = this.owner._realizedItems.get(convertView).view;
}

if (!view) {
view = template.createView();
}

let args: ItemEventData = {
const args: ItemEventData = {
eventName: ITEMLOADING,
object: this.owner,
index: index,
Expand Down Expand Up @@ -341,22 +406,16 @@ function ensureListViewAdapterClass() {
this.owner._addView(args.view);
convertView = args.view.nativeViewProtected;
} else {
let sp = new StackLayout();
const sp = new StackLayout();
sp.addChild(args.view);
this.owner._addView(sp);

convertView = sp.nativeViewProtected;
}
}

// Cache the view for recycling
let realizedItemsForTemplateKey = this.owner._realizedTemplates.get(template.key);
if (!realizedItemsForTemplateKey) {
realizedItemsForTemplateKey = new Map<android.view.View, View>();
this.owner._realizedTemplates.set(template.key, realizedItemsForTemplateKey);
}
realizedItemsForTemplateKey.set(convertView, args.view);
this.owner._realizedItems.set(convertView, args.view);
this.owner._registerViewToTemplate(template.key, convertView, args.view);
this.owner._markViewUsed(convertView);
}

return convertView;
Expand Down