• 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 ttypeGenFunc_?: RepeatTTypeGenFunc<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.ttypeGenFunc_ = config.ttypeGenFunc;
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 as number) < 0)
60            ? this.arr_.length
61            : config.totalCount as number;
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    // wraps a type gen function with validation logic
83    private ttypeGenFunc(item: T, index: number): string {
84        if (this.ttypeGenFunc_ === undefined) {
85            return RepeatEachFuncTtype;
86        }
87        let ttype = RepeatEachFuncTtype;
88        try {
89            ttype = this.ttypeGenFunc_(item, index);
90        } catch (e) {
91            stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl. Error generating ttype at index: ${index}`,
92                e?.message);
93        }
94        if (ttype in this.itemGenFuncs_ === false) {
95            stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl. No template found for ttype '${ttype}'`);
96            ttype = RepeatEachFuncTtype;
97        }
98        return ttype;
99    }
100
101    /**/
102    private initialRender(
103        owningView: ViewV2,
104        repeatElmtId: number
105    ): void {
106
107        this.repeatElmtId_ = repeatElmtId;
108
109        const onCreateNode = (forIndex: number): void => {
110            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode index ${forIndex} - start`);
111            if (forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
112                // STATE_MGMT_NOTE check also index < totalCount
113                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode: for index=${forIndex}  \
114                    with data array length ${this.arr_.length}, totalCount=${this.totalCount_}  out of range error.`);
115            }
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
122            const repeatItem = this.mkRepeatItem_(this.arr_[forIndex], forIndex);
123            let forKey = this.getOrMakeKey4Index(forIndex);
124            this.repeatItem4Key_.set(forKey, repeatItem);
125
126            // execute the itemGen function
127            this.initialRenderItem(repeatItem);
128            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode for index ${forIndex} key "${forKey}" - end`);
129        }; // onCreateNode
130
131        const onUpdateNode = (fromKey: string, forIndex: number): void => {
132            if (!fromKey || fromKey === '' || forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
133                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
134                    forIndex=${forIndex}, with data array length ${this.arr_.length}, totalCount=${this.totalCount_}, invalid function input error.`);
135            }
136            // create dependency array item [forIndex] -> Repeat
137            // so Repeat updates when the array item changes
138            // STATE_MGMT_NOTE observe dependencies, adding the array is insurgent for Array of objects
139            ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, forIndex.toString());
140            const repeatItem = this.repeatItem4Key_.get(fromKey);
141            if (!repeatItem) {
142                stateMgmtConsole.error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: \
143                    forIndex=${forIndex}, can not find RepeatItem for key. Unrecoverable error.`);
144                return;
145            }
146            const forKey = this.getOrMakeKey4Index(forIndex);
147            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
148                forIndex=${forIndex} forKey="${forKey}". Updating RepeatItem ...`);
149
150            // update Map according to made update:
151            // del fromKey entry and add forKey
152            this.repeatItem4Key_.delete(fromKey);
153            this.repeatItem4Key_.set(forKey, repeatItem);
154
155            if (repeatItem.item !== this.arr_[forIndex] || repeatItem.index !== forIndex) {
156                // repeatItem needs update, will trigger partial update to using UINodes:
157                repeatItem.updateItem(this.arr_[forIndex]);
158                repeatItem.updateIndex(forIndex);
159
160                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
161                    forIndex=${forIndex} forKey="${forKey}". Initiating UINodes update synchronously ...`);
162                ObserveV2.getObserve().updateDirty2(true);
163            }
164        }; // onUpdateNode
165
166        const onGetKeys4Range = (from: number, to: number): Array<string> => {
167            if (to > this.totalCount_ || to > this.arr_.length) {
168                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onGetKeys4Range: \
169                    from ${from} to ${to} with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
170                    Error!. Application fails to add more items to source data array on time. Trying with corrected input parameters ...`);
171                to = this.totalCount_;
172                from = Math.min(to, from);
173            }
174            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onGetKeys4Range from ${from} to ${to} - start`);
175            const result = new Array<string>();
176
177            // deep observe dependencies,
178            // create dependency array item [i] -> Repeat
179            // so Repeat updates when the array item or nested objects changes
180            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
181            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
182            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
183            for (let i = from; i <= to && i < this.arr_.length; i++) {
184                result.push(this.getOrMakeKey4Index(i));
185            }
186            ObserveV2.getObserve().stopRecordDependencies();
187            ViewStackProcessor.StopGetAccessRecording();
188
189            let needsRerender = false;
190            result.forEach((key, index) => {
191                const forIndex = index + from;
192                // if repeatItem exists, and needs update then do the update, and call sync update as well
193                // thereby ensure cached items are up-to-date on C++ side. C++ does not need to request update
194                // from TS side
195                const repeatItem4Key = this.repeatItem4Key_.get(key);
196                // make sure the index is up-to-date
197                if (repeatItem4Key && (repeatItem4Key.item !== this.arr_[forIndex] || repeatItem4Key.index !== forIndex)) {
198                    // repeatItem needs update, will trigger partial update to using UINodes:
199                    repeatItem4Key.updateItem(this.arr_[forIndex]);
200                    repeatItem4Key.updateIndex(forIndex);
201                    needsRerender = true;
202                }
203            }); // forEach
204
205            if (needsRerender) {
206                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}) onGetKeys4Range: \
207                    Initiating UINodes update synchronously ...`);
208                ObserveV2.getObserve().updateDirty2(true);
209            }
210
211            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetKeys4Range \
212                from ${from} to ${to} - returns ${result.toString()}`);
213            return result;
214        }; // const onGetKeys4Range
215
216        const onGetTypes4Range = (from: number, to: number): Array<string> => {
217            if (to > this.totalCount_ || to > this.arr_.length) {
218                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onGetTypes4Range: \
219                    from ${from} to ${to} with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
220                    Error! Application fails to add more items to source data array on time. Trying with corrected input parameters ...`);
221                to = this.totalCount_;
222                from = Math.min(to, from);
223            }
224            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range from ${from} to ${to} - start`);
225            const result = new Array<string>();
226
227            // deep observe dependencies,
228            // create dependency array item [i] -> Repeat
229            // so Repeat updates when the array item or nested objects changes
230            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
231            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
232            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
233
234            for (let i = from; i <= to && i < this.arr_.length; i++) {
235                let ttype = this.ttypeGenFunc(this.arr_[i], i);
236                result.push(ttype);
237            } // for
238            ObserveV2.getObserve().stopRecordDependencies();
239            ViewStackProcessor.StopGetAccessRecording();
240
241            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range \
242                from ${from} to ${to} - returns ${result.toString()}`);
243            return result;
244        }; // const onGetTypes4Range
245
246        const onSetActiveRange = (from: number, to: number): void => {
247            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onSetActiveRange(${from}, ${to}).`);
248            // make sparse copy of this.arr_
249            this.lastActiveRangeData_ = new Array<{item: T, ttype: string}>(this.arr_.length);
250
251            if (from <= to) {
252                for (let i = Math.max(0, from); i <= to && i < this.arr_.length; i++) {
253                    const item = this.arr_[i];
254                    const ttype = this.ttypeGenFunc(this.arr_[i], i);
255                    this.lastActiveRangeData_[i] = { item, ttype };
256                }
257            } else {
258                for (let i = 0; i <= to && i < this.arr_.length; i++) {
259                    const item = this.arr_[i];
260                    const ttype = this.ttypeGenFunc(this.arr_[i], i);
261                    this.lastActiveRangeData_[i] = { item, ttype };
262                }
263                for (let i = Math.max(0, from); i < this.arr_.length; i++) {
264                    const item = this.arr_[i];
265                    const ttype = this.ttypeGenFunc(this.arr_[i], i);
266                    this.lastActiveRangeData_[i] = { item, ttype };
267                }
268            }
269
270            if (!this.reusable_) {
271                this.updateRepeatItem4Key(from, to);
272            }
273        };
274
275        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll ...`);
276
277        RepeatVirtualScrollNative.create(this.totalCount_, Object.entries(this.templateOptions_), {
278            onCreateNode,
279            onUpdateNode,
280            onGetKeys4Range,
281            onGetTypes4Range,
282            onSetActiveRange
283        }, this.reusable_);
284        RepeatVirtualScrollNative.onMove(this.onMoveHandler_, this.itemDragEventHandler);
285        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll`);
286    }
287
288    private reRender(): void {
289        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): reRender ...`);
290
291        // When this.totalCount_ == 0 need render to clear visible items
292        if (this.hasVisibleItemsChanged() || this.totalCount_ === 0) {
293            this.purgeKeyCache();
294            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, true);
295            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender - done.`);
296        } else {
297            // avoid re-render when data pushed outside visible area
298            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, false);
299            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender (no changes in visible items) - done.`);
300        }
301    }
302
303    private initialRenderItem(repeatItem: __RepeatItemFactoryReturn<T>): void {
304        // execute the itemGen function
305        const itemType = this.ttypeGenFunc(repeatItem.item, repeatItem.index);
306        const isTemplate: boolean = (itemType !== '');
307        const itemFunc = this.itemGenFuncs_[itemType];
308        itemFunc(repeatItem);
309        RepeatVirtualScrollNative.setCreateByTemplate(isTemplate);
310    }
311
312    private hasVisibleItemsChanged(): boolean {
313        // has any item or ttype in the active range changed?
314        for (let i in this.lastActiveRangeData_) {
315            if (!(i in this.arr_)) {
316                return true;
317            }
318            const oldItem = this.lastActiveRangeData_[+i]?.item;
319            const oldType = this.lastActiveRangeData_[+i]?.ttype;
320            const newItem = this.arr_[+i];
321            const newType = this.ttypeGenFunc(this.arr_[+i], +i);
322
323            if (oldItem !== newItem) {
324                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} item changed => true`);
325                return true;
326            }
327            if (oldType !== newType) {
328                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} ttype changed => true`);
329                return true;
330            }
331        }
332
333        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() => false`);
334        return false;
335    }
336
337    /**
338     * maintain: index <-> key mapping
339     * create new key from keyGen function if not in cache
340     * check for duplicate key, and create random key if duplicate found
341     * @param forIndex
342     * @returns unique key
343     */
344    private getOrMakeKey4Index(forIndex: number): string {
345        let key = this.key4Index_.get(forIndex);
346        if (!key) {
347            key = this.keyGenFunc_(this.arr_[forIndex], forIndex);
348            const usedIndex = this.index4Key_.get(key);
349            if (usedIndex !== undefined) {
350                // duplicate key
351                stateMgmtConsole.applicationError(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) getOrMakeKey4Index: \
352                    Detected duplicate key for indices ${forIndex} and ${usedIndex}. \
353                    Generated random key will decrease Repeat performance. Correct the key gen function in your application!`);
354                key = `___${forIndex}_+_${key}_+_${Math.random()}`;
355            }
356            this.key4Index_.set(forIndex, key);
357            this.index4Key_.set(key, forIndex);
358        }
359        return key;
360    }
361
362    private purgeKeyCache(): void {
363        this.key4Index_.clear();
364        this.index4Key_.clear();
365    }
366
367    private updateRepeatItem4Key(from: number, to: number): void {
368        let newRepeatItem4Key = new Map<string, __RepeatItemFactoryReturn<T>>();
369        if (from <= to) {
370            for (let i = Math.max(0, from); i <= to && i < this.arr_.length; i++) {
371                let key = this.key4Index_.get(i);
372                if (key && this.repeatItem4Key_.has(key)) {
373                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
374                }
375            }
376        } else {
377            for (let i = 0; i <= to && i < this.arr_.length; i++) {
378                let key = this.key4Index_.get(i);
379                if (key && this.repeatItem4Key_.has(key)) {
380                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
381                }
382            }
383            for (let i = Math.max(0, from); i < this.arr_.length; i++) {
384                let key = this.key4Index_.get(i);
385                if (key && this.repeatItem4Key_.has(key)) {
386                    newRepeatItem4Key.set(key, this.repeatItem4Key_.get(key)!);
387                }
388            }
389        }
390        this.repeatItem4Key_ = newRepeatItem4Key;
391    }
392};