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