• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 Shenzhen Kaihong Digital Industry Development 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
16import Matrix4 from '@ohos.matrix4';
17import { UserFileDataItem } from '../base/UserFileDataItem';
18import { Log } from '../utils/Log';
19import { Broadcast } from './Broadcast';
20import { MathUtils } from '../utils/MathUtils';
21import { Constants } from '../constants/BrowserConstants';
22import { screenManager } from './ScreenManager';
23import { MediaConstants } from '../constants/MediaConstants';
24
25const TAG = 'EventPipeline'
26
27export interface Matrix4TransitWithMatrix4x4 extends Matrix4.Matrix4Transit {
28  matrix4x4: number[];
29}
30
31export interface AnimationOption {
32  duration: number,
33  curve: Curve
34}
35
36export class EventPipeline {
37
38  // last offset
39  private lastOffset: number[] = [0, 0];
40
41  // offset
42  private offset: number[] = [0, 0];
43
44  // default scale
45  private defaultScale = 1.0;
46
47  // last scale
48  private lastScale = 1.0;
49
50  // scale
51  private scale = 1.0;
52
53  // the zoom center point is a percentage position relative to the control, not an absolute position
54  private center: number[] = [Constants.CENTER_DEFAULT, Constants.CENTER_DEFAULT];
55
56  // leftmost zoom Center,(1 - leftMost)is rightmost zoom Center
57  private leftMost = 0.0;
58
59  // top zoom center,(1 - topMost)is bottom zoom center
60  private topMost = 0.0;
61
62  // double tap scale
63  private doubleTapScale = 1.0;
64
65  // max scale
66  private maxScale = 1.0;
67
68  // has reached the far left
69  private hasReachLeft = true;
70
71  // has reached the far right
72  private hasReachRight = true;
73
74  // has reached the far top
75  private hasReachTop = true;
76
77  // has reached the far bottom
78  private hasReachBottom = true;
79
80  // Broadcast
81  private broadCast: Broadcast;
82
83  // item
84  private item: UserFileDataItem;
85
86  // timeStamp
87  private timeStamp: string;
88
89  // width
90  private width: number;
91
92  // height
93  private height: number;
94
95  // Large display control width
96  private componentWidth: number = vp2px(screenManager.getWinWidth());
97
98  // Large display control height
99  private componentHeight = vp2px(screenManager.getWinHeight());
100
101  // is now in animation
102  private isInAnimation = false;
103
104  // pull down to return flag to prevent multiple triggers
105  private isExiting = false;
106
107  private updateMatrix: Function;
108
109  constructor(broadCastParam: Broadcast, item: UserFileDataItem, timeStamp: string, updateMatrix: Function) {
110    this.broadCast = broadCastParam;
111    this.item = item;
112    this.timeStamp = timeStamp;
113    this.updateMatrix = updateMatrix;
114    this.width = this.item.imgWidth === 0 ? MediaConstants.DEFAULT_SIZE : this.item.imgWidth;
115    this.height = this.item.imgHeight === 0 ? MediaConstants.DEFAULT_SIZE : this.item.imgHeight;
116    this.evaluateScales();
117  }
118
119  onDataChanged(item: UserFileDataItem): void {
120    this.item = item;
121    this.width = this.item.imgWidth === 0 ? MediaConstants.DEFAULT_SIZE : this.item.imgWidth;
122    this.height = this.item.imgHeight === 0 ? MediaConstants.DEFAULT_SIZE : this.item.imgHeight;
123    this.evaluateScales();
124  }
125
126  setDefaultScale(scale): void {
127    this.defaultScale = scale;
128    this.lastScale = scale;
129  }
130
131  onComponentSizeChanged(): void {
132    this.evaluateScales();
133  }
134
135  onTouch(event: TouchEvent): void {
136    Log.debug(TAG, 'onTouch trigger: ' + event.type + ', ' + this.isInAnimation + ', ' + this.isExiting);
137    if (this.isInAnimation || this.isExiting) {
138      return;
139    }
140    if (event.type === TouchType.Down || event.type === TouchType.Up) {
141      this.emitDirectionChange();
142    }
143
144    if (event.type === TouchType.Up) {
145      this.lastOffset = this.evaluateOffset();
146      this.lastScale = this.lastScale * this.scale;
147      this.scale = 1;
148      this.offset = [0, 0];
149    }
150  }
151
152  private emitDirectionChange(): void {
153
154    /**
155     * reachLeft reachRight scale>1,only five possible situations(when scale<=1,reachLeft、reachRight is true):
156     * T T T:Vertical
157     * T T F:Vertical(initial state)
158     * T F T:Vertical | Left
159     * F T T:Vertical | Right
160     * F F T:All
161     */
162    let direction;
163    let scale = this.lastScale * this.scale;
164    let isEnlarged = Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS));
165    if (!this.hasReachLeft && !this.hasReachRight && isEnlarged) {
166      direction = PanDirection.All;
167    } else if (!this.hasReachLeft && this.hasReachRight && isEnlarged) {
168      direction = (PanDirection.Vertical as number) | (PanDirection.Right as number);
169    } else if (this.hasReachLeft && !this.hasReachRight && isEnlarged) {
170      direction = (PanDirection.Vertical as number) | (PanDirection.Left as number);
171    } else {
172      direction = PanDirection.Vertical;
173    }
174
175    Log.info(TAG, 'emitDirectionChange reaches: ' + this.hasReachLeft + ', ' + this.hasReachRight + ', ' + this.hasReachTop + ', ' + this.hasReachBottom +
176    ', scale ' + scale + ', direction: ' + direction);
177    if (this.isExiting) {
178      return;
179    }
180
181    if (direction === (PanDirection.Vertical as number) || direction === ((PanDirection.Vertical as number) | (PanDirection.Left as number)) ||
182    direction === ((PanDirection.Vertical as number) | (PanDirection.Right as number))) {
183      this.broadCast.emit(Constants.SET_DISABLE_SWIPE, [false]);
184    } else {
185      this.broadCast.emit(Constants.SET_DISABLE_SWIPE, [true]);
186    }
187    this.broadCast.emit(Constants.DIRECTION_CHANGE + this.item.uri + this.timeStamp, [direction]);
188  }
189
190  private evaluateOffset(): number[] {
191    Log.info(TAG, 'evaluateOffset lastOffset: ' + this.lastOffset + ', offset: ' + this.offset);
192    let centerX = (this.center[0] - Constants.CENTER_DEFAULT) * this.componentWidth * (this.defaultScale - this.scale) * this.lastScale;
193    let centerY = (this.center[1] - Constants.CENTER_DEFAULT) * this.componentHeight * (this.defaultScale - this.scale) * this.lastScale;
194    let offsetX = this.lastOffset[0] + this.offset[0] + centerX;
195    let offsetY = this.lastOffset[1] + this.offset[1] + centerY;
196    Log.debug(TAG, 'evaluateOffset offsetX: ' + offsetX + ', offsetY: ' + offsetY);
197    return [offsetX, offsetY];
198  }
199
200  private emitTouchEvent(): void {
201    let offset: number[];
202    let scale = this.lastScale * this.scale;
203    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
204      let limits = this.evaluateOffsetRange(scale);
205      offset = this.evaluateOffset();
206      // the offset in the X direction is always limited for non shrinking scenes
207      offset[0] = MathUtils.clamp(offset[0], limits[0], limits[1]);
208      if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
209        // cannot pull down to return, limit y
210        offset[1] = MathUtils.clamp(offset[1], limits[2], limits[3]);
211      } else {
212        // can pull down to return to the scene, and only limit y to drag upward, limit the lower bound
213        offset[1] = Math.max(limits[2], offset[1]);
214      }
215    } else {
216      // When zooming in, adjust the zoom center to the display center point
217      offset = [0, 0];
218    }
219    let moveX = offset[0];
220    let moveY = offset[1];
221    Log.debug(TAG, 'emitTouchEvent moveX: ' + moveX + ', moveY: ' + moveY);
222    let scaleOption: Matrix4.ScaleOption = {
223      x: scale,
224      y: scale,
225    };
226    let translateOption: Matrix4.TranslateOption = {
227      x: moveX,
228      y: moveY
229    };
230    let matrix = Matrix4.identity()
231      .scale(scaleOption)
232      .translate(translateOption)
233      .copy();
234    Log.debug(TAG, 'emitTouchEvent lastOffset: ' + this.lastOffset + ', offset: ' + this.offset +
235    ',center: ' + this.center + ', scale: ' + this.lastScale + ', ' + this.scale);
236    this.updateMatrix(matrix);
237    this.evaluateBounds();
238  }
239
240  private evaluateScales(): void {
241    if (this.width * this.componentHeight < this.componentWidth * this.height) {
242      // The aspect ratio is less than the display aspect ratio of the control
243      // the height of the control is equal to the height of the picture
244      this.maxScale = this.height / this.componentHeight;
245      // Double click the enlarged scale to ensure that the left and right boundaries are filled
246      this.doubleTapScale = this.componentWidth * this.height / this.width / this.componentHeight;
247      // leftMost = (1 - dspW / compW) / 2 = (1 - compH * imgW / imgH / compW) / 2
248      this.leftMost = (1 - this.componentHeight * this.width / this.height / this.componentWidth) / Constants.NUMBER_2;
249      this.topMost = 0.0;
250    } else if (this.width * this.componentHeight === this.componentWidth * this.height) {
251      // The aspect ratio is equal to the display aspect ratio of the control
252      this.doubleTapScale = Constants.SAME_RATIO_SCALE_FACTOR;
253      this.maxScale = this.doubleTapScale * Constants.MAX_SCALE_EXTRA_FACTOR;
254      this.leftMost = 0;
255      this.topMost = 0;
256    } else {
257      // The aspect ratio is greater than the display aspect ratio of the control
258      // the width of the control is equal to the width of the picture
259      this.maxScale = this.width / this.componentWidth;
260      // Double click the enlarged scale to ensure that the top and bottom fill the boundary
261      this.doubleTapScale = this.componentHeight * this.width / this.height / this.componentWidth;
262      this.leftMost = 0.0;
263      this.topMost = (1 - this.componentWidth * this.height / this.width / this.componentHeight) / Constants.NUMBER_2;
264    }
265
266    this.maxScale = Math.max(this.maxScale, Constants.COMPONENT_SCALE_CEIL);
267    if (this.doubleTapScale > this.maxScale) {
268      this.maxScale = this.doubleTapScale * Constants.MAX_SCALE_EXTRA_FACTOR;
269    }
270    Log.debug(TAG, 'evaluateScales: ' + this.width + '*' + this.height + ' &' +
271    this.componentWidth + '*' + this.componentHeight +
272    ',max: ' + this.maxScale + ', most: [' + this.leftMost + ',' + this.topMost + '], double: ' + this.doubleTapScale);
273  }
274
275  private evaluateCompBounds(): number[] {
276    let scale = this.lastScale * this.scale;
277    let offset = this.evaluateOffset();
278    let result: number[] = [
279      offset[0] - this.componentWidth * (Number(scale.toFixed(Constants.RESERVED_DIGITS)) - Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) / Constants.NUMBER_2,
280      offset[1] - this.componentHeight * (Number(scale.toFixed(Constants.RESERVED_DIGITS)) - Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) / Constants.NUMBER_2
281    ];
282    Log.debug(TAG, 'evaluateCompBounds: ' + result);
283    return result;
284  }
285
286  private evaluateImgDisplaySize(): number[] {
287    let screenScale = 1;
288    let widthScale = this.componentWidth / this.item.imgWidth;
289    let heightScale = this.componentHeight / this.item.imgHeight;
290    screenScale = widthScale > heightScale ? heightScale : widthScale;
291    let scale = this.lastScale * this.scale * screenScale;
292    let imgDisplayWidth = 0;
293    let imgDisplayHeight = 0;
294    imgDisplayWidth = this.width * scale;
295    imgDisplayHeight = this.height * scale;
296    return [imgDisplayWidth, imgDisplayHeight];
297  }
298
299  private evaluateImgDisplayBounds(): number[] {
300    // For the left boundary of the component,
301    // the offset caused by amplification is - compw * (scale-1) / 2,
302    // plus the offset of the gesture to obtain the left boundary of the control.
303    // The same is true for the upper boundary
304    let scale = this.lastScale * this.scale;
305    let leftTop = this.evaluateCompBounds();
306    let imgDisplaySize: number[] = this.evaluateImgDisplaySize();
307    let imgDisplayWidth = imgDisplaySize[0];
308    let imgDisplayHeight = imgDisplaySize[1];
309    let imgLeftBound = 0;
310    let imgTopBound = 0;
311    if (this.width / this.height > this.componentWidth / this.componentHeight) {
312      imgLeftBound = leftTop[0];
313      imgTopBound = leftTop[1] + (this.componentHeight * scale - imgDisplayHeight) / Constants.NUMBER_2;
314    } else {
315      // Control width minus the picture width, divided by 2,
316      // you can get the distance from the left of the picture to the left of the control.
317      // Plus offsetX is the left boundary of the picture currently displayed
318      imgLeftBound = (this.componentWidth * scale - imgDisplayWidth) / Constants.NUMBER_2 + leftTop[0];
319      imgTopBound = leftTop[1];
320    }
321    return [imgLeftBound, imgTopBound];
322  }
323
324  // Calculate picture display boundary
325  private evaluateBounds(): void {
326    let imgDisplaySize: number[] = this.evaluateImgDisplaySize();
327    let imgDisplayWidth = imgDisplaySize[0];
328
329    let imgDisplayBounds = this.evaluateImgDisplayBounds();
330    let imgLeftBound = imgDisplayBounds[0];
331    this.hasReachLeft = imgLeftBound > -1;
332    this.hasReachRight = imgLeftBound + imgDisplayWidth < this.componentWidth + 1;
333  }
334
335  /**
336   * Calculate the upper and lower bounds of offset in X and Y directions under the current scale
337   *
338   * @param scale The display magnification of the current control, usually this.lastScale * this.scale
339   * @returns 0&1 X-direction offset lower & upper bound, 2&3 Y-direction offset lower & upper bound
340   */
341  private evaluateOffsetRange(scale: number): number[] {
342    let result: number[] = [0, 0, 0, 0];
343    let screenScale = 1;
344    let widthScale = this.componentWidth / this.item.imgWidth;
345    let heightScale = this.componentHeight / this.item.imgHeight;
346    screenScale = widthScale > heightScale ? heightScale : widthScale;
347    let left = (screenScale * scale * this.width - this.componentWidth) / Constants.NUMBER_2;
348    let top = (screenScale * scale * this.height - this.componentHeight) / Constants.NUMBER_2;
349    top = Math.max(top, 0);
350    left = Math.max(left, 0);
351    result = [-left, left, -top, top];
352    Log.debug(TAG, 'evaluateOffsetRange scale: ' + scale + ', defaultScale: ' + this.defaultScale + ', result: ' + result);
353    return result;
354  }
355
356  private emitPullDownToBackEvent(): void {
357    Log.debug(TAG, 'emitPullDownToBackEvent');
358    if (this.isExiting) {
359      Log.info(TAG, 'emitPullDownToBack isExiting: ' + this.isExiting);
360      return;
361    }
362    this.broadCast.emit(Constants.PULL_DOWN_END, []);
363    this.isExiting = true;
364  }
365
366  private emitPullDownCancelEvent(): void {
367    Log.debug(TAG, 'emitPullDownCancelEvent');
368    this.broadCast.emit(Constants.PULL_DOWN_CANCEL, []);
369  }
370
371  onMoveStart(offsetX: number, offsetY: number): void {
372    if (this.isInAnimation || this.isExiting) {
373      return;
374    }
375    // Reset offset at the beginning of dragging to prevent jumping
376    this.offset = [0, 0];
377    this.evaluateBounds();
378    let scale = this.lastScale * this.scale;
379    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
380      // Hide bars with zoom drag
381      this.broadCast.emit(Constants.HIDE_BARS, []);
382    }
383    if (scale.toFixed(Constants.RESERVED_DIGITS) === this.defaultScale.toFixed(Constants.RESERVED_DIGITS) && offsetY > 0) {
384      // Drop down return to hide details first
385      this.broadCast.emit(Constants.PULL_DOWN_START, []);
386    }
387  }
388
389  /**
390   * Each callback returns the displacement relative to the start point of the gesture
391   *
392   * @param offsetX offsetX
393   * @param offsetY offsetY
394   */
395  onMove(offsetX: number, offsetY: number): void {
396    if (this.isInAnimation || this.isExiting) {
397      return;
398    }
399    let scale = this.lastScale * this.scale;
400    let limits = this.evaluateOffsetRange(scale);
401    let measureX = this.lastOffset[0] + (this.center[0] - Constants.CENTER_DEFAULT) * this.componentWidth
402    * (this.defaultScale - this.scale) * this.lastScale;
403    let measureY = this.lastOffset[1] + (this.center[1] - Constants.CENTER_DEFAULT) * this.componentHeight
404    * (this.defaultScale - this.scale) * this.lastScale;
405    let moveX = offsetX;
406    let moveY = offsetY;
407    let offX = measureX + moveX;
408    let offY = measureY + moveY;
409    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
410      // The offset in the X direction is always limited for non shrinking scenes
411      offX = MathUtils.clamp(offX, limits[0], limits[1]);
412      if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
413        // cannot drop down to return to the scene, limit y
414        offY = MathUtils.clamp(offY, limits[Constants.NUMBER_2], limits[Constants.NUMBER_3]);
415      } else {
416        // pull down to return to the scene, and only limit y to drag upward, that is, limit the lower bound
417        offY = Math.max(limits[Constants.NUMBER_2], offY);
418      }
419    }
420    let tmpX = offX - measureX;
421    let tmpY = offY - measureY;
422    this.offset = [tmpX, tmpY];
423    this.emitTouchEvent();
424  }
425
426  onMoveEnd(offsetX, offsetY): void {
427    if (this.isInAnimation || this.isExiting) {
428      return;
429    }
430    let scale = this.lastScale * this.scale;
431    Log.debug(TAG, 'onMoveEnd: scale is ' + scale + ' offsetY is ' + offsetY);
432    if (scale.toFixed(Constants.RESERVED_DIGITS) === this.defaultScale.toFixed(Constants.RESERVED_DIGITS) && offsetY > Constants.PULL_DOWN_THRESHOLD) {
433      this.emitPullDownToBackEvent();
434    } else if (scale.toFixed(Constants.RESERVED_DIGITS) === this.defaultScale.toFixed(Constants.RESERVED_DIGITS)) {
435      // The reset animation is triggered when the threshold is not reached
436      let scaleOption: Matrix4.ScaleOption = {
437        x: this.defaultScale,
438        y: this.defaultScale
439      };
440      this.startAnimation(Matrix4.identity().scale(scaleOption).copy() as Matrix4TransitWithMatrix4x4);
441      this.emitPullDownCancelEvent();
442    } else {
443      this.emitDirectionChange();
444    }
445  }
446
447  onScaleStart(scale: number, centerX: number, centerY: number): void {
448    Log.info(TAG, 'onScaleStart: ' + this.isInAnimation + ', ' + this.isExiting);
449    if (this.isInAnimation || this.isExiting) {
450      return;
451    }
452    this.scale = 1;
453    this.evaluateBounds();
454    // Adjust action bar status
455    this.broadCast.emit(Constants.HIDE_BARS, []);
456    this.center = this.evaluateCenter(centerX, centerY);
457  }
458
459  /**
460   * Calculates the percentage position of the current zoom center relative to the control
461   *
462   * @param centerX The absolute position of the touch point on the screen
463   * @param centerY The absolute position of the touch point on the screen
464   * @returns The percentage position of the current zoom center relative to the control
465   */
466  private evaluateCenter(centerX: number, centerY: number): number[] {
467    // Calculate the coordinates of the upper left corner of the control relative to
468    // the upper left corner of the current display
469    let scale = this.lastScale * this.scale;
470    let leftTop = this.evaluateCompBounds();
471
472    // Get the touch coordinates relative to the control
473    let cxRelativeToComp = MathUtils.clamp((centerX - leftTop[0])
474    / (this.componentWidth * scale), this.leftMost, 1 - this.leftMost);
475    let cyRelativeToComp = MathUtils.clamp((centerY - leftTop[1])
476    / (this.componentHeight * scale), this.topMost, 1 - this.topMost);
477
478    let imgDisplaySize: number[] = this.evaluateImgDisplaySize();
479    let imgDisplayWidth = imgDisplaySize[0];
480    let imgDisplayHeight = imgDisplaySize[1];
481
482    let imgDisplayBounds = this.evaluateImgDisplayBounds();
483    let imgLeftBound = imgDisplayBounds[0];
484    let imgTopBound = imgDisplayBounds[1];
485
486    // When the touch center point is outside the picture display area, take the midpoint
487    if (this.width / this.height > this.componentWidth / this.componentHeight) {
488      if (centerY < imgTopBound || centerY > imgTopBound + imgDisplayHeight) {
489        cyRelativeToComp = Constants.CENTER_DEFAULT;
490      }
491    } else {
492      if (centerX < imgLeftBound || centerX > imgLeftBound + imgDisplayWidth) {
493        cxRelativeToComp = Constants.CENTER_DEFAULT;
494      }
495    }
496
497    // Calculate the percentage of the center point of the touch
498    let center: number[] = [cxRelativeToComp, cyRelativeToComp];
499    Log.debug(TAG, 'evaluateCenter center: ' + center + ', ' + centerX + ',' + centerY +
500    ',size: ' + imgDisplaySize + ', bounds: ' + imgDisplayBounds + ', leftTop: ' + leftTop +
501    ',compSize: ' + this.componentWidth * scale + ',' + this.componentHeight * scale);
502    return center;
503  }
504
505  onScale(scale: number): void {
506    Log.debug(TAG, 'onScale: ' + this.isInAnimation + ', ' + this.isExiting + ', scale: ' + scale);
507    if (this.isInAnimation || this.isExiting) {
508      return;
509    }
510    this.evaluateBounds();
511    this.scale = scale;
512    if (this.lastScale * scale <= Constants.COMPONENT_SCALE_FLOOR) {
513      this.scale = Constants.COMPONENT_SCALE_FLOOR / this.lastScale;
514    }
515    if (this.lastScale * scale >= this.maxScale * Constants.OVER_SCALE_EXTRA_FACTOR) {
516      this.scale = this.maxScale * Constants.OVER_SCALE_EXTRA_FACTOR / this.lastScale;
517    }
518    this.emitTouchEvent();
519  }
520
521  onScaleEnd(): void {
522    Log.info(TAG, 'onScaleEnd: ' + this.isInAnimation + ', ' + this.isExiting);
523    if (this.isInAnimation || this.isExiting) {
524      return;
525    }
526    this.evaluateBounds();
527    let scale = this.lastScale * this.scale;
528    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) >= Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS)) && scale <= this.maxScale) {
529      Log.info(TAG, 'does not need to do animation: ' + scale);
530      this.emitDirectionChange();
531      return;
532    }
533    let animationEndMatrix: Matrix4.Matrix4Transit = null;
534    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) <= Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
535      // Zoom out too small to trigger the restored animation
536      let scaleOption: Matrix4.ScaleOption = {
537        x: this.defaultScale,
538        y: this.defaultScale
539      };
540      animationEndMatrix = Matrix4.identity().scale(scaleOption).copy();
541    } else {
542      // Do the animation of retracting maxScale when zooming in
543      animationEndMatrix = this.evaluateAnimeMatrix(this.maxScale, this.center);
544    }
545    this.startAnimation(animationEndMatrix as Matrix4TransitWithMatrix4x4);
546  }
547
548  private evaluateAnimeMatrix(scale: number, center: number[]): Matrix4.Matrix4Transit {
549    let offset: number[] = [
550      this.lastOffset[0] + this.offset[0] + (center[0] - Constants.CENTER_DEFAULT) * this.componentWidth
551      * (this.defaultScale - scale / this.lastScale) * this.lastScale,
552      this.lastOffset[1] + this.offset[1] + (center[1] - Constants.CENTER_DEFAULT) * this.componentHeight
553      * (this.defaultScale - scale / this.lastScale) * this.lastScale
554    ];
555    if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
556      let limits = this.evaluateOffsetRange(scale);
557      // The offset in the X direction is always limited for non shrinking scenes
558      offset[0] = MathUtils.clamp(offset[0], limits[0], limits[1]);
559      if (Number(scale.toFixed(Constants.RESERVED_DIGITS)) > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
560        // Cannot drop down to return to the scene, limit y
561        offset[1] = MathUtils.clamp(offset[1], limits[Constants.NUMBER_2], limits[Constants.NUMBER_3]);
562      } else {
563        // You can pull down to return to the scene, and only limit y to drag upward,
564        // that is, limit the lower bound
565        offset[1] = Math.max(limits[Constants.NUMBER_2], offset[1]);
566      }
567    } else {
568      // When zooming in, adjust the zoom center to the display center point
569      offset = [0, 0];
570    }
571    let scaleOption: Matrix4.ScaleOption = {
572      x: scale,
573      y: scale,
574    };
575    let translateOption: Matrix4.TranslateOption = {
576      x: offset[0],
577      y: offset[1]
578    };
579    let animationEndMatrix = Matrix4.identity()
580      .copy()
581      .scale(scaleOption)
582      .translate(translateOption)
583      .copy();
584    Log.debug(TAG, 'evaluateAnimeMatrix scale:' + scale + ', center:' + center);
585    return animationEndMatrix;
586  }
587
588  /**
589   * Double click to trigger zoom.
590   * If the current scale is less than or equal to 1, zoom to doubleTapScale;
591   * If the current scale is greater than 1, scale to 1;
592   *
593   * @param centerX the location of double click
594   * @param centerY the location of double click
595   */
596  onDoubleTap(centerX: number, centerY: number): void {
597    if (this.isInAnimation || this.isExiting) {
598      Log.debug(TAG, 'onDoubleTap not avaliable: ' + this.isInAnimation + ', ' + this.isExiting);
599      return;
600    }
601    // Adjust action bar status
602    this.broadCast.emit(Constants.HIDE_BARS, []);
603    let matrix: Matrix4TransitWithMatrix4x4;
604    Log.debug(TAG, 'onDoubleTap lastScale: ' + this.lastScale + ', scale: ' + this.scale + ', defaultScale: ' + this.defaultScale);
605    if (Number(this.lastScale.toFixed(Constants.RESERVED_DIGITS)) * this.scale > Number(this.defaultScale.toFixed(Constants.RESERVED_DIGITS))) {
606      // Scale to original state when scale is greater than 1
607      let scaleOption: Matrix4.ScaleOption = {
608        x: this.defaultScale,
609        y: this.defaultScale
610      };
611      matrix = Matrix4.identity().scale(scaleOption).copy() as Matrix4TransitWithMatrix4x4;
612    } else {
613      // The zoom in status calculates the zoom in center according to the click position
614      let center = this.evaluateCenter(centerX, centerY);
615      // When the picture aspect ratio is less than the control aspect ratio,
616      // centerX is set to 0.5,
617      // whereas centerY is set to 0.5 to ensure that
618      // the short side is close to the side after double clicking and enlarging
619      if (this.width / this.height < this.componentWidth / this.componentHeight) {
620        center = [Constants.CENTER_DEFAULT, center[1]];
621      } else {
622        center = [center[0], Constants.CENTER_DEFAULT];
623      }
624      matrix = this.evaluateAnimeMatrix(this.doubleTapScale * this.defaultScale, center) as Matrix4TransitWithMatrix4x4;
625    }
626    Log.debug(TAG, 'onDoubleTap matrix: ' + matrix.matrix4x4);
627    this.startAnimation(matrix);
628  }
629
630  reset(): void {
631    this.lastOffset = [0, 0];
632    this.offset = [0, 0];
633    this.lastScale = 1.0;
634    this.scale = 1;
635    this.hasReachLeft = true;
636    this.hasReachRight = true;
637    this.hasReachTop = true;
638    this.hasReachBottom = true;
639    this.isInAnimation = false;
640    this.isExiting = false;
641    this.emitDirectionChange();
642  }
643
644  onDisAppear(): void {
645    Log.info(TAG, 'onDisAppear');
646  }
647
648  private startAnimation(animationEndMatrix: Matrix4TransitWithMatrix4x4): void {
649    this.isInAnimation = true;
650    let animationOption: AnimationOption = {
651      duration: Constants.OVER_SCALE_ANIME_DURATION,
652      curve: Curve.Ease
653    };
654    Log.debug(TAG, 'animationEndMatrix: ' + animationEndMatrix.matrix4x4);
655    this.broadCast.emit(Constants.ANIMATION_EVENT + this.item.uri + this.timeStamp, [animationOption, animationEndMatrix]);
656  }
657
658  /**
659   * At the end of the animation,
660   * refresh the current parameter values according to the end transformation matrix to ensure continuity and
661   * prevent jumping during the next gesture operation
662   *
663   * @param animationEndMatrix Transformation matrix at end
664   */
665  onAnimationEnd(animationEndMatrix: Matrix4TransitWithMatrix4x4): void {
666    if (animationEndMatrix != null) {
667      Log.info(TAG, 'onAnimationEnd: ' + animationEndMatrix.matrix4x4);
668      this.lastScale = animationEndMatrix.matrix4x4[0];
669      this.scale = 1;
670      this.lastOffset = [animationEndMatrix.matrix4x4[Constants.NUMBER_12], animationEndMatrix.matrix4x4[Constants.NUMBER_13]];
671      this.offset = [0, 0];
672      this.evaluateBounds();
673      this.isInAnimation = false;
674      this.emitDirectionChange();
675    }
676  }
677}
678