• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.shared.clocks.view
18 
19 import android.annotation.SuppressLint
20 import android.graphics.Canvas
21 import android.graphics.Color
22 import android.graphics.Paint
23 import android.graphics.PorterDuff
24 import android.graphics.PorterDuffXfermode
25 import android.graphics.Rect
26 import android.os.VibrationEffect
27 import android.text.TextPaint
28 import android.util.AttributeSet
29 import android.util.Log
30 import android.util.MathUtils.lerp
31 import android.util.TypedValue
32 import android.view.View
33 import android.view.View.MeasureSpec.EXACTLY
34 import android.view.animation.Interpolator
35 import android.view.animation.PathInterpolator
36 import android.widget.TextView
37 import com.android.app.animation.Interpolators
38 import com.android.internal.annotations.VisibleForTesting
39 import com.android.systemui.animation.AxisDefinition
40 import com.android.systemui.animation.GSFAxes
41 import com.android.systemui.animation.TextAnimator
42 import com.android.systemui.animation.TextAnimatorListener
43 import com.android.systemui.customization.R
44 import com.android.systemui.plugins.clocks.ClockAxisStyle
45 import com.android.systemui.plugins.clocks.ClockLogger
46 import com.android.systemui.plugins.clocks.VPoint
47 import com.android.systemui.plugins.clocks.VPointF
48 import com.android.systemui.plugins.clocks.VPointF.Companion.size
49 import com.android.systemui.plugins.clocks.VRectF
50 import com.android.systemui.shared.Flags.ambientAod
51 import com.android.systemui.shared.clocks.CanvasUtil.translate
52 import com.android.systemui.shared.clocks.CanvasUtil.use
53 import com.android.systemui.shared.clocks.ClockContext
54 import com.android.systemui.shared.clocks.DigitTranslateAnimator
55 import com.android.systemui.shared.clocks.DimensionParser
56 import com.android.systemui.shared.clocks.FLEX_CLOCK_ID
57 import com.android.systemui.shared.clocks.FontTextStyle
58 import com.android.systemui.shared.clocks.FontUtils.set
59 import com.android.systemui.shared.clocks.ViewUtils.measuredSize
60 import com.android.systemui.shared.clocks.ViewUtils.size
61 import java.lang.Thread
62 import kotlin.math.max
63 import kotlin.math.min
64 import kotlin.math.roundToInt
65 
66 private val TAG = SimpleDigitalClockTextView::class.simpleName!!
67 
68 private val tempRect = Rect()
69 
70 private fun Paint.getTextBounds(text: CharSequence): VRectF {
71     this.getTextBounds(text, 0, text.length, tempRect)
72     return VRectF(tempRect)
73 }
74 
75 enum class VerticalAlignment {
76     TOP,
77     BOTTOM,
78     BASELINE,
79     CENTER,
80 }
81 
82 enum class HorizontalAlignment {
83     LEFT {
resolveXAlignmentnull84         override fun resolveXAlignment(view: View) = XAlignment.LEFT
85     },
86     RIGHT {
87         override fun resolveXAlignment(view: View) = XAlignment.RIGHT
88     },
89     START {
resolveXAlignmentnull90         override fun resolveXAlignment(view: View): XAlignment {
91             return if (view.isLayoutRtl()) XAlignment.RIGHT else XAlignment.LEFT
92         }
93     },
94     END {
resolveXAlignmentnull95         override fun resolveXAlignment(view: View): XAlignment {
96             return if (view.isLayoutRtl()) XAlignment.LEFT else XAlignment.RIGHT
97         }
98     },
99     CENTER {
resolveXAlignmentnull100         override fun resolveXAlignment(view: View) = XAlignment.CENTER
101     };
102 
resolveXAlignmentnull103     abstract fun resolveXAlignment(view: View): XAlignment
104 }
105 
106 enum class XAlignment {
107     LEFT,
108     RIGHT,
109     CENTER,
110 }
111 
112 @SuppressLint("AppCompatCustomView")
113 open class SimpleDigitalClockTextView(
114     val clockCtx: ClockContext,
115     isLargeClock: Boolean,
116     attrs: AttributeSet? = null,
117 ) : TextView(clockCtx.context, attrs) {
118     val lockScreenPaint = TextPaint()
119     lateinit var textStyle: FontTextStyle
120     lateinit var aodStyle: FontTextStyle
121 
122     private val isLegacyFlex = clockCtx.settings.clockId == FLEX_CLOCK_ID
123     private val fixedAodAxes =
124         when {
125             !isLegacyFlex -> fromAxes(AOD_WEIGHT_AXIS, WIDTH_AXIS)
126             isLargeClock -> fromAxes(FLEX_AOD_LARGE_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
127             else -> fromAxes(FLEX_AOD_SMALL_WEIGHT_AXIS, FLEX_AOD_WIDTH_AXIS)
128         }
129 
130     private var lsFontVariation: String
131     private var aodFontVariation: String
132     private var fidgetFontVariation: String
133 
134     init {
135         val roundAxis = if (!isLegacyFlex) ROUND_AXIS else FLEX_ROUND_AXIS
136         val lsFontAxes =
137             if (!isLegacyFlex) fromAxes(LS_WEIGHT_AXIS, WIDTH_AXIS, ROUND_AXIS, SLANT_AXIS)
138             else fromAxes(FLEX_LS_WEIGHT_AXIS, FLEX_LS_WIDTH_AXIS, FLEX_ROUND_AXIS, SLANT_AXIS)
139 
140         lsFontVariation = lsFontAxes.toFVar()
141         aodFontVariation = fixedAodAxes.copyWith(fromAxes(roundAxis, SLANT_AXIS)).toFVar()
142         fidgetFontVariation = buildFidgetVariation(lsFontAxes).toFVar()
143     }
144 
145     var onViewBoundsChanged: ((VRectF) -> Unit)? = null
146     private val parser = DimensionParser(clockCtx.context)
147     var maxSingleDigitHeight = -1f
148     var maxSingleDigitWidth = -1f
149     var digitTranslateAnimator: DigitTranslateAnimator? = null
150     var aodFontSizePx = -1f
151 
152     // Store the font size when there's no height constraint as a reference when adjusting font size
153     private var lastUnconstrainedTextSize = Float.MAX_VALUE
154     // Calculated by height of styled text view / text size
155     // Used as a factor to calculate a smaller font size when text height is constrained
156     @VisibleForTesting var fontSizeAdjustFactor = 1f
157 
158     private val initThread = Thread.currentThread()
159 
160     // textBounds is the size of text in LS, which only measures current text in lockscreen style
161     var textBounds = VRectF.ZERO
162     // prevTextBounds and targetTextBounds are to deal with dozing animation between LS and AOD
163     // especially for the textView which has different bounds during the animation
164     // prevTextBounds holds the state we are transitioning from
165     private var prevTextBounds = VRectF.ZERO
166     // targetTextBounds holds the state we are interpolating to
167     private var targetTextBounds = VRectF.ZERO
168     protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
169         get() = field ?: ClockLogger.INIT_LOGGER
170 
171     private var aodDozingInterpolator: Interpolator = Interpolators.LINEAR
172 
173     @VisibleForTesting lateinit var textAnimator: TextAnimator
174 
175     private val typefaceCache = clockCtx.typefaceCache.getVariantCache("")
176 
177     var verticalAlignment: VerticalAlignment = VerticalAlignment.BASELINE
178     var horizontalAlignment: HorizontalAlignment = HorizontalAlignment.CENTER
179 
180     val xAlignment: XAlignment
181         get() = horizontalAlignment.resolveXAlignment(this)
182 
183     var isAnimationEnabled = true
184     var dozeFraction: Float = 0f
185         set(value) {
186             field = value
187             invalidate()
188         }
189 
190     var textBorderWidth = 0f
191     var measuredBaseline = 0
192     var lockscreenColor = Color.WHITE
193 
updateColornull194     fun updateColor(color: Int) {
195         lockscreenColor = color
196         lockScreenPaint.color = lockscreenColor
197         if (dozeFraction < 1f) {
198             textAnimator.setTextStyle(TextAnimator.Style(color = lockscreenColor))
199         }
200         invalidate()
201     }
202 
updateAxesnull203     fun updateAxes(lsAxes: ClockAxisStyle, isAnimated: Boolean) {
204         lsFontVariation = lsAxes.toFVar()
205         aodFontVariation = lsAxes.copyWith(fixedAodAxes).toFVar()
206         fidgetFontVariation = buildFidgetVariation(lsAxes).toFVar()
207         logger.updateAxes(lsFontVariation, aodFontVariation, isAnimated)
208 
209         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
210         typeface = lockScreenPaint.typeface
211 
212         updateTextBounds()
213 
214         textAnimator.setTextStyle(
215             TextAnimator.Style(fVar = lsFontVariation),
216             TextAnimator.Animation(
217                 animate = isAnimated && isAnimationEnabled,
218                 duration = AXIS_CHANGE_ANIMATION_DURATION,
219                 interpolator = aodDozingInterpolator,
220             ),
221         )
222 
223         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
224         recomputeMaxSingleDigitSizes()
225         requestLayout()
226         invalidate()
227     }
228 
buildFidgetVariationnull229     fun buildFidgetVariation(axes: ClockAxisStyle): ClockAxisStyle {
230         return ClockAxisStyle(
231             axes.items
232                 .map { (key, value) ->
233                     FIDGET_DISTS.get(key)?.let { (dist, midpoint) ->
234                         key to value + dist * if (value > midpoint) -1 else 1
235                     } ?: (key to value)
236                 }
237                 .toMap()
238         )
239     }
240 
onMeasurenull241     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
242         logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
243         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
244 
245         val layout = this.layout
246         if (layout != null) {
247             if (!this::textAnimator.isInitialized) {
248                 textAnimator =
249                     TextAnimator(
250                         layout,
251                         typefaceCache,
252                         object : TextAnimatorListener {
253                             override fun onInvalidate() = invalidate()
254 
255                             override fun onRebased() = updateAnimationTextBounds()
256 
257                             override fun onPaintModified() = updateAnimationTextBounds()
258                         },
259                     )
260                 setInterpolatorPaint()
261             } else {
262                 textAnimator.updateLayout(layout)
263             }
264             measuredBaseline = layout.getLineBaseline(0)
265         } else {
266             val currentThread = Thread.currentThread()
267             Log.wtf(
268                 TAG,
269                 "TextView.getLayout() is null after measure! " +
270                     "currentThread=$currentThread; initThread=$initThread",
271             )
272         }
273 
274         val bounds = getInterpolatedTextBounds()
275         val size = computeMeasuredSize(bounds, widthMeasureSpec, heightMeasureSpec)
276         setInterpolatedSize(size, widthMeasureSpec, heightMeasureSpec)
277     }
278 
279     private var drawnProgress: Float? = null
280 
onDrawnull281     override fun onDraw(canvas: Canvas) {
282         logger.onDraw(textAnimator.textInterpolator.shapedText)
283 
284         val interpProgress = textAnimator.progress
285         val interpBounds = getInterpolatedTextBounds(interpProgress)
286         if (interpProgress != drawnProgress) {
287             drawnProgress = interpProgress
288             val measureSize = computeMeasuredSize(interpBounds)
289             setInterpolatedSize(measureSize)
290             (parent as? FlexClockView)?.run {
291                 updateMeasuredSize()
292                 updateLocation()
293             } ?: setInterpolatedLocation(measureSize)
294         }
295 
296         canvas.use {
297             digitTranslateAnimator?.apply { canvas.translate(currentTranslation) }
298             canvas.translate(getDrawTranslation(interpBounds))
299             if (isLayoutRtl()) canvas.translate(interpBounds.width - textBounds.width, 0f)
300             textAnimator.draw(canvas)
301         }
302     }
303 
setVisibilitynull304     override fun setVisibility(visibility: Int) {
305         logger.setVisibility(visibility)
306         super.setVisibility(visibility)
307     }
308 
setAlphanull309     override fun setAlpha(alpha: Float) {
310         logger.setAlpha(alpha)
311         super.setAlpha(alpha)
312     }
313 
314     private var layoutBounds = VRectF.ZERO
315 
onLayoutnull316     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
317         super.onLayout(changed, left, top, right, bottom)
318         logger.onLayout(changed, left, top, right, bottom)
319         layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
320     }
321 
invalidatenull322     override fun invalidate() {
323         logger.invalidate()
324         super.invalidate()
325         (parent as? FlexClockView)?.invalidate()
326     }
327 
refreshTimenull328     fun refreshTime() {
329         logger.refreshTime()
330         refreshText()
331     }
332 
animateDozenull333     fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
334         if (!this::textAnimator.isInitialized) return
335         logger.animateDoze(isDozing, isAnimated)
336         textAnimator.setTextStyle(
337             TextAnimator.Style(
338                 fVar = if (isDozing) aodFontVariation else lsFontVariation,
339                 color = if (isDozing && !ambientAod()) AOD_COLOR else lockscreenColor,
340                 textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize,
341             ),
342             TextAnimator.Animation(
343                 animate = isAnimated && isAnimationEnabled,
344                 duration = aodStyle.transitionDuration,
345                 interpolator = aodDozingInterpolator,
346             ),
347         )
348 
349         if (!isAnimated) {
350             requestLayout()
351             (parent as? FlexClockView)?.requestLayout()
352         }
353     }
354 
animateChargenull355     fun animateCharge() {
356         if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
357             // Skip charge animation if dozing animation is already playing.
358             return
359         }
360         logger.animateCharge()
361 
362         val lsStyle = TextAnimator.Style(fVar = lsFontVariation)
363         val aodStyle = TextAnimator.Style(fVar = aodFontVariation)
364 
365         textAnimator.setTextStyle(
366             if (dozeFraction == 0f) aodStyle else lsStyle,
367             TextAnimator.Animation(
368                 animate = isAnimationEnabled,
369                 duration = CHARGE_ANIMATION_DURATION,
370                 onAnimationEnd = {
371                     textAnimator.setTextStyle(
372                         if (dozeFraction == 0f) lsStyle else aodStyle,
373                         TextAnimator.Animation(
374                             animate = isAnimationEnabled,
375                             duration = CHARGE_ANIMATION_DURATION,
376                         ),
377                     )
378                 },
379             ),
380         )
381     }
382 
animateFidgetnull383     fun animateFidget(x: Float, y: Float) = animateFidget(0L)
384 
385     fun animateFidget(delay: Long) {
386         if (!this::textAnimator.isInitialized || textAnimator.isRunning) {
387             // Skip fidget animation if other animation is already playing.
388             return
389         }
390 
391         logger.animateFidget(x, y)
392         clockCtx.vibrator?.vibrate(FIDGET_HAPTICS)
393 
394         textAnimator.setTextStyle(
395             TextAnimator.Style(fVar = fidgetFontVariation),
396             TextAnimator.Animation(
397                 animate = isAnimationEnabled,
398                 duration = FIDGET_ANIMATION_DURATION,
399                 interpolator = FIDGET_INTERPOLATOR,
400                 startDelay = delay,
401                 onAnimationEnd = {
402                     textAnimator.setTextStyle(
403                         TextAnimator.Style(fVar = lsFontVariation),
404                         TextAnimator.Animation(
405                             animate = isAnimationEnabled,
406                             duration = FIDGET_ANIMATION_DURATION,
407                             interpolator = FIDGET_INTERPOLATOR,
408                         ),
409                     )
410                 },
411             ),
412         )
413     }
414 
refreshTextnull415     fun refreshText() {
416         updateTextBounds()
417 
418         if (layout == null) {
419             requestLayout()
420         } else {
421             textAnimator.updateLayout(layout)
422         }
423     }
424 
isSingleDigitnull425     private fun isSingleDigit(): Boolean {
426         return id == R.id.HOUR_FIRST_DIGIT ||
427             id == R.id.HOUR_SECOND_DIGIT ||
428             id == R.id.MINUTE_FIRST_DIGIT ||
429             id == R.id.MINUTE_SECOND_DIGIT
430     }
431 
432     /** Returns the interpolated text bounding rect based on interpolation progress */
getInterpolatedTextBoundsnull433     private fun getInterpolatedTextBounds(progress: Float = textAnimator.progress): VRectF {
434         if (progress <= 0f) {
435             return prevTextBounds
436         } else if (!textAnimator.isRunning || progress >= 1f) {
437             return targetTextBounds
438         }
439 
440         return VRectF(
441             left = lerp(prevTextBounds.left, targetTextBounds.left, progress),
442             right = lerp(prevTextBounds.right, targetTextBounds.right, progress),
443             top = lerp(prevTextBounds.top, targetTextBounds.top, progress),
444             bottom = lerp(prevTextBounds.bottom, targetTextBounds.bottom, progress),
445         )
446     }
447 
computeMeasuredSizenull448     private fun computeMeasuredSize(
449         interpBounds: VRectF,
450         widthMeasureSpec: Int = measuredWidthAndState,
451         heightMeasureSpec: Int = measuredHeightAndState,
452     ): VPointF {
453         val mode =
454             VPoint(
455                 x = MeasureSpec.getMode(widthMeasureSpec),
456                 y = MeasureSpec.getMode(heightMeasureSpec),
457             )
458 
459         return VPointF(
460             when {
461                 mode.x == EXACTLY -> MeasureSpec.getSize(widthMeasureSpec).toFloat()
462                 else -> interpBounds.width + 2 * lockScreenPaint.strokeWidth
463             },
464             when {
465                 mode.y == EXACTLY -> MeasureSpec.getSize(heightMeasureSpec).toFloat()
466                 else -> interpBounds.height + 2 * lockScreenPaint.strokeWidth
467             },
468         )
469     }
470 
471     /** Set the measured size of the view to match the interpolated text bounds */
setInterpolatedSizenull472     private fun setInterpolatedSize(
473         measureBounds: VPointF,
474         widthMeasureSpec: Int = measuredWidthAndState,
475         heightMeasureSpec: Int = measuredHeightAndState,
476     ) {
477         val mode =
478             VPoint(
479                 x = MeasureSpec.getMode(widthMeasureSpec),
480                 y = MeasureSpec.getMode(heightMeasureSpec),
481             )
482 
483         setMeasuredDimension(
484             MeasureSpec.makeMeasureSpec(measureBounds.x.roundToInt(), mode.x),
485             MeasureSpec.makeMeasureSpec(measureBounds.y.roundToInt(), mode.y),
486         )
487 
488         logger.d({
489             val size = VPointF.fromLong(long1)
490             val mode = VPoint.fromLong(long2)
491             "setInterpolatedSize(size=$size, mode=$mode)"
492         }) {
493             long1 = measureBounds.toLong()
494             long2 = mode.toLong()
495         }
496     }
497 
498     /** Set the location of the view to match the interpolated text bounds */
setInterpolatedLocationnull499     private fun setInterpolatedLocation(measureSize: VPointF): VRectF {
500         val pos =
501             VPointF(
502                 when (xAlignment) {
503                     XAlignment.LEFT -> layoutBounds.left
504                     XAlignment.CENTER -> layoutBounds.center.x - measureSize.x / 2f
505                     XAlignment.RIGHT -> layoutBounds.right - measureSize.x
506                 },
507                 when (verticalAlignment) {
508                     VerticalAlignment.TOP -> layoutBounds.top
509                     VerticalAlignment.CENTER -> layoutBounds.center.y - measureSize.y / 2f
510                     VerticalAlignment.BOTTOM -> layoutBounds.bottom - measureSize.y
511                     VerticalAlignment.BASELINE -> layoutBounds.center.y - measureSize.y / 2f
512                 },
513             )
514 
515         val targetRect = VRectF.fromTopLeft(pos, measureSize)
516         setFrame(
517             targetRect.left.roundToInt(),
518             targetRect.top.roundToInt(),
519             targetRect.right.roundToInt(),
520             targetRect.bottom.roundToInt(),
521         )
522         onViewBoundsChanged?.let { it(targetRect) }
523         logger.d({ "setInterpolatedLocation(${VRectF.fromLong(long1)})" }) {
524             long1 = targetRect.toLong()
525         }
526         return targetRect
527     }
528 
getDrawTranslationnull529     private fun getDrawTranslation(interpBounds: VRectF): VPointF {
530         val sizeDiff = this.measuredSize - interpBounds.size
531         val alignment =
532             VPointF(
533                 when (xAlignment) {
534                     XAlignment.LEFT -> 0f
535                     XAlignment.CENTER -> 0.5f
536                     XAlignment.RIGHT -> 1f
537                 },
538                 when (verticalAlignment) {
539                     VerticalAlignment.TOP -> 0f
540                     VerticalAlignment.CENTER -> 0.5f
541                     VerticalAlignment.BASELINE -> 0.5f
542                     VerticalAlignment.BOTTOM -> 1f
543                 },
544             )
545         val renderCorrection =
546             VPointF(
547                 x = -interpBounds.left,
548                 y = -interpBounds.top - (if (baseline != -1) baseline else measuredBaseline),
549             )
550         return sizeDiff * alignment + renderCorrection
551     }
552 
applyStylesnull553     fun applyStyles(textStyle: FontTextStyle, aodStyle: FontTextStyle?) {
554         this.textStyle = textStyle
555         lockScreenPaint.strokeJoin = Paint.Join.ROUND
556         lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
557         typeface = lockScreenPaint.typeface
558         textStyle.lineHeight?.let { lineHeight = it.roundToInt() }
559 
560         this.aodStyle = aodStyle ?: textStyle.copy()
561         aodDozingInterpolator = this.aodStyle.transitionInterpolator ?: Interpolators.LINEAR
562         lockScreenPaint.strokeWidth = textBorderWidth
563         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
564         setInterpolatorPaint()
565         recomputeMaxSingleDigitSizes()
566         invalidate()
567     }
568 
569     /** When constrainedByHeight is on, targetFontSizePx is the constrained height of textView */
applyTextSizenull570     fun applyTextSize(targetFontSizePx: Float?, constrainedByHeight: Boolean = false) {
571         val adjustedFontSizePx = adjustFontSize(targetFontSizePx, constrainedByHeight)
572         val fontSizePx = adjustedFontSizePx * (textStyle.fontSizeScale ?: 1f)
573         aodFontSizePx =
574             adjustedFontSizePx * (aodStyle.fontSizeScale ?: textStyle.fontSizeScale ?: 1f)
575         if (fontSizePx > 0) {
576             setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
577             lockScreenPaint.textSize = textSize
578             updateTextBounds()
579         }
580         if (!constrainedByHeight) {
581             val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
582             fontSizeAdjustFactor = lastUnconstrainedHeight / lastUnconstrainedTextSize
583         }
584 
585         lockScreenPaint.strokeWidth = textBorderWidth
586         recomputeMaxSingleDigitSizes()
587 
588         if (this::textAnimator.isInitialized) {
589             textAnimator.setTextStyle(TextAnimator.Style(textSize = lockScreenPaint.textSize))
590         }
591     }
592 
recomputeMaxSingleDigitSizesnull593     private fun recomputeMaxSingleDigitSizes() {
594         maxSingleDigitHeight = 0f
595         maxSingleDigitWidth = 0f
596 
597         for (i in 0..9) {
598             val rectForCalculate = lockScreenPaint.getTextBounds("$i")
599             maxSingleDigitHeight = max(maxSingleDigitHeight, rectForCalculate.height)
600             maxSingleDigitWidth = max(maxSingleDigitWidth, rectForCalculate.width)
601         }
602         maxSingleDigitWidth += 2 * lockScreenPaint.strokeWidth
603         maxSingleDigitHeight += 2 * lockScreenPaint.strokeWidth
604     }
605 
606     /** Called without animation, can be used to set the initial state of animator */
setInterpolatorPaintnull607     private fun setInterpolatorPaint() {
608         if (this::textAnimator.isInitialized) {
609             // set initial style
610             textAnimator.textInterpolator.targetPaint.set(lockScreenPaint)
611             textAnimator.textInterpolator.onTargetPaintModified()
612             textAnimator.setTextStyle(
613                 TextAnimator.Style(
614                     fVar = lsFontVariation,
615                     textSize = lockScreenPaint.textSize,
616                     color = lockscreenColor,
617                 )
618             )
619         }
620     }
621 
622     /** Updates both the lockscreen text bounds and animation text bounds */
updateTextBoundsnull623     private fun updateTextBounds() {
624         textBounds = lockScreenPaint.getTextBounds(text)
625         updateAnimationTextBounds()
626     }
627 
628     /**
629      * Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and
630      * rebase if previous animator is canceled so basePaint will store the state we transition from
631      * and targetPaint will store the state we transition to
632      */
updateAnimationTextBoundsnull633     private fun updateAnimationTextBounds() {
634         drawnProgress = null
635         if (this::textAnimator.isInitialized) {
636             prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
637             targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
638         } else {
639             prevTextBounds = textBounds
640             targetTextBounds = textBounds
641         }
642     }
643 
644     /**
645      * Adjust text size to adapt to large display / font size where the text view will be
646      * constrained by height
647      */
adjustFontSizenull648     private fun adjustFontSize(targetFontSizePx: Float?, constrainedByHeight: Boolean): Float {
649         return if (constrainedByHeight) {
650             min((targetFontSizePx ?: 0F) / fontSizeAdjustFactor, lastUnconstrainedTextSize)
651         } else {
652             lastUnconstrainedTextSize = targetFontSizePx ?: 1F
653             lastUnconstrainedTextSize
654         }
655     }
656 
657     companion object {
658         private val PORTER_DUFF_XFER_MODE_PAINT =
<lambda>null659             Paint().also { it.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) }
660 
661         val FIDGET_HAPTICS =
662             VibrationEffect.startComposition()
663                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, 0)
664                 .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 1.0f, 43)
665                 .compose()
666 
667         val CHARGE_ANIMATION_DURATION = 500L
668         val AXIS_CHANGE_ANIMATION_DURATION = 400L
669         val FIDGET_ANIMATION_DURATION = 250L
670         val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f)
671         val FIDGET_DISTS =
672             mapOf(
673                 GSFAxes.WEIGHT.tag to Pair(200f, 500f),
674                 GSFAxes.WIDTH.tag to Pair(30f, 75f),
675                 GSFAxes.ROUND.tag to Pair(0f, 50f),
676                 GSFAxes.SLANT.tag to Pair(0f, -5f),
677             )
678 
679         val AOD_COLOR = Color.WHITE
680         private val LS_WEIGHT_AXIS = GSFAxes.WEIGHT to 400f
681         private val AOD_WEIGHT_AXIS = GSFAxes.WEIGHT to 200f
682         private val WIDTH_AXIS = GSFAxes.WIDTH to 85f
683         private val ROUND_AXIS = GSFAxes.ROUND to 0f
684         private val SLANT_AXIS = GSFAxes.SLANT to 0f
685 
686         // Axes for Legacy version of the Flex Clock
687         private val FLEX_LS_WEIGHT_AXIS = GSFAxes.WEIGHT to 600f
688         private val FLEX_AOD_LARGE_WEIGHT_AXIS = GSFAxes.WEIGHT to 74f
689         private val FLEX_AOD_SMALL_WEIGHT_AXIS = GSFAxes.WEIGHT to 133f
690         private val FLEX_LS_WIDTH_AXIS = GSFAxes.WIDTH to 100f
691         private val FLEX_AOD_WIDTH_AXIS = GSFAxes.WIDTH to 43f
692         private val FLEX_ROUND_AXIS = GSFAxes.ROUND to 100f
693 
fromAxesnull694         private fun fromAxes(vararg axes: Pair<AxisDefinition, Float>): ClockAxisStyle {
695             return ClockAxisStyle(axes.map { (def, value) -> def.tag to value }.toMap())
696         }
697     }
698 }
699