mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
fix(list-view): handle reusing wrong view (#9023)
This commit is contained in:
@@ -608,7 +608,7 @@ export class ListViewTest extends UITest<ListView> {
|
|||||||
|
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
// simulates Angular way of removing views
|
// simulates Angular way of removing views
|
||||||
(<any>listView)._realizedItems.forEach((view, nativeView, map) => {
|
(<any>listView)._realizedItems.forEach(({ view }, nativeView, map) => {
|
||||||
//console.log("view: " + view);
|
//console.log("view: " + view);
|
||||||
listView._removeView(view);
|
listView._removeView(view);
|
||||||
});
|
});
|
||||||
@@ -902,6 +902,29 @@ export class ListViewTest extends UITest<ListView> {
|
|||||||
TKUnit.assertEqual(secondNativeElementText, 'green', 'second element text');
|
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() {
|
public test_ItemTemplateSelector_TestVirtualization() {
|
||||||
let listView = this.testView;
|
let listView = this.testView;
|
||||||
listView.height = 300;
|
listView.height = 300;
|
||||||
|
|||||||
@@ -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) {
|
onItemClick<T extends android.widget.Adapter>(parent: android.widget.AdapterView<T>, convertView: android.view.View, index: number, id: number) {
|
||||||
const owner = this.owner;
|
const owner = this.owner;
|
||||||
const view = owner._realizedTemplates.get(owner._getItemTemplate(index).key).get(convertView);
|
const view = owner._realizedItems.get(convertView).view;
|
||||||
owner.notify({
|
owner.notify({
|
||||||
eventName: ITEMTAP,
|
eventName: ITEMTAP,
|
||||||
object: owner,
|
object: owner,
|
||||||
@@ -55,9 +55,69 @@ export class ListView extends ListViewBase {
|
|||||||
nativeViewProtected: android.widget.ListView;
|
nativeViewProtected: android.widget.ListView;
|
||||||
private _androidViewId = -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>>();
|
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
|
@profile
|
||||||
public createNativeView() {
|
public createNativeView() {
|
||||||
const listView = new android.widget.ListView(this._context);
|
const listView = new android.widget.ListView(this._context);
|
||||||
@@ -112,7 +172,7 @@ export class ListView extends ListViewBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate
|
// 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)) {
|
if (!(view.bindingContext instanceof Observable)) {
|
||||||
view.bindingContext = null;
|
view.bindingContext = null;
|
||||||
}
|
}
|
||||||
@@ -140,7 +200,7 @@ export class ListView extends ListViewBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public eachChildView(callback: (child: View) => boolean): void {
|
public eachChildView(callback: (child: View) => boolean): void {
|
||||||
this._realizedItems.forEach((view, nativeView) => {
|
this._realizedItems.forEach(({ view }, nativeView) => {
|
||||||
if (view.parent instanceof ListView) {
|
if (view.parent instanceof ListView) {
|
||||||
callback(view);
|
callback(view);
|
||||||
} else {
|
} else {
|
||||||
@@ -165,7 +225,7 @@ export class ListView extends ListViewBase {
|
|||||||
|
|
||||||
private clearRealizedCells(): void {
|
private clearRealizedCells(): void {
|
||||||
// clear the cache
|
// clear the cache
|
||||||
this._realizedItems.forEach((view, nativeView) => {
|
this._realizedItems.forEach(({ view }, nativeView) => {
|
||||||
if (view.parent) {
|
if (view.parent) {
|
||||||
// This is to clear the StackLayout that is used to wrap non LayoutBase & ProxyViewContainer instances.
|
// This is to clear the StackLayout that is used to wrap non LayoutBase & ProxyViewContainer instances.
|
||||||
if (!(view.parent instanceof ListView)) {
|
if (!(view.parent instanceof ListView)) {
|
||||||
@@ -176,6 +236,7 @@ export class ListView extends ListViewBase {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._realizedItems.clear();
|
this._realizedItems.clear();
|
||||||
|
this._availableViews.clear();
|
||||||
this._realizedTemplates.clear();
|
this._realizedTemplates.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,12 +363,16 @@ function ensureListViewAdapterClass() {
|
|||||||
// Recycle an existing view or create a new one if needed.
|
// Recycle an existing view or create a new one if needed.
|
||||||
const template = this.owner._getItemTemplate(index);
|
const template = this.owner._getItemTemplate(index);
|
||||||
let view: View;
|
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) {
|
if (convertView) {
|
||||||
view = this.owner._realizedTemplates.get(template.key).get(convertView);
|
view = this.owner._realizedItems.get(convertView).view;
|
||||||
if (!view) {
|
}
|
||||||
throw new Error(`There is no entry with key '${convertView}' in the realized views cache for template with key'${template.key}'.`);
|
|
||||||
}
|
if (!view) {
|
||||||
} else {
|
|
||||||
view = template.createView();
|
view = template.createView();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,14 +414,8 @@ function ensureListViewAdapterClass() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the view for recycling
|
this.owner._registerViewToTemplate(template.key, convertView, args.view);
|
||||||
let realizedItemsForTemplateKey = this.owner._realizedTemplates.get(template.key);
|
this.owner._markViewUsed(convertView);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertView;
|
return convertView;
|
||||||
|
|||||||
Reference in New Issue
Block a user