• 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
31    private mkRepeatItem_: (item: T, index?: number) => __RepeatItemFactoryReturn<T>;
32    private onMoveHandler_?: OnMoveHandler;
33
34    // index <-> key maps
35    private key4Index_: Map<number, string> = new Map<number, string>();
36    private index4Key_: Map<string, number> = new Map<string, number>();
37
38    // Map key -> RepeatItem
39    // added to closure of following lambdas
40    private repeatItem4Key_ = new Map<string, __RepeatItemFactoryReturn<T>>();
41
42    // RepeatVirtualScrollNode elmtId
43    private repeatElmtId_ : number = -1;
44
45    // Last known active range (as sparse array)
46    private lastActiveRangeData_: Array<{ item: T, ttype: string }> = [];
47
48    public render(config: __RepeatConfig<T>, isInitialRender: boolean): void {
49        this.arr_ = config.arr;
50        this.itemGenFuncs_ = config.itemGenFuncs;
51        this.keyGenFunc_ = config.keyGenFunc;
52        this.typeGenFunc_ = config.typeGenFunc;
53
54        // if totalCountSpecified==false, then need to create dependency on array length
55        // so when array length changes, will update totalCount
56        this.totalCountSpecified = config.totalCountSpecified;
57        this.totalCount_ = (!this.totalCountSpecified || config.totalCount < 0)
58            ? this.arr_.length
59            : config.totalCount;
60
61        this.templateOptions_ = config.templateOptions;
62
63        this.mkRepeatItem_ = config.mkRepeatItem;
64        this.onMoveHandler_ = config.onMoveHandler;
65
66        if (isInitialRender) {
67            this.initialRender(config.owningView_, ObserveV2.getCurrentRecordedId());
68        } else {
69            this.reRender();
70        }
71    }
72
73    /**/
74    private initialRender(
75        owningView: ViewV2,
76        repeatElmtId: number
77    ): void {
78
79        this.repeatElmtId_ = repeatElmtId;
80
81        const onCreateNode = (forIndex: number): void => {
82            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) 2onCreateNode index ${forIndex} - start`);
83            if (forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
84                // STATE_MGMT_NOTE check also index < totalCount
85                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode: for index=${forIndex}  \
86                    with data array length ${this.arr_.length}, totalCount=${this.totalCount_}  out of range error.`);
87            }
88
89            // create dependency array item [forIndex] -> Repeat
90            // so Repeat updates when the array item changes
91            // STATE_MGMT_NOTE observe dependencies, adding the array is insurgent for Array of objects
92            ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, forIndex.toString());
93
94            const repeatItem = this.mkRepeatItem_(this.arr_[forIndex], forIndex);
95            let forKey = this.getOrMakeKey4Index(forIndex);
96            this.repeatItem4Key_.set(forKey, repeatItem);
97
98            // execute the itemGen function
99            this.initialRenderItem(repeatItem);
100            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onCreateNode for index ${forIndex} key "${forKey}" - end `);
101        }; // onCreateNode
102
103        const onUpdateNode = (fromKey: string, forIndex: number): void => {
104            if (!fromKey || fromKey === '' || forIndex < 0 || forIndex >= this.totalCount_ || forIndex >= this.arr_.length) {
105                throw new Error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", \
106                    forIndex=${forIndex}, with data array length ${this.arr_.length}, totalCount=${this.totalCount_}, invalid function input error.`);
107            }
108            // create dependency array item [forIndex] -> Repeat
109            // so Repeat updates when the array item changes
110            // STATE_MGMT_NOTE observe dependencies, adding the array is insurgent for Array of objects
111            ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, forIndex.toString());
112            const repeatItem = this.repeatItem4Key_.get(fromKey);
113            if (!repeatItem) {
114                stateMgmtConsole.error(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", forIndex=${forIndex}, can not find RepeatItem for key. Unrecoverable error`);
115                return;
116            }
117            const forKey = this.getOrMakeKey4Index(forIndex);
118            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", forIndex=${forIndex} forKey="${forKey}". Updating RepeatItem ...`);
119
120                        // update Map according to made update:
121            // del fromKey entry and add forKey
122            this.repeatItem4Key_.delete(fromKey);
123            this.repeatItem4Key_.set(forKey, repeatItem);
124
125            if (repeatItem.item !== this.arr_[forIndex] || repeatItem.index !== forIndex) {
126                // repeatItem needs update, will trigger partial update to using UINodes:
127                repeatItem.updateItem(this.arr_[forIndex]);
128                repeatItem.updateIndex(forIndex);
129
130                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl (${this.repeatElmtId_}) onUpdateNode: fromKey "${fromKey}", forIndex=${forIndex} forKey="${forKey}". Initiating UINodes update synchronously ...`);
131                ObserveV2.getObserve().updateDirty2(true);
132            }
133        }; // onUpdateNode
134
135        const onGetKeys4Range = (from: number, to: number): Array<string> => {
136            if (to > this.totalCount_ || to > this.arr_.length) {
137                stateMgmtConsole.applicationError(`Repeat with virtualScroll elmtId ${this.repeatElmtId_}:  onGetKeys4Range from ${from} to ${to} \
138                    with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
139                    Error!. Application fails to add more items to source data array. on time. Trying with corrected input parameters ...`);
140                to = this.totalCount_;
141                from = Math.min(to, from);
142            }
143            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onGetKeys4Range from ${from} to ${to} - start`);
144            const result = new Array<string>();
145
146            // deep observe dependencies,
147            // create dependency array item [i] -> Repeat
148            // so Repeat updates when the array item or nested objects changes
149            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
150            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
151            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
152            for (let i = from; i <= to && i < this.arr_.length; i++) {
153                result.push(this.getOrMakeKey4Index(i));
154            }
155            ObserveV2.getObserve().stopRecordDependencies();
156            ViewStackProcessor.StopGetAccessRecording();
157
158            let needsRerender = false;
159            result.forEach((key, index) => {
160                const forIndex = index + from;
161                // if repeatItem exists, and needs update then do the update, and call sync update as well
162            // thereby ensure cached items are up-to-date on C++ side. C++ does not need to request update
163            // from TS side
164                const repeatItem4Key = this.repeatItem4Key_.get(key);
165                // make sure the index is up-to-date
166                if (repeatItem4Key && (repeatItem4Key.item !== this.arr_[forIndex] || repeatItem4Key.index !== forIndex)) {
167                    // repeatItem needs update, will trigger partial update to using UINodes:
168                    repeatItem4Key.updateItem(this.arr_[forIndex]);
169                    repeatItem4Key.updateIndex(forIndex);
170                    needsRerender = true;
171                }
172            }); // forEach
173
174            if (needsRerender) {
175                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}) onGetKeys4Range:  Initiating UINodes update synchronously ...`);
176                ObserveV2.getObserve().updateDirty2(true);
177            }
178
179            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetKeys4Range from ${from} to ${to} - returns ${result.toString()}`);
180            return result;
181        }; // const onGetKeys4Range
182
183        const onGetTypes4Range = (from: number, to: number): Array<string> => {
184            if (to > this.totalCount_ || to > this.arr_.length) {
185                stateMgmtConsole.applicationError(`Repeat with virtualScroll elmtId: ${this.repeatElmtId_}:  onGetTypes4Range from ${from} to ${to} \
186                  with data array length ${this.arr_.length}, totalCount=${this.totalCount_} \
187                  Error! Application fails to add more items to source data array.on time.Trying with corrected input parameters ...`);
188                to = this.totalCount_;
189                from = Math.min(to, from);
190            }
191            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range from ${from} to ${to} - start`);
192            const result = new Array<string>();
193
194            // deep observe dependencies,
195            // create dependency array item [i] -> Repeat
196            // so Repeat updates when the array item or nested objects changes
197            // not enough: ObserveV2.getObserve().addRef4Id(this.repeatElmtId_, this.arr_, i.toString());
198            ViewStackProcessor.StartGetAccessRecordingFor(this.repeatElmtId_);
199            ObserveV2.getObserve().startRecordDependencies(owningView, this.repeatElmtId_, false);
200
201            for (let i = from; i <= to && i < this.arr_.length; i++) {
202                let ttype = this.typeGenFunc_(this.arr_[i], i);
203                result.push(ttype);
204            } // for
205            ObserveV2.getObserve().stopRecordDependencies();
206            ViewStackProcessor.StopGetAccessRecording();
207
208            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): onGetTypes4Range from ${from} to ${to} - returns ${result.toString()}`);
209            return result;
210        }; // const onGetTypes4Range
211
212        const onSetActiveRange = (from: number, to: number): void => {
213            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: onSetActiveRange(${from}, ${to})`);
214            // make sparse copy of this.arr_
215            this.lastActiveRangeData_ = new Array<{item: T, ttype: string}>(this.arr_.length);
216
217            if (from <= to) {
218                for (let i = Math.max(0, from); i <= to && i < this.arr_.length; i++) {
219                    const item = this.arr_[i];
220                    const ttype = this.typeGenFunc_(this.arr_[i], i);
221                    this.lastActiveRangeData_[i] = { item, ttype };
222                }
223            } else {
224                for (let i = 0; i <= to && i < this.arr_.length; i++) {
225                    const item = this.arr_[i];
226                    const ttype = this.typeGenFunc_(this.arr_[i], i);
227                    this.lastActiveRangeData_[i] = { item, ttype };
228                }
229                for (let i = Math.max(0, from); i < this.arr_.length; i++) {
230                    const item = this.arr_[i];
231                    const ttype = this.typeGenFunc_(this.arr_[i], i);
232                    this.lastActiveRangeData_[i] = { item, ttype };
233                }
234            }
235        };
236
237        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll`);
238
239        RepeatVirtualScrollNative.create(this.totalCount_, Object.entries(this.templateOptions_), {
240            onCreateNode,
241            onUpdateNode,
242            onGetKeys4Range,
243            onGetTypes4Range,
244            onSetActiveRange
245        });
246        RepeatVirtualScrollNative.onMove(this.onMoveHandler_);
247        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): initialRenderVirtualScroll`);
248    }
249
250    private reRender(): void {
251        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl(${this.repeatElmtId_}): reRender ...`);
252
253        // When this.totalCount_ == 0 need render to clear visible items
254        if (this.hasVisibleItemsChanged() || this.totalCount_ === 0) {
255            this.purgeKeyCache();
256            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, true);
257            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender - done.`);
258        } else {
259            // avoid re-render when data pushed outside visible area
260            RepeatVirtualScrollNative.updateRenderState(this.totalCount_, false);
261            stateMgmtConsole.debug(`__RepeatVirtualScrollImpl: reRender (no changes in visible items) - done.`);
262        }
263    }
264
265    private initialRenderItem(repeatItem: __RepeatItemFactoryReturn<T>): void {
266        // execute the itemGen function
267        const itemType = this.typeGenFunc_(repeatItem.item, repeatItem.index);
268        const itemFunc = this.itemGenFuncs_[itemType];
269        itemFunc(repeatItem);
270    }
271
272    private hasVisibleItemsChanged(): boolean {
273        // has any item or ttype in the active range changed?
274        for (let i in this.lastActiveRangeData_) {
275            if (!(i in this.arr_)) {
276                return true;
277            }
278            const oldItem = this.lastActiveRangeData_[+i]?.item;
279            const oldType = this.lastActiveRangeData_[+i]?.ttype;
280            const newItem = this.arr_[+i];
281            const newType = this.typeGenFunc_(this.arr_[+i], +i);
282
283            if (oldItem !== newItem) {
284                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} item changed => true`);
285                return true;
286            }
287            if (oldType !== newType) {
288                stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() i:#${i} ttype changed => true`);
289                return true;
290            }
291        }
292
293        stateMgmtConsole.debug(`__RepeatVirtualScrollImpl.hasVisibleItemsChanged() => false`);
294        return false;
295    }
296
297    /**
298     * maintain: index <-> key mapping
299     * create new key from keyGen function if not in cache
300     * check for duplicate key, and create random key if duplicate found
301     * @param forIndex
302     * @returns unique key
303     */
304    private getOrMakeKey4Index(forIndex: number): string {
305        let key = this.key4Index_.get(forIndex);
306        if (!key) {
307            key = this.keyGenFunc_(this.arr_[forIndex], forIndex);
308            const usedIndex = this.index4Key_.get(key);
309            if (usedIndex !== undefined) {
310                // duplicate key
311                stateMgmtConsole.applicationError(`Repeat key gen function elmtId ${this.repeatElmtId_}: Detected duplicate key ${key} for indices ${forIndex} and ${usedIndex}. \
312                            Generated random key will decrease Repeat performance. Correct the Key gen function in your application!`);
313                key = `___${forIndex}_+_${key}_+_${Math.random()}`;
314            }
315            this.key4Index_.set(forIndex, key);
316            this.index4Key_.set(key, forIndex);
317        }
318        return key;
319    }
320
321    private purgeKeyCache(): void {
322        this.key4Index_.clear();
323        this.index4Key_.clear();
324    }
325};