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};