• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 *
15 * all definitions in this file are framework internal
16*/
17
18// Implements ForEach with child re-use for both existing state observation and
19// deep observation. For virtual-scroll code paths
20
21class __RepeatVirtualScrollImpl<T> {
22    private arr_: Array<T>;
23    private itemGenFuncs_: { [type: string]: RepeatItemGenFunc<T> };
24    private keyGenFunc_?: RepeatKeyGenFunc<T>;
25    private typeGenFunc_: RepeatTypeGenFunc<T>;
26
27    private totalCount_: number;
28    private totalCountSpecified : boolean = false;
29    private templateOptions_: { [type: string]: RepeatTemplateImplOptions };
30    private reusable_: boolean = true;
31
32    private mkRepeatItem_: (item: T, index?: number) => __RepeatItemFactoryReturn<T>;
33    private onMoveHandler_?: OnMoveHandler;
34    private itemDragEventHandler?: ItemDragEventHandler;
35
36    // index <-> key maps
37    private key4Index_: Map<number, string> = new Map<number, string>();
38    private index4Key_: Map<string, number> = new Map<string, number>();
39
40    // Map key -> RepeatItem
41    // added to closure of following lambdas
42    private repeatItem4Key_ = new Map<string, __RepeatItemFactoryReturn<T>>();
43
44    // RepeatVirtualScrollNode elmtId
45    private repeatElmtId_ : number = -1;
46
47    // Last known active range (as sparse array)
48    private lastActiveRangeData_: Array<{ item: T, ttype: string }> = [];
49
50    public render(config: __RepeatConfig<T>, isInitialRender: boolean): void {
51        this.arr_ = config.arr;
52        this.itemGenFuncs_ = config.itemGenFuncs;
53        this.keyGenFunc_ = config.keyGenFunc;
54        this.typeGenFunc_ = config.typeGenFunc;
55
56        // if totalCountSpecified==false, then need to create dependency on array length
57        // so when array length changes, will update totalCount
58        this.totalCountSpecified = config.totalCountSpecified;
59        this.totalCount_ = (!this.totalCountSpecified || config.totalCount < 0)
60            ? this.arr_.length
61            : config.totalCount;
62
63        this.templateOptions_ = config.templateOptions;
64
65        this.mkRepeatItem_ = config.mkRepeatItem;
66        this.onMoveHandler_ = config.onMoveHandler;
67        this.itemDragEventHandler = config.itemDragEventHandler;
68
69        if (isInitialRender) {
70            this.reusable_ = config.reusable;
71            if (!this.reusable_) {
72                for (let templateType in this.templateOptions_) {
73                    this.templateOptions_[templateType] = { cachedCountSpecified: true, cachedCount: 0 };
74                }
75            }
76            this.initialRender(config.owningView_, ObserveV2.getCurrentRecordedId());
77        } else {
78            this.reRender();
79        }
80    }
81
82    /**/
83    private initialRender(
84        owningView: ViewV2,
85        repeatElmtId: number
86    ): void {
87
88        this.repeatElmtId_ = repeatElmtId;
89
90        const onCreateNode = (forIndex: number): void => {
91            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode index ${forIndex} - start`);
92            if (forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
93                // STATE_MGMT_NOTE check also index < totalCount
94                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode: for index=${forIndex}  \
95                    with data array length ${this.arr_.length}, totalCount=${this.totalCount_}  out of range error.`);
96            }
97
98            // create dependency array item [forIndex] -> Repeat
99            // so Repeat updates when the array item changes
100            // STATE_MGMT_NOTE observe dependencies, adding the array is insurgent for Array of objects
101            ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, forIndex.toString());
102
103            const repeatItem = this.mkRepeatItem_(this.arr_[forIndex], forIndex);
104            let forKey = this.getOrMakeKey4Index(forIndex);
105            this.repeatItem4Key_.set(forKey, repeatItem);
106
107            // execute the itemGen function
108            this.initialRenderItem(repeatItem);
109            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode for index ${forIndex} key "${forKey}" - end`);
110        }; // onCreateNode
111
112        const onUpdateNode = (fromKey: string, forIndex: number): void => {
113            if (!fromKey || fromKey === '' || forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
114                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
115                    forIndex=${forIndex}, with data array length ${this.arr_.length}, totalCount=${this.totalCount_}, invalid function input error.`);
116            }
117            // create dependency array item [forIndex] -> Repeat
118            // so Repeat updates when the array item changes
119            // STATE_MGMT_NOTE observe dependencies, adding the array is insurgent for Array of objects
120            ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, forIndex.toString());
121            const repeatItem = this.repeatItem4Key_.get(fromKey);
122            if (!repeatItem) {
123                stateMgmtConsole.error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
124                    forIndex=${forIndex}, can not find RepeatItem for key. Unrecoverable error.`);
125                return;
126            }
127            const forKey = this.getOrMakeKey4Index(forIndex);
128            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
129                forIndex=${forIndex} forKey="${forKey}". Updating RepeatItem ...`);
130
131            // update Map according to made update:
132            // del fromKey entry and add forKey
133            this.repeatItem4Key_.delete(fromKey);
134            this.repeatItem4Key_.set(forKey, repeatItem);
135
136            if (repeatItem.item !== this.arr_[forIndex] || repeatItem.index !== forIndex) {
137                // repeatItem needs update, will trigger partial update to using UINodes:
138                repeatItem.updateItem(this.arr_[forIndex]);
139                repeatItem.updateIndex(forIndex);
140
141                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
142                    forIndex=${forIndex} forKey="${forKey}". Initiating UINodes update synchronously ...`);
143                ObserveV2.getObserve().updateDirty2(true);
144            }
145        }; // onUpdateNode
146
147        const onGetKeys4Range = (from: number, to: number): Array<string> => {
148            if (to > this.totalCount_ || to > this.arr_.length) {
149                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onGetKeys4Range: \
150                    from ${from} to ${to} with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
151                    Error!. Application fails to add more items to source data array on time. Trying with corrected input parameters ...`);
152                to = this.totalCount_;
153                from = Math.min(to, from);
154            }
155            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onGetKeys4Range from ${from} to ${to} - start`);
156            const result = new Array<string>();
157
158            // deep observe dependencies,
159            // create dependency array item [i] -> Repeat
160            // so Repeat updates when the array item or nested objects changes
161            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
162            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
163            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
164            for (let i = from; i <= to && i < this.arr_.length; i++) {
165                result.push(this.getOrMakeKey4Index(i));
166            }
167            ObserveV2.getObserve().stopRecordDependencies();
168            ViewStackProcessor.StopGetAccessRecording();
169
170            let needsRerender = false;
171            result.forEach((key, index) => {
172                const forIndex = index + from;
173                // if repeatItem exists, and needs update then do the update, and call sync update as well
174                // thereby ensure cached items are up-to-date on C++ side. C++ does not need to request update
175                // from TS side
176                const repeatItem4Key = this.repeatItem4Key_.get(key);
177                // make sure the index is up-to-date
178                if (repeatItem4Key && (repeatItem4Key.item !== this.arr_[forIndex] || repeatItem4Key.index !== forIndex)) {
179                    // repeatItem needs update, will trigger partial update to using UINodes:
180                    repeatItem4Key.updateItem(this.arr_[forIndex]);
181                    repeatItem4Key.updateIndex(forIndex);
182                    needsRerender = true;
183                }
184            }); // forEach
185
186            if (needsRerender) {
187                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}) onGetKeys4Range: \
188                    Initiating UINodes update synchronously ...`);
189                ObserveV2.getObserve().updateDirty2(true);
190            }
191
192            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetKeys4Range \
193                from ${from} to ${to} - returns ${result.toString()}`);
194            return result;
195        }; // const onGetKeys4Range
196
197        const onGetTypes4Range = (from: number, to: number): Array<string> => {
198            if (to > this.totalCount_ || to > this.arr_.length) {
199                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onGetTypes4Range: \
200                    from ${from} to ${to} with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
201                    Error! Application fails to add more items to source data array on time. Trying with corrected input parameters ...`);
202                to = this.totalCount_;
203                from = Math.min(to, from);
204            }
205            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range from ${from} to ${to} - start`);
206            const result = new Array<string>();
207
208            // deep observe dependencies,
209            // create dependency array item [i] -> Repeat
210            // so Repeat updates when the array item or nested objects changes
211            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
212            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
213            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
214
215            for (let i = from; i <= to && i < this.arr_.length; i++) {
216                let ttype = this.typeGenFunc_(this.arr_[i], i);
217                result.push(ttype);
218            } // for
219            ObserveV2.getObserve().stopRecordDependencies();
220            ViewStackProcessor.StopGetAccessRecording();
221
222            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range \
223                from ${from} to ${to} - returns ${result.toString()}`);
224            return result;
225        }; // const onGetTypes4Range
226
227        const onSetActiveRange = (from: number, to: number): void => {
228            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onSetActiveRange(${from}, ${to}).`);
229            // make sparse copy of this.arr_
230            this.lastActiveRangeData_ = new Array<{item: T, ttype: string}>(this.arr_.length);
231
232            if (from <= to) {
233                for (let i = Math.max(0, from); i <= to && i < this.arr_.length; i++) {
234                    const item = this.arr_[i];
235                    const ttype = this.typeGenFunc_(this.arr_[i], i);
236                    this.lastActiveRangeData_[i] = { item, ttype };
237                }
238            } else {
239                for (let i = 0; i <= to && i < this.arr_.length; i++) {
240                    const item = this.arr_[i];
241                    const ttype = this.typeGenFunc_(this.arr_[i], i);
242                    this.lastActiveRangeData_[i] = { item, ttype };
243                }
244                for (let i = Math.max(0, from); i < this.arr_.length; i++) {
245                    const item = this.arr_[i];
246                    const ttype = this.typeGenFunc_(this.arr_[i], i);
247                    this.lastActiveRangeData_[i] = { item, ttype };
248                }
249            }
250
251            if (!this.reusable_) {
252                this.updateRepeatItem4Key(from, to);
253            }
254        };
255
256        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll ...`);
257
258        RepeatVirtualScrollNative.create(this.totalCount_, Object.entries(this.templateOptions_), {
259            onCreateNode,
260            onUpdateNode,
261            onGetKeys4Range,
262            onGetTypes4Range,
263            onSetActiveRange
264        }, this.reusable_);
265        RepeatVirtualScrollNative.onMove(this.onMoveHandler_, this.itemDragEventHandler);
266        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll`);
267    }
268
269    private reRender(): void {
270        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): reRender ...`);
271
272        // When this.totalCount_ == 0 need render to clear visible items
273        if (this.hasVisibleItemsChanged() || this.totalCount_ === 0) {
274            this.purgeKeyCache();
275            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, true);
276            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender - done.`);
277        } else {
278            // avoid re-render when data pushed outside visible area
279            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, false);
280            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender (no changes in visible items) - done.`);
281        }
282    }
283
284    private initialRenderItem(repeatItem: __RepeatItemFactoryReturn<T>): void {
285        // execute the itemGen function
286        const itemType = this.typeGenFunc_(repeatItem.item, repeatItem.index);
287        const isTemplate: boolean = (itemType !== '');
288        const itemFunc = this.itemGenFuncs_[itemType];
289        itemFunc(repeatItem);
290        RepeatVirtualScrollNative.setCreateByTemplate(isTemplate);
291    }
292
293    private hasVisibleItemsChanged(): boolean {
294        // has any item or ttype in the active range changed?
295        for (let i in this.lastActiveRangeData_) {
296            if (!(i in this.arr_)) {
297                return true;
298            }
299            const oldItem = this.lastActiveRangeData_[+i]?.item;
300            const oldType = this.lastActiveRangeData_[+i]?.ttype;
301            const newItem = this.arr_[+i];
302            const newType = this.typeGenFunc_(this.arr_[+i], +i);
303
304            if (oldItem !== newItem) {
305                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} item changed => true`);
306                return true;
307            }
308            if (oldType !== newType) {
309                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} ttype changed => true`);
310                return true;
311            }
312        }
313
314        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() => false`);
315        return false;
316    }
317
318    /**
319     * maintain: index <-> key mapping
320     * create new key from keyGen function if not in cache
321     * check for duplicate key, and create random key if duplicate found
322     * @param forIndex
323     * @returns unique key
324     */
325    private getOrMakeKey4Index(forIndex: number): string {
326        let key = this.key4Index_.get(forIndex);
327        if (!key) {
328            key = this.keyGenFunc_(this.arr_[forIndex], forIndex);
329            const usedIndex = this.index4Key_.get(key);
330            if (usedIndex !== undefined) {
331                // duplicate key
332                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) getOrMakeKey4Index: \
333                    Detected duplicate key ${key} for indices ${forIndex} and ${usedIndex}. \
334                    Generated random key will decrease Repeat performance. Correct the key gen function in your application!`);
335                key = `___${forIndex}_+_${key}_+_${Math.random()}`;
336            }
337            this.key4Index_.set(forIndex, key);
338            this.index4Key_.set(key, forIndex);
339        }
340        return key;
341    }
342
343    private purgeKeyCache(): void {
344        this.key4Index_.clear();
345        this.index4Key_.clear();
346    }
347
348    private updateRepeatItem4Key(from: number, to: number): void {
349        let newRepeatItem4Key = new Map<string, __RepeatItemFactoryReturn<T>>();
350        if (from <= to) {
351            for (let i = Math.max(0, from); i <= to && i < this.arr_.length; i++) {
352                let key = this.key4Index_.get(i);
353                if (key && this.repeatItem4Key_.has(key)) {
354                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
355                }
356            }
357        } else {
358            for (let i = 0; i <= to && i < this.arr_.length; i++) {
359                let key = this.key4Index_.get(i);
360                if (key && this.repeatItem4Key_.has(key)) {
361                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
362                }
363            }
364            for (let i = Math.max(0, from); i < this.arr_.length; i++) {
365                let key = this.key4Index_.get(i);
366                if (key && this.repeatItem4Key_.has(key)) {
367                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
368                }
369            }
370        }
371        this.repeatItem4Key_ = newRepeatItem4Key;
372    }
373};