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