• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16import animator, { AnimatorResult } from '@ohos.animator';
17import { vibrator } from '@kit.SensorServiceKit';
18import { BusinessError } from '@kit.BasicServicesKit';
19import { common2D, drawing } from '@kit.ArkGraphics2D';
20import { ColorMetrics } from '@ohos.arkui.node';
21import hilog from '@ohos.hilog';
22import display from '@ohos.display';
23import { PathShape } from '@kit.ArkUI';
24import { systemDateTime } from '@kit.BasicServicesKit';
25
26const ANGLE_TO_RADIAN = Math.PI / 180; // Common or specific numerical values
27const RADIAN_TO_ANGLE = 180 / Math.PI;
28const PI_ANGLE = 180;
29const TWO_PI_RADIAN = 2 * Math.PI;
30const APPROXIMATE_NUMBER = Math.pow(10, -7);
31const ANGLE_OVER_MIN = 10 * ANGLE_TO_RADIAN;
32const LENGTH_OVER_MIN = 0.15;
33const INVALID_TIMEOUT_ID = -1;
34const RESTORE_TIMEOUT = 3000;
35
36const PROGRESS_DEFAULT = 0; // Default value
37const MIN_DEFAULT = 0;
38const MAX_DEFAULT = 100;
39const PADDING_DEFAULT = 5.5;
40const START_ANGLE_DEFAULT = 15;
41const END_ANGLE_DEFAULT = 45;
42const ACTIVE_START_ANGLE_DEFAULT = -60;
43const ACTIVE_END_ANGLE_DEFAULT = 60;
44const REVERSE_DEFAULT = true;
45const TRACK_THICKNESS_DEFAULT = 5;
46const ACTIVE_TRACK_THICKNESS_DEFAULT = 24;
47const TRACK_THICKNESS_MAX = 16;
48const ACTIVE_TRACK_THICKNESS_MAX = 36;
49const TRACK_BLUR_DEFAULT = 20;
50const TRACK_COLOR_DEFAULT = '#33FFFFFF';
51const SELECTED_COLOR_DEFAULT = '#FF5EA1FF';
52const BLUR_COLOR_DEFAULT = $r('sys.color.ohos_id_color_subtab_bg');
53const TOUCH_ANIMATION_DURATION = 200;
54const LIMIT_RESTORE_ANIMATION_DURATION = 333;
55const RESTORE_ANIMATION_DURATION = 167;
56const DIAMETER_DEFAULT = 233;
57
58const VIBRATOR_TYPE_TWO = 'watchhaptic.feedback.crown.strength2'; // About crown
59const CROWN_TIME_FLAG = 30;
60const CROWN_CONTROL_RATIO = 2.10;
61const CROWN_SENSITIVITY_LOW = 0.5;
62const CROWN_SENSITIVITY_MEDIUM = 1;
63const CROWN_SENSITIVITY_HIGH = 2;
64
65export enum AnimatorStatus {
66  MIN,
67  MAX,
68  NORMAL,
69}
70
71export enum ClipPathArc {
72  ARC1,
73  ARC2,
74  ARC3,
75  ARC4
76}
77
78export enum ArcSliderPosition {
79  LEFT,
80  RIGHT
81}
82
83export interface ArcSliderValueOptionsConstructorOptions {
84  progress?: number;
85  min?: number;
86  max?: number;
87}
88
89export interface ArcSliderLayoutOptionsConstructorOptions {
90  reverse?: boolean;
91  position?: ArcSliderPosition;
92}
93
94export interface ArcSliderStyleOptionsConstructorOptions {
95  trackThickness?: number;
96  activeTrackThickness?: number;
97  trackColor?: string;
98  selectedColor?: string;
99  trackBlur?: number;
100}
101
102export interface ArcSliderOptionsConstructorOptions {
103  valueOptions?: ArcSliderValueOptions;
104  layoutOptions?: ArcSliderLayoutOptions;
105  styleOptions?: ArcSliderStyleOptions;
106  digitalCrownSensitivity?: CrownSensitivity;
107  onTouch?: Callback<TouchEvent>;
108  onChange?: Callback<number>;
109  onEnlarge?: Callback<boolean>;
110}
111
112@ObservedV2
113export class ArcSliderValueOptions {
114  @Trace public progress: number;
115  @Trace public min: number;
116  @Trace public max: number;
117
118  constructor(options?: ArcSliderValueOptionsConstructorOptions) {
119    this.progress = options?.progress ?? PROGRESS_DEFAULT;
120    this.min = options?.min ?? MIN_DEFAULT;
121    this.max = options?.max ?? MAX_DEFAULT;
122  }
123}
124
125@ObservedV2
126export class ArcSliderLayoutOptions {
127  @Trace public reverse: boolean;
128  @Trace public position: ArcSliderPosition;
129
130  constructor(options?: ArcSliderLayoutOptionsConstructorOptions) {
131    this.reverse = options?.reverse ?? REVERSE_DEFAULT;
132    this.position = options?.position ?? ArcSliderPosition.RIGHT;
133  }
134}
135
136@ObservedV2
137export class ArcSliderStyleOptions {
138  @Trace public trackThickness: number;
139  @Trace public activeTrackThickness: number;
140  @Trace public trackColor: string;
141  @Trace public selectedColor: string;
142  @Trace public trackBlur: number;
143
144  constructor(options?: ArcSliderStyleOptionsConstructorOptions) {
145    this.trackThickness = options?.trackThickness ?? TRACK_THICKNESS_DEFAULT;
146    this.activeTrackThickness = options?.activeTrackThickness ?? ACTIVE_TRACK_THICKNESS_DEFAULT;
147    this.trackColor = options?.trackColor ?? TRACK_COLOR_DEFAULT;
148    this.selectedColor = options?.selectedColor ?? SELECTED_COLOR_DEFAULT;
149    this.trackBlur = options?.trackBlur ?? TRACK_BLUR_DEFAULT;
150  }
151}
152
153@ObservedV2
154export class ArcSliderOptions {
155  @Trace public valueOptions: ArcSliderValueOptions;
156  @Trace public layoutOptions: ArcSliderLayoutOptions;
157  @Trace public styleOptions: ArcSliderStyleOptions;
158  @Trace public digitalCrownSensitivity: CrownSensitivity;
159  @Trace public onTouch: Callback<TouchEvent>;
160  @Trace public onChange: Callback<number>;
161  @Trace public onEnlarge: Callback<boolean>;
162
163  constructor(options?: ArcSliderOptionsConstructorOptions) {
164    this.valueOptions = options?.valueOptions ?? new ArcSliderValueOptions();
165    this.layoutOptions = options?.layoutOptions ?? new ArcSliderLayoutOptions();
166    this.styleOptions = options?.styleOptions ?? new ArcSliderStyleOptions();
167    this.digitalCrownSensitivity = options?.digitalCrownSensitivity ?? CrownSensitivity.MEDIUM;
168    this.onTouch = options?.onTouch ?? ((event: TouchEvent) => {
169    });
170    this.onChange = options?.onChange ?? ((progress: number) => {
171    });
172    this.onEnlarge = options?.onEnlarge ?? ((isEnlarged: boolean) => {
173    });
174  }
175}
176
177export class DrawParameters {
178  public lineWidth: number = 0;
179  public radius: number = 0;
180  public trackEndAngle: number = 0;
181  public trackStartAngle: number = 0;
182  public selectedStartAngle: number = 0;
183  public selectedEndAngle: number = 0;
184  public trackColor: ColorMetrics = ColorMetrics.resourceColor(TRACK_COLOR_DEFAULT);
185  public selectedColor: ColorMetrics = ColorMetrics.resourceColor(SELECTED_COLOR_DEFAULT);
186  public x: number = 0;
187  public y: number = 0;
188  public blur: number = TRACK_BLUR_DEFAULT;
189  public uiContext: UIContext | undefined = undefined;
190}
191
192export function nearEqual(num1: number, num2: number): boolean {
193  return Math.abs(num1 - num2) < APPROXIMATE_NUMBER;
194}
195
196export class MyFullDrawModifier extends DrawModifier {
197  public parameters: DrawParameters = new DrawParameters();
198
199  constructor(parameters: DrawParameters) {
200    super();
201    this.parameters = parameters;
202  }
203
204  private parseColorString(color: ColorMetrics): common2D.Color {
205    return { alpha: color.alpha, red: color.red, green: color.green, blue: color.blue };
206  }
207
208  private drawTrack(context: DrawContext) {
209    if (this.parameters.uiContext === undefined) {
210      hilog.error(0x3900, 'ArcSlider', `uiContext is undefined`);
211      return;
212    }
213    const canvas = context.canvas;
214    const pen = new drawing.Pen();
215    pen.setAntiAlias(true);
216    pen.setColor(this.parseColorString(this.parameters.trackColor));
217    pen.setStrokeWidth(this.parameters.uiContext.vp2px(this.parameters.lineWidth));
218    pen.setCapStyle(drawing.CapStyle.ROUND_CAP);
219    canvas.attachPen(pen);
220    const path = new drawing.Path();
221    const leftTopX = this.parameters.uiContext.vp2px(this.parameters.x - this.parameters.radius);
222    const leftTopY = this.parameters.uiContext.vp2px(this.parameters.y - this.parameters.radius);
223    const rightBottomX = this.parameters.uiContext.vp2px(this.parameters.x + this.parameters.radius);
224    const rightBottomY = this.parameters.uiContext.vp2px(this.parameters.y + this.parameters.radius);
225    let startAngle: number;
226    let sweepAngle: number;
227    startAngle = this.parameters.trackEndAngle * RADIAN_TO_ANGLE;
228    sweepAngle = (this.parameters.trackStartAngle - this.parameters.trackEndAngle) * RADIAN_TO_ANGLE;
229    path.arcTo(leftTopX, leftTopY, rightBottomX, rightBottomY, startAngle, sweepAngle);
230    canvas.drawPath(path);
231    canvas.detachPen();
232  }
233
234  private drawSelection(context: DrawContext) {
235    if (this.parameters.uiContext === undefined) {
236      hilog.error(0x3900, 'ArcSlider', `uiContext is undefined`);
237      return;
238    }
239    if (nearEqual(this.parameters.selectedStartAngle, this.parameters.selectedEndAngle)) {
240      return;
241    }
242    const canvas = context.canvas;
243    const pen = new drawing.Pen();
244    pen.setAntiAlias(true);
245    pen.setColor(this.parseColorString(this.parameters.selectedColor));
246    pen.setStrokeWidth(this.parameters.uiContext.vp2px(this.parameters.lineWidth));
247    pen.setCapStyle(drawing.CapStyle.ROUND_CAP);
248    canvas.attachPen(pen);
249    let path = new drawing.Path();
250    const leftTopX = this.parameters.uiContext.vp2px(this.parameters.x - this.parameters.radius);
251    const leftTopY = this.parameters.uiContext.vp2px(this.parameters.y - this.parameters.radius);
252    const rightBottomX = this.parameters.uiContext.vp2px(this.parameters.x + this.parameters.radius);
253    const rightBottomY = this.parameters.uiContext.vp2px(this.parameters.y + this.parameters.radius);
254    let startAngle: number;
255    let sweepAngle: number;
256    startAngle = this.parameters.selectedEndAngle * RADIAN_TO_ANGLE;
257    sweepAngle = (this.parameters.selectedStartAngle - this.parameters.selectedEndAngle) * RADIAN_TO_ANGLE;
258    path.arcTo(leftTopX, leftTopY, rightBottomX, rightBottomY, startAngle, sweepAngle);
259    canvas.drawPath(path);
260    canvas.detachPen();
261  }
262
263  public drawContent(context: DrawContext): void {
264    this.drawTrack(context);
265  }
266
267  public drawFront(context: DrawContext): void {
268    this.drawSelection(context);
269  }
270}
271
272@ComponentV2
273export struct ArcSlider {
274  @Param options: ArcSliderOptions = new ArcSliderOptions();
275  @Local lineWidth: number = 0;
276  @Local radius: number = 0;
277  @Local trackStartAngle: number = 0;
278  @Local trackEndAngle: number = 0;
279  @Local selectedStartAngle: number = 0;
280  @Local selectedEndAngle: number = 0;
281  @Local selectRatioNow: number = 0;
282  @Local isEnlarged: boolean = false;
283  @Local clipPath: string = '';
284  @Local isReverse: boolean = false;
285  @Local isLargeArc: boolean = false;
286  @Local isFocus: boolean = false;
287  private timePre: number = 0;
288  private timeCur: number = 0;
289  private parameters: DrawParameters = new DrawParameters();
290  private fullModifier: MyFullDrawModifier = new MyFullDrawModifier(this.parameters);
291  private touchAnimator: AnimatorResult | undefined = undefined;
292  private restoreAnimator: AnimatorResult | undefined = undefined;
293  private maxRestoreAnimator: AnimatorResult | undefined = undefined;
294  private minRestoreAnimator: AnimatorResult | undefined = undefined;
295  private delta: number = 0;
296  private crownDeltaAngle: number = 0;
297  private lineWidthBegin: number = 0;
298  private touchY: number = 0;
299  private meter: number = 0;
300  private trackStartAngleBegin: number = 0;
301  private selectedEndAngleBegin: number = 0;
302  private isTouchAnimatorFinished: boolean = false;
303  private clickValue: number = 0;
304  private normalStartAngle: number = 0;
305  private normalEndAngle: number = 0;
306  private activeStartAngle: number = 0;
307  private activeEndAngle: number = 0;
308  private selectedMaxOrMin: number = AnimatorStatus.NORMAL;
309  private needVibrate: boolean = true;
310  private crownEventCounter: number = 0;
311  private diameter: number = 0;
312  private isAntiClock: boolean = true;
313  private normalRadius: number = 0;
314
315  @Monitor('trackStartAngle', 'trackEndAngle', 'selectedStartAngle', 'selectedEndAngle', 'options.valueOptions.min',
316  'options.valueOptions.max', 'options.valueOptions.progress', 'options.layoutOptions.reverse',
317  'options.layoutOptions.position', 'options.styleOptions.trackThickness', 'options.styleOptions.activeTrackThickness',
318  'options.styleOptions.trackColor', 'options.styleOptions.selectedColor', 'options.styleOptions.trackBlur', 'clipPath',
319  'isFocus', 'isAntiClock')
320  onChange(monitor: IMonitor) {
321    monitor.dirty.forEach((path: string) => {
322      const isAntiClockChanged: boolean = monitor.value('isAntiClock')?.now !== monitor.value('isAntiClock')?.before;
323      const isReverseChanged: boolean = monitor.value('options.layoutOptions.reverse')?.now !==
324        monitor.value('options.layoutOptions.reverse')?.before;
325      const isThicknessChanged: boolean = monitor.value('options.styleOptions.activeTrackThickness')?.now !==
326        monitor.value('options.styleOptions.activeTrackThickness')?.before;
327      if (isAntiClockChanged || isReverseChanged) {
328        this.selectedStartAngle = this.activeStartAngle;
329        this.trackEndAngle = this.activeEndAngle;
330        this.trackStartAngle = this.activeStartAngle;
331      }
332      if (isThicknessChanged) {
333        this.lineWidth = this.options.styleOptions.activeTrackThickness;
334      }
335      this.updateArcSlider();
336    })
337  }
338
339  aboutToAppear() {
340    this.updateArcSlider();
341    this.resetDrawing();
342    this.setTouchAnimator();
343    this.setMaxRestoreAnimator();
344    this.setMinRestoreAnimator();
345    this.setRestoreAnimator();
346    this.setDiameter();
347  }
348
349  aboutToDisappear() {
350    clearTimeout(this.meter);
351    this.touchAnimator = undefined;
352    this.restoreAnimator = undefined;
353    this.maxRestoreAnimator = undefined;
354    this.minRestoreAnimator = undefined;
355  }
356
357  setTouchAnimator() {
358    this.touchAnimator = animator.create({
359      duration: TOUCH_ANIMATION_DURATION,
360      easing: 'friction',
361      delay: 0,
362      fill: 'forwards',
363      direction: 'normal',
364      iterations: 1,
365      begin: 0,
366      end: 1
367    })
368    this.touchAnimator.onFrame = (fraction: number) => {
369      this.lineWidth = this.calcAnimatorChange(this.options.styleOptions.trackThickness,
370        this.options.styleOptions.activeTrackThickness, fraction);
371      this.selectedStartAngle = this.calcAnimatorChange(this.normalStartAngle, this.activeStartAngle, fraction);
372      this.trackStartAngle = this.selectedStartAngle;
373      this.trackEndAngle = this.calcAnimatorChange(this.normalEndAngle, this.activeEndAngle, fraction);
374      this.resetDrawing();
375    }
376    this.touchAnimator.onFinish = () => {
377      this.isTouchAnimatorFinished = true;
378    }
379  }
380
381  startTouchAnimator() {
382    if (this.touchAnimator) {
383      this.options.onEnlarge?.(true);
384      this.touchAnimator.play();
385    }
386  }
387
388  setMaxRestoreAnimator() {
389    this.maxRestoreAnimator = animator.create({
390      duration: LIMIT_RESTORE_ANIMATION_DURATION,
391      easing: 'sharp',
392      delay: 0,
393      fill: 'forwards',
394      direction: 'normal',
395      iterations: 1,
396      begin: 0,
397      end: 1
398    })
399    this.maxRestoreAnimator.onFrame = (fraction: number) => {
400      this.lineWidth = this.calcAnimatorChange(this.lineWidthBegin, this.options.styleOptions.activeTrackThickness,
401        fraction);
402      this.selectedEndAngle = this.calcAnimatorChange(this.selectedEndAngleBegin, this.activeEndAngle, fraction);
403      this.trackEndAngle = this.selectedEndAngle;
404      this.resetDrawing();
405    }
406    this.maxRestoreAnimator.onFinish = () => {
407      this.selectedMaxOrMin = AnimatorStatus.NORMAL;
408    }
409  }
410
411  startMaxRestoreAnimator() {
412    if (this.maxRestoreAnimator) {
413      this.maxRestoreAnimator.play();
414    }
415  }
416
417  setMinRestoreAnimator() {
418    this.minRestoreAnimator = animator.create({
419      duration: LIMIT_RESTORE_ANIMATION_DURATION,
420      easing: 'sharp',
421      delay: 0,
422      fill: 'forwards',
423      direction: 'normal',
424      iterations: 1,
425      begin: 0,
426      end: 1
427    })
428    this.minRestoreAnimator.onFrame = (fraction: number) => {
429      this.lineWidth = this.calcAnimatorChange(this.lineWidthBegin, this.options.styleOptions.activeTrackThickness,
430        fraction);
431      this.trackStartAngle = this.calcAnimatorChange(this.trackStartAngleBegin, this.activeStartAngle,
432        fraction);
433      this.resetDrawing();
434    }
435    this.minRestoreAnimator.onFinish = () => {
436      this.selectedMaxOrMin = AnimatorStatus.NORMAL;
437    }
438  }
439
440  startMinRestoreAnimator() {
441    if (this.minRestoreAnimator) {
442      this.minRestoreAnimator.play();
443    }
444  }
445
446  setRestoreAnimator() {
447    this.restoreAnimator = animator.create({
448      duration: RESTORE_ANIMATION_DURATION,
449      easing: 'friction',
450      delay: 0,
451      fill: 'forwards',
452      direction: 'normal',
453      iterations: 1,
454      begin: 0,
455      end: 1
456    })
457    this.restoreAnimator.onFrame = (fraction: number) => {
458      this.lineWidth = this.calcAnimatorChange(this.options.styleOptions.activeTrackThickness,
459        this.options.styleOptions.trackThickness, fraction)
460      this.selectedStartAngle = this.calcAnimatorChange(this.activeStartAngle, this.normalStartAngle, fraction) *
461        ANGLE_TO_RADIAN;
462      this.trackStartAngle = this.selectedStartAngle;
463      this.trackEndAngle = this.calcAnimatorChange(this.activeEndAngle, this.normalEndAngle, fraction) *
464        ANGLE_TO_RADIAN;
465      this.resetDrawing();
466    }
467  }
468
469  startRestoreAnimator() {
470    if (this.restoreAnimator) {
471      this.options.onEnlarge?.(false);
472      this.restoreAnimator.play();
473    }
474  }
475
476  updateArcSlider() {
477    this.checkParam();
478    this.setLayoutState(this.options.layoutOptions.reverse, this.options.layoutOptions.position);
479    this.resetDrawing();
480  }
481
482  private setLimitValues(attr: number, defaultValue: number, min: number, max?: number): number {
483    if (attr < min) {
484      attr = defaultValue;
485    }
486    if (max != undefined && attr > max) {
487      attr = defaultValue;
488    }
489    return attr;
490  }
491
492  private checkParam() {
493    if (this.options.valueOptions.max === this.options.valueOptions.min ||
494      this.options.valueOptions.max < this.options.valueOptions.min) {
495      this.options.valueOptions.max = MAX_DEFAULT;
496      this.options.valueOptions.min = MIN_DEFAULT;
497      this.options.valueOptions.progress = PROGRESS_DEFAULT;
498    }
499    this.options.valueOptions.progress = Math.min(this.options.valueOptions.max, this.options.valueOptions.progress);
500    this.options.valueOptions.progress = Math.max(this.options.valueOptions.min, this.options.valueOptions.progress);
501    this.options.styleOptions.trackBlur = this.setLimitValues(this.options.styleOptions.trackBlur, TRACK_BLUR_DEFAULT,
502      0);
503    this.options.styleOptions.trackThickness = this.setLimitValues(this.options.styleOptions.trackThickness,
504      TRACK_THICKNESS_DEFAULT, TRACK_THICKNESS_DEFAULT, TRACK_THICKNESS_MAX);
505    this.options.styleOptions.activeTrackThickness = this.setLimitValues(this.options.styleOptions.activeTrackThickness,
506      ACTIVE_TRACK_THICKNESS_DEFAULT, ACTIVE_TRACK_THICKNESS_DEFAULT, ACTIVE_TRACK_THICKNESS_MAX);
507  }
508
509  private updateModifier() {
510    this.parameters.lineWidth = this.lineWidth;
511    this.parameters.radius = this.radius;
512    this.parameters.selectedStartAngle = this.selectedStartAngle;
513    this.parameters.trackEndAngle = this.trackEndAngle;
514    this.parameters.trackStartAngle = this.trackStartAngle;
515    this.parameters.selectedEndAngle = this.selectedEndAngle;
516    try {
517      this.parameters.trackColor = ColorMetrics.resourceColor(this.options.styleOptions.trackColor);
518    } catch (err) {
519      let error = err as BusinessError;
520      console.error(`Failed to set track color, code = ${error.code}, message =${error.message}`);
521      this.parameters.trackColor = ColorMetrics.resourceColor(TRACK_COLOR_DEFAULT);
522    }
523    try {
524      this.parameters.selectedColor = ColorMetrics.resourceColor(this.options.styleOptions.selectedColor);
525    } catch (err) {
526      let error = err as BusinessError;
527      console.error(`Failed to set selected color, code = ${error.code}, message =${error.message}`);
528      this.parameters.selectedColor = ColorMetrics.resourceColor(SELECTED_COLOR_DEFAULT);
529    }
530    this.parameters.blur = this.options.styleOptions.trackBlur;
531  }
532
533  setDiameter() {
534    let width: number;
535    try {
536      width = display.getDefaultDisplaySync().width;
537    } catch (err) {
538      let error = err as BusinessError;
539      console.error(`Failed to get default display width, code = ${error.code}, message =${error.message}`);
540      width = 0;
541    }
542    this.parameters.uiContext = this.getUIContext();
543    if (this.parameters.uiContext) {
544      if (width !== 0) {
545        this.diameter = this.parameters.uiContext.px2vp(width);
546      } else {
547        this.diameter = DIAMETER_DEFAULT;
548      }
549    }
550    this.normalRadius = this.diameter / 2;
551    this.parameters.x = this.normalRadius;
552    this.parameters.y = this.normalRadius;
553  }
554
555  resetDrawing() {
556    this.setLayoutOptions();
557    this.updateModifier();
558    this.fullModifier.invalidate();
559    this.calcBlur();
560  }
561
562  setLayoutState(reverse: boolean, position: number) {
563    const normalStartAngleRight = -START_ANGLE_DEFAULT * ANGLE_TO_RADIAN;
564    const normalEndAngleRight = -END_ANGLE_DEFAULT * ANGLE_TO_RADIAN;
565    const activeStartAngleRight = -ACTIVE_START_ANGLE_DEFAULT * ANGLE_TO_RADIAN;
566    const activeEndAngleRight = -ACTIVE_END_ANGLE_DEFAULT * ANGLE_TO_RADIAN;
567    const normalStartAngleLeft = -Math.PI - normalStartAngleRight;
568    const normalEndAngleLeft = -Math.PI - normalEndAngleRight;
569    const activeStartAngleLeft = -Math.PI - activeStartAngleRight;
570    const activeEndAngleLeft = -Math.PI - activeEndAngleRight;
571    if (reverse && position === ArcSliderPosition.RIGHT) {
572      this.isAntiClock = true;
573      this.normalStartAngle = normalStartAngleRight;
574      this.normalEndAngle = normalEndAngleRight;
575      this.activeStartAngle = activeStartAngleRight;
576      this.activeEndAngle = activeEndAngleRight;
577    } else if (!reverse && position === ArcSliderPosition.RIGHT) {
578      this.isAntiClock = false;
579      this.normalStartAngle = normalEndAngleRight;
580      this.normalEndAngle = normalStartAngleRight;
581      this.activeStartAngle = activeEndAngleRight;
582      this.activeEndAngle = activeStartAngleRight;
583    } else if (reverse && position === ArcSliderPosition.LEFT) {
584      this.isAntiClock = false;
585      this.normalStartAngle = normalStartAngleLeft;
586      this.normalEndAngle = normalEndAngleLeft;
587      this.activeStartAngle = activeStartAngleLeft;
588      this.activeEndAngle = activeEndAngleLeft;
589    } else if (!reverse && position === ArcSliderPosition.LEFT) {
590      this.isAntiClock = true;
591      this.normalStartAngle = normalEndAngleLeft;
592      this.normalEndAngle = normalStartAngleLeft;
593      this.activeStartAngle = activeEndAngleLeft;
594      this.activeEndAngle = activeStartAngleLeft;
595    }
596  }
597
598  setLayoutOptions() {
599    this.radius = this.normalRadius - (this.lineWidth / 2);
600    // Without setting the angle and width in the enlarged state, the animation will be affected
601    if (!this.isEnlarged) {
602      this.selectedStartAngle = this.normalStartAngle;
603      this.trackEndAngle = this.normalEndAngle;
604      this.trackStartAngle = this.normalStartAngle;
605      this.radius = this.radius - PADDING_DEFAULT;
606      this.lineWidth = this.options.styleOptions.trackThickness;
607    }
608    const selectedRatio = (this.options.valueOptions.progress - this.options.valueOptions.min) /
609      (this.options.valueOptions.max - this.options.valueOptions.min);
610    const deltaRadian = this.trackEndAngle - this.selectedStartAngle;
611    const selectedAngle = selectedRatio * Math.abs(deltaRadian);
612    if (this.trackEndAngle > this.selectedStartAngle) {
613      this.selectedEndAngle = this.selectedStartAngle + selectedAngle;
614    } else {
615      this.selectedEndAngle = this.selectedStartAngle - selectedAngle;
616    }
617    this.calcBlur();
618  }
619
620  calcPathXY(isRLarge: ClipPathArc) {
621    if (this.parameters.uiContext) {
622      const halfLineWidth = this.parameters.lineWidth / 2;
623      let distance = this.parameters.radius;
624      let angle = 0;
625      if (isRLarge === ClipPathArc.ARC1) {
626        distance += halfLineWidth;
627        angle = this.parameters.trackStartAngle;
628      } else if (isRLarge === ClipPathArc.ARC2) {
629        distance += halfLineWidth;
630        angle = this.parameters.trackEndAngle;
631      } else if (isRLarge === ClipPathArc.ARC3) {
632        distance -= halfLineWidth;
633        angle = this.parameters.trackEndAngle;
634      } else if (isRLarge === ClipPathArc.ARC4) {
635        distance -= halfLineWidth;
636        angle = this.parameters.trackStartAngle;
637      }
638      return `${(this.parameters.uiContext.vp2px(this.parameters.x + distance * Math.cos(angle)))} ` +
639        `${(this.parameters.uiContext.vp2px(this.parameters.y + (distance) * Math.sin(angle)))}`;
640    }
641    return 0;
642  }
643
644  calcPathR(isRLarge: ClipPathArc) {
645    if (this.parameters.uiContext) {
646      const halfLineWidth = this.parameters.lineWidth / 2;
647      let pathR = this.parameters.uiContext.vp2px(halfLineWidth);
648      if (isRLarge === ClipPathArc.ARC2) {
649        pathR += this.parameters.uiContext.vp2px(this.parameters.radius);
650      } else if (isRLarge === ClipPathArc.ARC4) {
651        pathR = this.parameters.uiContext.vp2px(this.parameters.radius) - pathR;
652      }
653      return `${pathR} ${pathR}`;
654    }
655    return 0;
656  }
657
658  setClipPath() {
659    let littleArc = this.calcPathR(ClipPathArc.ARC1);
660    const sourcePoint = `M${this.calcPathXY(ClipPathArc.ARC4)}`;
661    const arc1 = ` A${littleArc} 0 1 ` + `${Number(this.isReverse)} ${this.calcPathXY(ClipPathArc.ARC1)}`;
662    const arc2 = ` A${this.calcPathR(ClipPathArc.ARC2)} 0 ${Number(this.isLargeArc)} ${Number(this.isReverse)} ` +
663      `${this.calcPathXY(ClipPathArc.ARC2)}`;
664    const arc3 = ` A${littleArc} 0 1 ` + `${Number(this.isReverse)} ${this.calcPathXY(ClipPathArc.ARC3)}`;
665    const arc4 = ` A${this.calcPathR(ClipPathArc.ARC4)} 0 ${Number(this.isLargeArc)} ${Number(!this.isReverse)} ` +
666      `${this.calcPathXY(ClipPathArc.ARC4)}`;
667    this.clipPath = sourcePoint + arc1 + arc2 + arc3 + arc4;
668  }
669
670  calcBlur() {
671    this.isLargeArc = false;
672    if (this.isAntiClock) {
673      this.isReverse = false;
674    } else {
675      this.isReverse = true;
676    }
677    this.setClipPath();
678  }
679
680  calcAnimatorChange(start: number, end: number, fraction: number) {
681    return (fraction * (end - start) + start);
682  }
683
684  calcClickValue(clickX: number, clickY: number) {
685    if (clickY - this.normalRadius > this.radius) {
686      clickY = this.radius + this.normalRadius;
687    } else if (this.normalRadius - clickY > this.radius) {
688      clickY = this.normalRadius - this.radius;
689    }
690    const sin = Math.abs(clickY - this.normalRadius) / this.radius;
691    let radian = Math.asin(sin);
692    const isXPositive: boolean = clickX > this.normalRadius;
693    const isYPositive: boolean = clickY > this.normalRadius;
694    if (!isXPositive && isYPositive) {
695      radian = -Math.PI - radian;
696    } else if (!isXPositive && !isYPositive) {
697      radian = radian - Math.PI;
698    } else if (isXPositive && !isYPositive) {
699      radian = -radian;
700    }
701    this.selectedEndAngle = radian;
702    const delta = (this.selectedStartAngle - this.selectedEndAngle) / ANGLE_TO_RADIAN;
703    if (!this.isAntiClock) {
704      this.selectRatioNow = -delta / (ACTIVE_END_ANGLE_DEFAULT - ACTIVE_START_ANGLE_DEFAULT);
705    } else {
706      this.selectRatioNow = delta / (ACTIVE_END_ANGLE_DEFAULT - ACTIVE_START_ANGLE_DEFAULT);
707    }
708    this.selectRatioNow = Math.min(1, this.selectRatioNow);
709    this.selectRatioNow = Math.max(0, this.selectRatioNow);
710    this.clickValue = this.selectRatioNow * (this.options.valueOptions.max - this.options.valueOptions.min) +
711    this.options.valueOptions.min;
712    this.options.valueOptions.progress = this.clickValue;
713    this.setLayoutOptions();
714    this.updateModifier();
715    this.fullModifier.invalidate();
716  }
717
718  calcValue(moveY: number) {
719    this.delta = this.touchY - moveY;
720    const total = this.radius * Math.sqrt(3);
721    let valueNow = (this.options.valueOptions.progress - this.options.valueOptions.min) /
722      (this.options.valueOptions.max - this.options.valueOptions.min);
723    if (this.options.layoutOptions.reverse) {
724      valueNow += this.delta / total;
725    } else {
726      valueNow -= this.delta / total;
727    }
728    valueNow = Math.min(1, valueNow);
729    valueNow = Math.max(0, valueNow);
730    this.options.valueOptions.progress = valueNow * (this.options.valueOptions.max - this.options.valueOptions.min) +
731    this.options.valueOptions.min;
732    this.setLayoutOptions();
733    this.updateModifier();
734    this.fullModifier.invalidate();
735    this.touchY = moveY;
736  }
737
738  calcCrownTotal(activeStartAngle: number, activeEndAngle: number): number {
739    if (activeEndAngle > activeStartAngle) {
740      if (this.options.layoutOptions.reverse) {
741        return (TWO_PI_RADIAN - Math.abs(activeEndAngle - activeStartAngle));
742      }
743      return Math.abs(activeEndAngle - activeStartAngle);
744    } else {
745      if (this.options.layoutOptions.reverse) {
746        return Math.abs(activeEndAngle - activeStartAngle);
747      }
748      return (TWO_PI_RADIAN - Math.abs(activeEndAngle - activeStartAngle));
749    }
750  }
751
752  calcCrownValue(deltaCrownAngle: number) {
753    const totalAngle = this.calcCrownTotal(this.activeStartAngle, this.activeEndAngle);
754    const totalValue = this.options.valueOptions.max - this.options.valueOptions.min;
755    let valueNow = (this.options.valueOptions.progress - this.options.valueOptions.min) / totalValue;
756    valueNow += deltaCrownAngle / totalAngle;
757    valueNow = Math.min(1, valueNow);
758    valueNow = Math.max(0, valueNow);
759    this.options.valueOptions.progress = valueNow * totalValue + this.options.valueOptions.min;
760    this.setLayoutOptions();
761    this.updateModifier();
762    this.fullModifier.invalidate();
763  }
764
765  calcMaxValueDeltaIsPositive(delta: number) {
766    const isLineWidthFitted: boolean = this.lineWidth >= this.options.styleOptions.activeTrackThickness *
767      (1 - LENGTH_OVER_MIN);
768    if (this.isAntiClock) {
769      const isEndAngleFitted: boolean = this.selectedEndAngle >= (this.activeEndAngle - ANGLE_OVER_MIN);
770      if (isEndAngleFitted && isLineWidthFitted) {
771        this.selectedEndAngle -= (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
772        this.lineWidth -= LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) / (LENGTH_OVER_MIN * this.lineWidth +
773        Math.abs(this.delta));
774        this.trackEndAngle = this.selectedEndAngle;
775      }
776      if (this.selectedEndAngle <= (this.activeEndAngle - ANGLE_OVER_MIN)) {
777        this.selectedEndAngle = this.activeEndAngle - ANGLE_OVER_MIN;
778      }
779    } else {
780      const isEndAngleFitted: boolean = this.selectedEndAngle <= (this.activeEndAngle + ANGLE_OVER_MIN);
781      if (isEndAngleFitted && isLineWidthFitted) {
782        this.selectedEndAngle += (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
783        this.lineWidth -= LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) / (LENGTH_OVER_MIN * this.lineWidth +
784        Math.abs(this.delta));
785        this.trackEndAngle = this.selectedEndAngle;
786      }
787      if (this.selectedEndAngle >= (this.activeEndAngle + ANGLE_OVER_MIN)) {
788        this.selectedEndAngle = this.activeEndAngle + ANGLE_OVER_MIN;
789      }
790    }
791    this.trackEndAngle = this.selectedEndAngle;
792    if (this.lineWidth <= this.options.styleOptions.activeTrackThickness * (1 - LENGTH_OVER_MIN)) {
793      this.lineWidth = this.options.styleOptions.activeTrackThickness * (1 - LENGTH_OVER_MIN);
794    }
795  }
796
797  calcMaxValueDeltaIsNegative(delta: number) {
798    const isLineWidthFitted: boolean = this.lineWidth <= this.options.styleOptions.activeTrackThickness;
799    const isEndAngleFitted: boolean = this.selectedEndAngle <= this.activeEndAngle;
800    if (this.isAntiClock) {
801      if (isEndAngleFitted || isLineWidthFitted) {
802        this.selectedEndAngle -= (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
803      }
804      if (this.selectedEndAngle >= this.activeEndAngle) {
805        this.selectedEndAngle = this.activeEndAngle;
806        this.trackEndAngle = this.selectedEndAngle;
807      }
808    } else {
809      if ((!isEndAngleFitted) || isLineWidthFitted) {
810        this.selectedEndAngle += (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
811      }
812      if (this.selectedEndAngle <= this.activeEndAngle) {
813        this.selectedEndAngle = this.activeEndAngle;
814        this.trackEndAngle = this.selectedEndAngle;
815      }
816    }
817    this.lineWidth += LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) / (LENGTH_OVER_MIN * this.lineWidth +
818    Math.abs(this.delta));
819    this.trackEndAngle = this.selectedEndAngle;
820    if (this.lineWidth >= this.options.styleOptions.activeTrackThickness) {
821      this.lineWidth = this.options.styleOptions.activeTrackThickness;
822    }
823  }
824
825  calcMaxValue(moveY: number) {
826    this.delta = this.touchY - moveY;
827    let delta: number = this.delta;
828    if (!this.options.layoutOptions.reverse) {
829      delta = -this.delta;
830    }
831    if (delta >= 0) {
832      this.calcMaxValueDeltaIsPositive(delta);
833    } else {
834      this.calcMaxValueDeltaIsNegative(delta);
835    }
836    this.updateModifier();
837    this.fullModifier.invalidate();
838    this.touchY = moveY;
839    this.calcBlur();
840  }
841
842  calcMinValueDeltaIsNegative(delta: number) {
843    const isLineWidthFitted: boolean = this.lineWidth >= this.options.styleOptions.activeTrackThickness *
844      (1 - LENGTH_OVER_MIN);
845    if (this.isAntiClock) {
846      const isStartAngleFitted: boolean = this.trackStartAngle <= this.selectedStartAngle + ANGLE_OVER_MIN;
847      if (isStartAngleFitted && isLineWidthFitted) {
848        this.trackStartAngle -= (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius +
849        Math.abs(this.delta));
850        this.lineWidth -= LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) /
851          (LENGTH_OVER_MIN * this.lineWidth + Math.abs(this.delta));
852      }
853      if (this.trackStartAngle >= this.selectedStartAngle + ANGLE_OVER_MIN) {
854        this.trackStartAngle = this.selectedStartAngle + ANGLE_OVER_MIN;
855      }
856    } else {
857      const isStartAngleFitted: boolean = this.trackStartAngle >= this.selectedStartAngle - ANGLE_OVER_MIN;
858      if (isStartAngleFitted && isLineWidthFitted) {
859        this.trackStartAngle += (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius +
860        Math.abs(this.delta));
861        this.lineWidth -= LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) /
862          (LENGTH_OVER_MIN * this.lineWidth + Math.abs(this.delta));
863      }
864      if (this.trackStartAngle <= this.selectedStartAngle - ANGLE_OVER_MIN) {
865        this.trackStartAngle = this.selectedStartAngle - ANGLE_OVER_MIN;
866      }
867    }
868    if (this.lineWidth <= this.options.styleOptions.activeTrackThickness * (1 - LENGTH_OVER_MIN)) {
869      this.lineWidth = this.options.styleOptions.activeTrackThickness * (1 - LENGTH_OVER_MIN);
870    }
871  }
872
873  calcMinValueDeltaIsPositive(delta: number) {
874    const isLineWidthFitted: boolean = this.lineWidth <= this.options.styleOptions.activeTrackThickness;
875    const isStartAngleFitted: boolean = this.trackStartAngle > this.selectedStartAngle;
876    if (this.isAntiClock) {
877      if (isStartAngleFitted || isLineWidthFitted) {
878        this.trackStartAngle -= (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
879        this.lineWidth += LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) / (LENGTH_OVER_MIN * this.lineWidth +
880        Math.abs(this.delta));
881      }
882      if (this.trackStartAngle < this.selectedStartAngle) {
883        this.trackStartAngle = this.selectedStartAngle;
884      }
885    } else {
886      if (!isStartAngleFitted || isLineWidthFitted) {
887        this.trackStartAngle += (ANGLE_OVER_MIN) * delta / (ANGLE_OVER_MIN * this.radius + Math.abs(this.delta));
888        this.lineWidth += LENGTH_OVER_MIN * this.lineWidth * Math.abs(this.delta) / (LENGTH_OVER_MIN * this.lineWidth +
889        Math.abs(this.delta));
890      }
891      if (this.trackStartAngle > this.selectedStartAngle) {
892        this.trackStartAngle = this.selectedStartAngle;
893      }
894    }
895    if (this.lineWidth >= this.options.styleOptions.activeTrackThickness) {
896      this.lineWidth = this.options.styleOptions.activeTrackThickness;
897    }
898  }
899
900  calcMinValue(moveY: number) {
901    this.delta = this.touchY - moveY;
902    let delta: number = this.delta;
903    if (!this.options.layoutOptions.reverse) {
904      delta = -this.delta;
905    }
906    if (delta <= 0) {
907      this.calcMinValueDeltaIsNegative(delta);
908    } else {
909      this.calcMinValueDeltaIsPositive(delta);
910    }
911    this.updateModifier();
912    this.fullModifier.invalidate();
913    this.touchY = moveY;
914    this.calcBlur();
915  }
916
917  isHotRegion(touchX: number, touchY: number): boolean {
918    const radius = Math.sqrt(Math.pow(touchX - this.normalRadius, 2) + Math.pow(touchY - this.normalRadius, 2));
919    let isRadiusNoFitted: boolean = (radius < this.normalRadius - this.options.styleOptions.activeTrackThickness) ||
920      (radius > this.normalRadius);
921    if (isRadiusNoFitted) {
922      return false;
923    }
924    const sin = Math.abs(touchY - this.normalRadius) / radius;
925    const radian = Math.asin(sin);
926    let angle = radian / ANGLE_TO_RADIAN;
927    const isXPositive: boolean = touchX > this.normalRadius;
928    const isYPositive: boolean = touchY > this.normalRadius;
929    if (!isXPositive && isYPositive) {
930      angle = -PI_ANGLE - angle;
931    } else if (!isXPositive && !isYPositive) {
932      angle = angle - PI_ANGLE;
933    } else if (isXPositive && !isYPositive) {
934      angle = -angle;
935    }
936    let angleToRadian = angle * ANGLE_TO_RADIAN;
937    const isAntiClockAngleFitted: boolean = angleToRadian <= this.selectedStartAngle &&
938      angleToRadian >= this.trackEndAngle;
939    const isClockAngleFitted: boolean = angleToRadian >= this.selectedStartAngle && angleToRadian <= this.trackEndAngle;
940    if (this.isAntiClock && isAntiClockAngleFitted || !this.isAntiClock && isClockAngleFitted) {
941      return true;
942    }
943    return false;
944  }
945
946  calcDisplayControlRatio(crownSensitivity: CrownSensitivity): number {
947    if (crownSensitivity === CrownSensitivity.LOW) {
948      return CROWN_CONTROL_RATIO * CROWN_SENSITIVITY_LOW;
949    } else if (crownSensitivity === CrownSensitivity.MEDIUM) {
950      return CROWN_CONTROL_RATIO * CROWN_SENSITIVITY_MEDIUM;
951    } else if (crownSensitivity === CrownSensitivity.HIGH) {
952      return CROWN_CONTROL_RATIO * CROWN_SENSITIVITY_HIGH;
953    }
954    return CROWN_CONTROL_RATIO * CROWN_SENSITIVITY_MEDIUM;
955  }
956
957  clearTimeout() {
958    if (this.meter !== INVALID_TIMEOUT_ID) {
959      clearTimeout(this.meter);
960      this.meter = INVALID_TIMEOUT_ID
961    }
962  }
963
964  onTouchEvent(event: TouchEvent) {
965    const isTouchTypeUp: boolean = this.isEnlarged && event.type === TouchType.Up;
966    const isTouchTypeMove: boolean = this.isEnlarged && this.isTouchAnimatorFinished && event.type === TouchType.Move;
967    if (event.type === TouchType.Down) {
968      this.isFocus = false;
969      if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
970        this.isFocus = true;
971      }
972      this.onTouchDown(event);
973    } else if (isTouchTypeUp) {
974      this.clearTimeout();
975      if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
976        this.options.onTouch?.(event);
977      }
978      this.meter = setTimeout(() => {
979        if (this.isEnlarged) {
980          this.isTouchAnimatorFinished = false;
981          this.isEnlarged = false;
982          this.startRestoreAnimator();
983          this.calcBlur();
984        }
985      }, RESTORE_TIMEOUT)
986      this.isMaxOrMinAnimator();
987    } else if (isTouchTypeMove && this.isFocus) {
988      this.options.onTouch?.(event);
989      this.onTouchMove(event.touches[0].y);
990      this.options.onChange?.(this.options.valueOptions.progress);
991      this.clearTimeout();
992    }
993  }
994
995  onTouchDown(event: TouchEvent) {
996    if (!this.isEnlarged) {
997      this.touchY = event.touches[0].y;
998      this.clearTimeout();
999      if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
1000        this.options.onTouch?.(event);
1001        this.isEnlarged = true;
1002        this.startTouchAnimator();
1003        this.calcBlur();
1004      }
1005    } else {
1006      this.touchY = event.touches[0].y;
1007      if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
1008        this.options.onTouch?.(event);
1009        this.clearTimeout();
1010        if (this.isTouchAnimatorFinished) {
1011          this.calcClickValue(event.touches[0].x, event.touches[0].y);
1012        }
1013        this.touchY = event.touches[0].y;
1014        this.calcValue(event.touches[0].y);
1015        this.options.onChange?.(this.options.valueOptions.progress);
1016        this.setLayoutOptions();
1017        this.updateModifier();
1018        this.fullModifier.invalidate();
1019      }
1020    }
1021  }
1022
1023  isMaxOrMinAnimator() {
1024    let selectedEndAngle = this.selectedEndAngle;
1025    let trackStartAngle = this.trackStartAngle;
1026    let activeEndAngle = this.activeEndAngle;
1027    let activeStartAngle = this.activeStartAngle;
1028    if (!this.isAntiClock) {
1029      selectedEndAngle = -this.selectedEndAngle;
1030      trackStartAngle = -this.trackStartAngle;
1031      activeEndAngle = -this.activeEndAngle;
1032      activeStartAngle = -this.activeStartAngle;
1033    }
1034    const canMaxRestoreAnimatorStart: boolean = this.selectedMaxOrMin === AnimatorStatus.MAX &&
1035      selectedEndAngle < activeEndAngle;
1036    const canMinRestoreAnimatorStart: boolean = this.selectedMaxOrMin === AnimatorStatus.MIN &&
1037      trackStartAngle > activeStartAngle;
1038    if (canMaxRestoreAnimatorStart) {
1039      this.lineWidthBegin = this.lineWidth;
1040      this.selectedEndAngleBegin = this.selectedEndAngle;
1041      this.startMaxRestoreAnimator();
1042    }
1043    if (canMinRestoreAnimatorStart) {
1044      this.lineWidthBegin = this.lineWidth;
1045      this.trackStartAngleBegin = this.trackStartAngle;
1046      this.startMinRestoreAnimator();
1047      this.calcBlur();
1048    }
1049  }
1050
1051  onTouchMove(touchY: number) {
1052    let maxAngel: number;
1053    let minAngel: number;
1054    let delta: number = this.delta;
1055    maxAngel = this.selectedEndAngle;
1056    minAngel = this.trackStartAngle;
1057    if (!this.options.layoutOptions.reverse) {
1058      delta = -this.delta;
1059    }
1060    const isMaxFitted: boolean = !(delta < 0 && nearEqual(maxAngel, this.activeEndAngle));
1061    const isMinFitted: boolean = !(delta > 0 && nearEqual(this.trackStartAngle, this.activeStartAngle)) &&
1062    nearEqual(this.options.valueOptions.progress, this.options.valueOptions.min);
1063    const isMaxNearEqual: boolean = nearEqual(maxAngel, this.activeEndAngle);
1064    const isMinNearEqual: boolean = nearEqual(minAngel, this.activeStartAngle);
1065    if (this.isAntiClock) {
1066      const isCalcMax: boolean = (maxAngel < this.activeEndAngle || isMaxNearEqual) && isMaxFitted;
1067      const isCalcMin: boolean = (minAngel >= this.activeStartAngle || isMinNearEqual) && isMinFitted;
1068      if (isCalcMax) {
1069        this.selectedMaxOrMin = AnimatorStatus.MAX;
1070        this.calcMaxValue(touchY);
1071      } else if (isCalcMin) {
1072        this.selectedMaxOrMin = AnimatorStatus.MIN;
1073        this.calcMinValue(touchY);
1074      } else {
1075        this.calcValue(touchY);
1076        this.selectedMaxOrMin = AnimatorStatus.NORMAL;
1077      }
1078    } else {
1079      const isCalcMax: boolean = (maxAngel > this.activeEndAngle || isMaxNearEqual) && isMaxFitted;
1080      const isCalcMin: boolean = (minAngel <= this.activeStartAngle || isMinNearEqual) && isMinFitted;
1081      if (isCalcMax) {
1082        this.selectedMaxOrMin = AnimatorStatus.MAX;
1083        this.calcMaxValue(touchY);
1084      } else if (isCalcMin) {
1085        this.selectedMaxOrMin = AnimatorStatus.MIN;
1086        this.calcMinValue(touchY);
1087      } else {
1088        this.calcValue(touchY);
1089        this.selectedMaxOrMin = AnimatorStatus.NORMAL;
1090      }
1091    }
1092  }
1093
1094  onDigitalCrownEvent(event: CrownEvent) {
1095    this.timeCur = systemDateTime.getTime(false);
1096    const isVibEnabled: boolean = this.isEnlarged && (event.action === CrownAction.BEGIN ||
1097      this.isTouchAnimatorFinished && event.action === CrownAction.UPDATE);
1098    if (event.action === CrownAction.BEGIN && !this.isEnlarged) {
1099      this.clearTimeout();
1100      this.isEnlarged = true;
1101      this.startTouchAnimator();
1102      this.calcBlur();
1103    } else if (isVibEnabled) {
1104      this.clearTimeout();
1105      this.crownDeltaAngle = this.getUIContext().px2vp(-event.degree *
1106      this.calcDisplayControlRatio(this.options.digitalCrownSensitivity)) / this.radius;
1107      this.calcCrownValue(this.crownDeltaAngle);
1108      this.setVibration();
1109    } else if (this.isEnlarged && event.action === CrownAction.END) {
1110      this.clearTimeout();
1111      this.meter = setTimeout(() => {
1112        if (this.isEnlarged) {
1113          this.isTouchAnimatorFinished = false;
1114          this.isEnlarged = false;
1115          this.startRestoreAnimator();
1116          this.calcBlur();
1117        }
1118      }, RESTORE_TIMEOUT)
1119    }
1120  }
1121
1122  setVibration() {
1123    const isMaxOrMin: boolean = this.options.valueOptions.progress === this.options.valueOptions.max ||
1124      this.options.valueOptions.progress === this.options.valueOptions.min;
1125    if (this.timeCur - this.timePre >= CROWN_TIME_FLAG && !isMaxOrMin) {
1126      try {
1127        this.startVibration();
1128      } catch (error) {
1129        const e: BusinessError = error as BusinessError;
1130        hilog.error(0x3900, 'ArcSlider', `An unexpected error occurred in starting vibration.
1131                      Code: ${e.code}, message: ${e.message}`);
1132      }
1133      this.timePre = this.timeCur;
1134    }
1135  }
1136
1137  startVibration() {
1138    const ret = vibrator.isSupportEffectSync(VIBRATOR_TYPE_TWO);
1139    if (ret) {
1140      vibrator.startVibration({
1141        type: 'preset',
1142        effectId: VIBRATOR_TYPE_TWO,
1143        count: 1,
1144      }, {
1145        usage: 'unknown'
1146      }, (error: BusinessError) => {
1147        if (error) {
1148          hilog.error(0x3900, 'ArcSlider', `Failed to start vibration.
1149                            Code: ${error.code}, message: ${error.message}`);
1150          this.timePre = this.timeCur;
1151          return;
1152        }
1153        hilog.info(0x3900, 'ArcSlider', 'Succeed in starting vibration');
1154      });
1155    } else {
1156      hilog.error(0x3900, 'ArcSlider', VIBRATOR_TYPE_TWO + ` is not supported`);
1157    }
1158  }
1159
1160  build() {
1161    Stack() {
1162      Circle({ width: this.diameter, height: this.diameter })
1163        .width(this.diameter)
1164        .height(this.diameter)
1165        .fill(BLUR_COLOR_DEFAULT)
1166        .backdropBlur(this.options.styleOptions.trackBlur, undefined, { disableSystemAdaptation: true })
1167
1168      Button()
1169        .stateEffect(false)
1170        .backgroundColor(BLUR_COLOR_DEFAULT)
1171        .drawModifier(this.fullModifier)
1172        .width(this.diameter)
1173        .height(this.diameter)
1174        .onTouch((event?: TouchEvent) => {
1175          if (event) {
1176            this.onTouchEvent(event);
1177          }
1178        })
1179        .onTouchIntercept((event: TouchEvent) => {
1180          if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
1181            return HitTestMode.Block;
1182          }
1183          return HitTestMode.Transparent;
1184        })
1185        .focusable(true)
1186        .focusOnTouch(true)
1187        .onDigitalCrown((event: CrownEvent) => {
1188          if (event) {
1189            this.onDigitalCrownEvent(event);
1190            event.stopPropagation();
1191          }
1192          this.options.onChange?.(this.options.valueOptions.progress);
1193        })
1194    }
1195    .clipShape(new PathShape().commands(this.clipPath))
1196    .onTouchIntercept((event: TouchEvent) => {
1197      if (this.isHotRegion(event.touches[0].x, event.touches[0].y)) {
1198        return HitTestMode.Default;
1199      }
1200      return HitTestMode.Transparent;
1201    })
1202  }
1203}