Files
NativeScript/tns-core-modules/ui/list-view/list-view.android.ts
Hristo Hristov 0f14101238 recycling now happens only if nativeView and android properties are not accessed. (#4627)
recycleNativeView filed now accepts: "always" | "never" | "auto". Always will recycle the nativeView no matter if its nativeView or android proprties are accessed. Never will disable recycling. Auto will recycle it only if nativeView and android properties are not accessed.
2017-08-01 15:04:16 +03:00

321 lines
11 KiB
TypeScript

import { ItemEventData, ItemsSource } from ".";
import {
ListViewBase, View, KeyedTemplate, Length, unsetValue, Observable, Color,
separatorColorProperty, itemTemplatesProperty
} from "./list-view-common";
import { StackLayout } from "../layouts/stack-layout";
import { ProxyViewContainer } from "../proxy-view-container";
import { LayoutBase } from "../layouts/layout-base";
import { profile } from "../../profiling";
export * from "./list-view-common";
const ITEMLOADING = ListViewBase.itemLoadingEvent;
const LOADMOREITEMS = ListViewBase.loadMoreItemsEvent;
const ITEMTAP = ListViewBase.itemTapEvent;
interface ItemClickListener {
new (owner: ListView): android.widget.AdapterView.OnItemClickListener;
}
let ItemClickListener: ItemClickListener;
function initializeItemClickListener(): void {
if (ItemClickListener) {
return;
}
@Interfaces([android.widget.AdapterView.OnItemClickListener])
class ItemClickListenerImpl extends java.lang.Object implements android.widget.AdapterView.OnItemClickListener {
constructor(public owner: ListView) {
super();
return global.__native(this);
}
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);
owner.notify({ eventName: ITEMTAP, object: owner, index: index, view: view });
}
}
ItemClickListener = ItemClickListenerImpl;
}
export class ListView extends ListViewBase {
nativeViewProtected: android.widget.ListView;
private _androidViewId: number = -1;
public _realizedItems = new Map<android.view.View, View>();
public _realizedTemplates = new Map<string, Map<android.view.View, View>>();
@profile
public createNativeView() {
initializeItemClickListener();
const listView = new android.widget.ListView(this._context);
listView.setDescendantFocusability(android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS);
this.updateEffectiveRowHeight();
// Fixes issue with black random black items when scrolling
listView.setCacheColorHint(android.graphics.Color.TRANSPARENT);
// listView.setId(this._androidViewId);
ensureListViewAdapterClass();
const adapter = new ListViewAdapterClass(this);
listView.setAdapter(adapter);
(<any>listView).adapter = adapter;
const itemClickListener = new ItemClickListener(this);
listView.setOnItemClickListener(itemClickListener);
(<any>listView).itemClickListener = itemClickListener;
return listView;
}
public initNativeView(): void {
super.initNativeView();
const nativeView: any = this.nativeViewProtected;
(<any>nativeView).itemClickListener.owner = this;
const adapter = (<any>nativeView).adapter;
adapter.owner = this;
nativeView.setAdapter(adapter);
if (this._androidViewId < 0) {
this._androidViewId = android.view.View.generateViewId();
}
nativeView.setId(this._androidViewId);
}
public disposeNativeView() {
const nativeView = this.nativeViewProtected;
nativeView.setAdapter(null);
(<any>nativeView).itemClickListener.owner = null;
(<any>nativeView).adapter.owner = null;
this.clearRealizedCells();
super.disposeNativeView();
}
public refresh() {
const nativeView = this.nativeViewProtected;
if (!nativeView || !nativeView.getAdapter()) {
return;
}
// clear bindingContext when it is not observable because otherwise bindings to items won't reevaluate
this._realizedItems.forEach((view, nativeView) => {
if (!(view.bindingContext instanceof Observable)) {
view.bindingContext = null;
}
});
(<android.widget.BaseAdapter>nativeView.getAdapter()).notifyDataSetChanged();
}
public scrollToIndex(index: number) {
const nativeView = this.nativeViewProtected;
if (nativeView) {
nativeView.setSelection(index);
}
}
get _childrenCount(): number {
return this._realizedItems.size;
}
public eachChildView(callback: (child: View) => boolean): void {
this._realizedItems.forEach((view, nativeView) => {
if (view.parent instanceof ListView) {
callback(view);
}
else {
// in some cases (like item is unloaded from another place (like angular) view.parent becomes undefined)
if (view.parent) {
callback(<View>view.parent);
}
}
});
}
public _dumpRealizedTemplates() {
console.log(`Realized Templates:`);
this._realizedTemplates.forEach((value, index) => {
console.log(`\t${index}:`);
value.forEach((value, index) => {
console.log(`\t\t${index.hashCode()}: ${value}`);
});
});
console.log(`Realized Items Size: ${this._realizedItems.size}`);
}
private clearRealizedCells(): void {
// clear the cache
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)) {
this._removeView(view.parent);
}
view.parent._removeView(view);
}
});
this._realizedItems.clear();
this._realizedTemplates.clear();
}
[separatorColorProperty.getDefault](): { dividerHeight: number, divider: android.graphics.drawable.Drawable } {
let nativeView = this.nativeViewProtected;
return {
dividerHeight: nativeView.getDividerHeight(),
divider: nativeView.getDivider()
};
}
[separatorColorProperty.setNative](value: Color | { dividerHeight: number, divider: android.graphics.drawable.Drawable }) {
let nativeView = this.nativeViewProtected;
if (value instanceof Color) {
nativeView.setDivider(new android.graphics.drawable.ColorDrawable(value.android));
nativeView.setDividerHeight(1);
} else {
nativeView.setDivider(value.divider);
nativeView.setDividerHeight(value.dividerHeight);
}
}
[itemTemplatesProperty.getDefault](): KeyedTemplate[] {
return null;
}
[itemTemplatesProperty.setNative](value: KeyedTemplate[]) {
this._itemTemplatesInternal = new Array<KeyedTemplate>(this._defaultTemplate);
if (value) {
this._itemTemplatesInternal = this._itemTemplatesInternal.concat(value);
}
this.nativeViewProtected.setAdapter(new ListViewAdapterClass(this));
this.refresh();
}
}
let ListViewAdapterClass;
function ensureListViewAdapterClass() {
if (ListViewAdapterClass) {
return;
}
class ListViewAdapter extends android.widget.BaseAdapter {
constructor(public owner: ListView) {
super();
return global.__native(this);
}
public getCount() {
return this.owner && this.owner.items && this.owner.items.length ? this.owner.items.length : 0;
}
public getItem(i: number) {
if (this.owner && this.owner.items && i < this.owner.items.length) {
let getItem = (<ItemsSource>this.owner.items).getItem;
return getItem ? getItem(i) : this.owner.items[i];
}
return null;
}
public getItemId(i: number) {
return long(i);
}
public hasStableIds(): boolean {
return true;
}
public getViewTypeCount() {
return this.owner._itemTemplatesInternal.length;
}
public getItemViewType(index: number) {
let template = this.owner._getItemTemplate(index);
let itemViewType = this.owner._itemTemplatesInternal.indexOf(template);
return itemViewType;
}
@profile
public getView(index: number, convertView: android.view.View, parent: android.view.ViewGroup): android.view.View {
//this.owner._dumpRealizedTemplates();
if (!this.owner) {
return null;
}
let totalItemCount = this.owner.items ? this.owner.items.length : 0;
if (index === (totalItemCount - 1)) {
this.owner.notify({ eventName: LOADMOREITEMS, object: this.owner });
}
// Recycle an existing view or create a new one if needed.
let template = this.owner._getItemTemplate(index);
let view: View;
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 = template.createView();
}
let args: ItemEventData = {
eventName: ITEMLOADING, object: this.owner, index: index, view: view,
android: parent,
ios: undefined
};
this.owner.notify(args);
if (!args.view) {
args.view = this.owner._getDefaultItemContent(index);
}
if (args.view) {
if (this.owner._effectiveRowHeight > -1) {
args.view.height = this.owner.rowHeight;
}
else {
args.view.height = <Length>unsetValue;
}
this.owner._prepareItem(args.view, index);
if (!args.view.parent) {
// Proxy containers should not get treated as layouts.
// Wrap them in a real layout as well.
if (args.view instanceof LayoutBase &&
!(args.view instanceof ProxyViewContainer)) {
this.owner._addView(args.view);
convertView = args.view.nativeViewProtected;
} else {
let 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);
}
return convertView;
}
}
ListViewAdapterClass = ListViewAdapter;
}