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