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}