1/*
2 * Copyright (c) 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
16type VisibleRangeChangedCallback = () => void;
17
18interface IItemsOnScreenProvider {
19  register(callback: VisibleRangeChangedCallback): void;
20  get visibleRange(): IndexRange;
21  get meanValue(): number;
22  get direction(): ScrollDirection;
23  get speed(): number;
24  updateSpeed(minVisible: number, maxVisible: number): void;
25  update(minVisible: number, maxVisible: number): void;
26}
27
28// eslint-disable-next-line @typescript-eslint/no-unused-vars
29class ItemsOnScreenProvider implements IItemsOnScreenProvider {
30  private firstScreen = true;
31  private meanImagesOnScreen = 0;
32  private minVisible = 0;
33  private maxVisible = 0;
34  private directionInternal: ScrollDirection = 'UNKNOWN';
35  private speedInternal = 0;
36  private lastUpdateTimestamp = 0;
37  private visibleRangeInternal: IndexRange = new IndexRange(0, 0);
38
39  private callbacks: VisibleRangeChangedCallback[] = [];
40
41  register(callback: VisibleRangeChangedCallback): void {
42    this.callbacks.push(callback);
43  }
44
45  get visibleRange(): IndexRange {
46    return this.visibleRangeInternal;
47  }
48
49  get meanValue(): number {
50    return this.meanImagesOnScreen;
51  }
52
53  get direction(): ScrollDirection {
54    return this.directionInternal;
55  }
56
57  get speed(): number {
58    return this.speedInternal;
59  }
60
61  updateSpeed(minVisible: number, maxVisible: number): void {
62    const timeDifference = Date.now() - this.lastUpdateTimestamp;
63    if (timeDifference > 0) {
64      const speedTau = 100;
65      const speedWeight = 1 - Math.exp(-timeDifference / speedTau);
66      const distance =
67        minVisible + (maxVisible - minVisible) / 2 - (this.minVisible + (this.maxVisible - this.minVisible) / 2);
68      const rawSpeed = Math.abs(distance / timeDifference) * 1000;
69      this.speedInternal = speedWeight * rawSpeed + (1 - speedWeight) * this.speedInternal;
70    }
71  }
72
73  update(minVisible: number, maxVisible: number): void {
74    if (minVisible !== this.minVisible || maxVisible !== this.maxVisible) {
75      if (
76        Math.max(minVisible, this.minVisible) === minVisible &&
77        Math.max(maxVisible, this.maxVisible) === maxVisible
78      ) {
79        this.directionInternal = 'DOWN';
80      } else if (
81        Math.min(minVisible, this.minVisible) === minVisible &&
82        Math.min(maxVisible, this.maxVisible) === maxVisible
83      ) {
84        this.directionInternal = 'UP';
85      }
86    }
87
88    let imagesOnScreen = maxVisible - minVisible + 1;
89    let oldMeanImagesOnScreen = this.meanImagesOnScreen;
90    if (this.firstScreen) {
91      this.meanImagesOnScreen = imagesOnScreen;
92      this.firstScreen = false;
93      this.lastUpdateTimestamp = Date.now();
94    } else {
95      {
96        const imagesWeight = 0.95;
97        this.meanImagesOnScreen = this.meanImagesOnScreen * imagesWeight + (1 - imagesWeight) * imagesOnScreen;
98      }
99      this.updateSpeed(minVisible, maxVisible);
100    }
101
102    this.minVisible = minVisible;
103    this.maxVisible = maxVisible;
104
105    const visibleRangeSizeChanged = Math.ceil(oldMeanImagesOnScreen) !== Math.ceil(this.meanImagesOnScreen);
106    this.visibleRangeInternal = new IndexRange(minVisible, maxVisible + 1);
107
108    if (visibleRangeSizeChanged) {
109      this.notifyObservers();
110    }
111    this.lastUpdateTimestamp = Date.now();
112  }
113
114  private notifyObservers(): void {
115    this.callbacks.forEach((callback) => callback());
116  }
117}
118