• 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.graphics.Canvas
20 import android.icu.text.NumberFormat
21 import android.util.MathUtils.constrainedMap
22 import android.view.View
23 import android.view.ViewGroup
24 import android.widget.RelativeLayout
25 import androidx.annotation.VisibleForTesting
26 import androidx.core.view.children
27 import com.android.app.animation.Interpolators
28 import com.android.systemui.customization.R
29 import com.android.systemui.plugins.clocks.ClockAxisStyle
30 import com.android.systemui.plugins.clocks.ClockLogger
31 import com.android.systemui.plugins.clocks.VPoint
32 import com.android.systemui.plugins.clocks.VPointF
33 import com.android.systemui.plugins.clocks.VPointF.Companion.max
34 import com.android.systemui.plugins.clocks.VPointF.Companion.times
35 import com.android.systemui.plugins.clocks.VRectF
36 import com.android.systemui.shared.clocks.CanvasUtil.translate
37 import com.android.systemui.shared.clocks.CanvasUtil.use
38 import com.android.systemui.shared.clocks.ClockContext
39 import com.android.systemui.shared.clocks.DigitTranslateAnimator
40 import com.android.systemui.shared.clocks.ViewUtils.measuredSize
41 import java.util.Locale
42 import kotlin.collections.filterNotNull
43 import kotlin.collections.map
44 import kotlin.math.abs
45 import kotlin.math.max
46 import kotlin.math.min
47 import kotlin.math.roundToInt
48 
49 fun clamp(value: Float, minVal: Float, maxVal: Float): Float = max(min(value, maxVal), minVal)
50 
51 class FlexClockView(clockCtx: ClockContext) : ViewGroup(clockCtx.context) {
52     protected val logger = ClockLogger(this, clockCtx.messageBuffer, this::class.simpleName!!)
53         get() = field ?: ClockLogger.INIT_LOGGER
54 
55     @VisibleForTesting
56     var isAnimationEnabled = true
57         set(value) {
58             field = value
59             childViews.forEach { view -> view.isAnimationEnabled = value }
60         }
61 
62     var dozeFraction: Float = 0F
63         set(value) {
64             field = value
65             childViews.forEach { view -> view.dozeFraction = field }
66         }
67 
68     var isReactiveTouchInteractionEnabled = false
69         set(value) {
70             field = value
71         }
72 
73     var _childViews: List<SimpleDigitalClockTextView>? = null
74     val childViews: List<SimpleDigitalClockTextView>
75         get() {
76             return _childViews
77                 ?: this.children
78                     .map { child -> child as? SimpleDigitalClockTextView }
79                     .filterNotNull()
80                     .toList()
81                     .also { _childViews = it }
82         }
83 
84     private var maxChildSize = VPointF(-1, -1)
85     private val lockscreenTranslate = VPointF.ZERO
86     private var aodTranslate = VPointF.ZERO
87 
88     private var onAnimateDoze: (() -> Unit)? = null
89     private var isDozeReadyToAnimate = false
90 
91     // Does the current language have mono vertical size when displaying numerals
92     private var isMonoVerticalNumericLineSpacing = true
93 
94     init {
95         setWillNotDraw(false)
96         layoutParams =
97             RelativeLayout.LayoutParams(
98                 ViewGroup.LayoutParams.WRAP_CONTENT,
99                 ViewGroup.LayoutParams.WRAP_CONTENT,
100             )
101         updateLocale(Locale.getDefault())
102     }
103 
104     var onViewBoundsChanged: ((VRectF) -> Unit)? = null
105     private val digitOffsets = mutableMapOf<Int, Float>()
106 
107     protected fun calculateSize(
108         widthMeasureSpec: Int,
109         heightMeasureSpec: Int,
110         shouldMeasureChildren: Boolean,
111     ): VPointF {
112         maxChildSize = VPointF(-1, -1)
113         childViews.forEach { textView ->
114             if (shouldMeasureChildren) {
115                 textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
116             }
117             maxChildSize = max(maxChildSize, textView.measuredSize)
118         }
119         aodTranslate = VPointF.ZERO
120         // TODO(b/364680879): Cleanup
121         /*
122         aodTranslate = VPointF(
123             maxChildSize.x * AOD_HORIZONTAL_TRANSLATE_RATIO,
124             maxChildSize.y * AOD_VERTICAL_TRANSLATE_RATIO
125         )
126         */
127 
128         val xScale = if (childViews.size < 4) 1f else 2f
129         val yBuffer = context.resources.getDimensionPixelSize(R.dimen.clock_vertical_digit_buffer)
130         return (maxChildSize + aodTranslate.abs()) * VPointF(xScale, 2f) + VPointF(0f, yBuffer)
131     }
132 
133     override fun onViewAdded(child: View?) {
134         if (child == null) return
135         logger.onViewAdded(child)
136         super.onViewAdded(child)
137         (child as? SimpleDigitalClockTextView)?.let {
138             it.digitTranslateAnimator = DigitTranslateAnimator { invalidate() }
139         }
140         child.setWillNotDraw(true)
141         _childViews = null
142     }
143 
144     override fun onViewRemoved(child: View?) {
145         super.onViewRemoved(child)
146         _childViews = null
147     }
148 
149     fun refreshTime() {
150         logger.refreshTime()
151         childViews.forEach { textView -> textView.refreshText() }
152     }
153 
154     override fun setVisibility(visibility: Int) {
155         logger.setVisibility(visibility)
156         super.setVisibility(visibility)
157     }
158 
159     override fun setAlpha(alpha: Float) {
160         logger.setAlpha(alpha)
161         super.setAlpha(alpha)
162     }
163 
164     override fun invalidate() {
165         logger.invalidate()
166         super.invalidate()
167     }
168 
169     override fun requestLayout() {
170         logger.requestLayout()
171         super.requestLayout()
172     }
173 
174     fun updateMeasuredSize() =
175         updateMeasuredSize(
176             measuredWidthAndState,
177             measuredHeightAndState,
178             shouldMeasureChildren = false,
179         )
180 
181     private fun updateMeasuredSize(
182         widthMeasureSpec: Int,
183         heightMeasureSpec: Int,
184         shouldMeasureChildren: Boolean,
185     ) {
186         val size = calculateSize(widthMeasureSpec, heightMeasureSpec, shouldMeasureChildren)
187         setMeasuredDimension(size.x.roundToInt(), size.y.roundToInt())
188     }
189 
190     fun updateLocation() {
191         val layoutBounds = this.layoutBounds ?: return
192         val bounds = VRectF.fromCenter(layoutBounds.center, this.measuredSize)
193         setFrame(
194             bounds.left.roundToInt(),
195             bounds.top.roundToInt(),
196             bounds.right.roundToInt(),
197             bounds.bottom.roundToInt(),
198         )
199         updateChildFrames(isLayout = false)
200         onViewBoundsChanged?.let { it(bounds) }
201     }
202 
203     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
204         logger.onMeasure(widthMeasureSpec, heightMeasureSpec)
205         updateMeasuredSize(widthMeasureSpec, heightMeasureSpec, shouldMeasureChildren = true)
206 
207         isDozeReadyToAnimate = true
208         onAnimateDoze?.invoke()
209         onAnimateDoze = null
210     }
211 
212     private var layoutBounds = VRectF.ZERO
213 
214     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
215         logger.onLayout(changed, left, top, right, bottom)
216         layoutBounds = VRectF(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat())
217         updateChildFrames(isLayout = true)
218     }
219 
220     private fun updateChildFrames(isLayout: Boolean) {
221         val yBuffer = context.resources.getDimensionPixelSize(R.dimen.clock_vertical_digit_buffer)
222         childViews.forEach { child ->
223             var offset =
224                 maxChildSize.run {
225                     when (child.id) {
226                         R.id.HOUR_FIRST_DIGIT -> VPointF.ZERO
227                         R.id.HOUR_SECOND_DIGIT -> VPointF(x, 0f)
228                         R.id.HOUR_DIGIT_PAIR -> VPointF.ZERO
229                         // Add a small vertical buffer for second line views
230                         R.id.MINUTE_DIGIT_PAIR -> VPointF(0f, y + yBuffer)
231                         R.id.MINUTE_FIRST_DIGIT -> VPointF(0f, y + yBuffer)
232                         R.id.MINUTE_SECOND_DIGIT -> VPointF(x, y + yBuffer)
233                         else -> VPointF.ZERO
234                     }
235                 }
236 
237             val childSize = child.measuredSize
238             offset += aodTranslate.abs()
239 
240             // Horizontal offset to center each view in the available space
241             val midX = if (childViews.size < 4) measuredWidth / 2f else measuredWidth / 4f
242             offset += VPointF(midX - childSize.x / 2f, 0f)
243 
244             val setPos = if (isLayout) child::layout else child::setLeftTopRightBottom
245             setPos(
246                 offset.x.roundToInt(),
247                 offset.y.roundToInt(),
248                 (offset.x + childSize.x).roundToInt(),
249                 (offset.y + childSize.y).roundToInt(),
250             )
251         }
252     }
253 
254     override fun onDraw(canvas: Canvas) {
255         logger.onDraw()
256         childViews.forEach { child ->
257             canvas.use { canvas ->
258                 canvas.translate(digitOffsets.getOrDefault(child.id, 0f), 0f)
259                 canvas.translate(child.left.toFloat(), child.top.toFloat())
260                 child.draw(canvas)
261             }
262         }
263     }
264 
265     fun onLocaleChanged(locale: Locale) {
266         updateLocale(locale)
267         requestLayout()
268     }
269 
270     fun updateColor(color: Int) {
271         childViews.forEach { view -> view.updateColor(color) }
272         invalidate()
273     }
274 
275     fun updateAxes(axes: ClockAxisStyle, isAnimated: Boolean) {
276         childViews.forEach { view -> view.updateAxes(axes, isAnimated) }
277         requestLayout()
278     }
279 
280     fun onFontSettingChanged(fontSizePx: Float) {
281         childViews.forEach { view -> view.applyTextSize(fontSizePx) }
282     }
283 
284     fun animateDoze(isDozing: Boolean, isAnimated: Boolean) {
285         fun executeDozeAnimation() {
286             childViews.forEach { view -> view.animateDoze(isDozing, isAnimated) }
287             if (maxChildSize.x < 0 || maxChildSize.y < 0) {
288                 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
289             }
290             childViews.forEach { textView ->
291                 textView.digitTranslateAnimator?.let {
292                     if (!isDozing) {
293                         it.animatePosition(
294                             animate = isAnimated && isAnimationEnabled,
295                             interpolator = Interpolators.EMPHASIZED,
296                             duration = AOD_TRANSITION_DURATION,
297                             targetTranslation =
298                                 updateDirectionalTargetTranslate(id, lockscreenTranslate),
299                         )
300                     } else {
301                         it.animatePosition(
302                             animate = isAnimated && isAnimationEnabled,
303                             interpolator = Interpolators.EMPHASIZED,
304                             duration = AOD_TRANSITION_DURATION,
305                             onAnimationEnd = null,
306                             targetTranslation = updateDirectionalTargetTranslate(id, aodTranslate),
307                         )
308                     }
309                 }
310             }
311         }
312 
313         if (isDozeReadyToAnimate) executeDozeAnimation()
314         else onAnimateDoze = { executeDozeAnimation() }
315     }
316 
317     fun animateCharge() {
318         childViews.forEach { view -> view.animateCharge() }
319         childViews.forEach { textView ->
320             textView.digitTranslateAnimator?.let {
321                 it.animatePosition(
322                     animate = isAnimationEnabled,
323                     interpolator = Interpolators.EMPHASIZED,
324                     duration = CHARGING_TRANSITION_DURATION,
325                     onAnimationEnd = {
326                         it.animatePosition(
327                             animate = isAnimationEnabled,
328                             interpolator = Interpolators.EMPHASIZED,
329                             duration = CHARGING_TRANSITION_DURATION,
330                             targetTranslation =
331                                 updateDirectionalTargetTranslate(
332                                     textView.id,
333                                     if (dozeFraction == 1F) aodTranslate else lockscreenTranslate,
334                                 ),
335                         )
336                     },
337                     targetTranslation =
338                         updateDirectionalTargetTranslate(
339                             textView.id,
340                             if (dozeFraction == 1F) lockscreenTranslate else aodTranslate,
341                         ),
342                 )
343             }
344         }
345     }
346 
347     fun animateFidget(x: Float, y: Float) {
348         val touchPt = VPointF(x, y)
349         val ints = intArrayOf(0, 0)
350         childViews
351             .sortedBy { view ->
352                 view.getLocationInWindow(ints)
353                 val loc = VPoint(ints[0], ints[1])
354                 val center = loc + view.measuredSize / 2f
355                 (center - touchPt).length()
356             }
357             .forEachIndexed { i, view ->
358                 view.animateFidget(FIDGET_DELAYS[min(i, FIDGET_DELAYS.size - 1)])
359             }
360     }
361 
362     private fun updateLocale(locale: Locale) {
363         isMonoVerticalNumericLineSpacing =
364             !NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES.any {
365                 val newLocaleNumberFormat =
366                     NumberFormat.getInstance(locale).format(FORMAT_NUMBER.toLong())
367                 val nonMonoVerticalNumericLineSpaceNumberFormat =
368                     NumberFormat.getInstance(Locale.forLanguageTag(it))
369                         .format(FORMAT_NUMBER.toLong())
370                 newLocaleNumberFormat == nonMonoVerticalNumericLineSpaceNumberFormat
371             }
372     }
373 
374     /**
375      * Offsets the textViews of the clock for the step clock animation.
376      *
377      * The animation makes the textViews of the clock move at different speeds, when the clock is
378      * moving horizontally.
379      *
380      * @param clockStartLeft the [getLeft] position of the clock, before it started moving.
381      * @param clockMoveDirection the direction in which it is moving. A positive number means right,
382      *   and negative means left.
383      * @param moveFraction fraction of the clock movement. 0 means it is at the beginning, and 1
384      *   means it finished moving.
385      */
386     fun offsetGlyphsForStepClockAnimation(
387         clockStartLeft: Int,
388         clockMoveDirection: Int,
389         moveFraction: Float,
390     ) {
391         // TODO(b/393577936): The step animation isn't correct with the two pairs approach
392         val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
393         // The sign of moveAmountDeltaForDigit is already set here
394         // we can interpret (left - clockStartLeft) as (destinationPosition - originPosition)
395         // so we no longer need to multiply direct sign to moveAmountDeltaForDigit
396         val currentMoveAmount = left - clockStartLeft
397         var index = 0
398         childViews.forEach { child ->
399             val digitFraction =
400                 getDigitFraction(
401                     digit = index++,
402                     isMovingToCenter = isMovingToCenter,
403                     fraction = moveFraction,
404                 )
405             // left here is the final left position after the animation is done
406             val moveAmountForDigit = currentMoveAmount * digitFraction
407             var moveAmountDeltaForDigit = moveAmountForDigit - currentMoveAmount
408             if (isMovingToCenter && moveAmountForDigit < 0) moveAmountDeltaForDigit *= -1
409             digitOffsets[child.id] = moveAmountDeltaForDigit
410             invalidate()
411         }
412     }
413 
414     private val moveToCenterDelays: List<Int>
415         get() = if (isLayoutRtl) MOVE_LEFT_DELAYS else MOVE_RIGHT_DELAYS
416 
417     private val moveToSideDelays: List<Int>
418         get() = if (isLayoutRtl) MOVE_RIGHT_DELAYS else MOVE_LEFT_DELAYS
419 
420     private fun getDigitFraction(digit: Int, isMovingToCenter: Boolean, fraction: Float): Float {
421         // The delay for the digit, in terms of fraction.
422         // (i.e. the digit should not move during 0.0 - 0.1).
423         val delays = if (isMovingToCenter) moveToCenterDelays else moveToSideDelays
424         val digitInitialDelay = delays[digit] * MOVE_DIGIT_STEP
425         return MOVE_INTERPOLATOR.getInterpolation(
426             constrainedMap(
427                 /* rangeMin= */ 0.0f,
428                 /* rangeMax= */ 1.0f,
429                 /* valueMin= */ digitInitialDelay,
430                 /* valueMax= */ digitInitialDelay + availableAnimationTime(childViews.size),
431                 /* value= */ fraction,
432             )
433         )
434     }
435 
436     companion object {
437         val AOD_TRANSITION_DURATION = 750L
438         val CHARGING_TRANSITION_DURATION = 300L
439 
440         val AOD_HORIZONTAL_TRANSLATE_RATIO = -0.15F
441         val AOD_VERTICAL_TRANSLATE_RATIO = 0.075F
442 
443         val FIDGET_DELAYS = listOf(0L, 75L, 150L, 225L)
444 
445         // Delays. Each digit's animation should have a slight delay, so we get a nice
446         // "stepping" effect. When moving right, the second digit of the hour should move first.
447         // When moving left, the first digit of the hour should move first. The lists encode
448         // the delay for each digit (hour[0], hour[1], minute[0], minute[1]), to be multiplied
449         // by delayMultiplier.
450         private val MOVE_LEFT_DELAYS = listOf(0, 1, 2, 3)
451         private val MOVE_RIGHT_DELAYS = listOf(1, 0, 3, 2)
452 
453         // How much delay to apply to each subsequent digit. This is measured in terms of "fraction"
454         // (i.e. a value of 0.1 would cause a digit to wait until fraction had hit 0.1, or 0.2 etc
455         // before moving).
456         //
457         // The current specs dictate that each digit should have a 33ms gap between them. The
458         // overall time is 1s right now.
459         private const val MOVE_DIGIT_STEP = 0.033f
460 
461         // Constants for the animation
462         private val MOVE_INTERPOLATOR = Interpolators.EMPHASIZED
463 
464         private const val FORMAT_NUMBER = 1234567890
465 
466         // Total available transition time for each digit, taking into account the step. If step is
467         // 0.1, then digit 0 would animate over 0.0 - 0.7, making availableTime 0.7.
468         private fun availableAnimationTime(numDigits: Int): Float {
469             return 1.0f - MOVE_DIGIT_STEP * (numDigits.toFloat() - 1)
470         }
471 
472         // Add language tags below that do not have vertically mono spaced numerals
473         private val NON_MONO_VERTICAL_NUMERIC_LINE_SPACING_LANGUAGES =
474             setOf(
475                 "my" // Burmese
476             )
477 
478         // Use the sign of targetTranslation to control the direction of digit translation
479         fun updateDirectionalTargetTranslate(id: Int, targetTranslation: VPointF): VPointF {
480             return targetTranslation *
481                 when (id) {
482                     R.id.HOUR_FIRST_DIGIT -> VPointF(-1, -1)
483                     R.id.HOUR_SECOND_DIGIT -> VPointF(1, -1)
484                     R.id.MINUTE_FIRST_DIGIT -> VPointF(-1, 1)
485                     R.id.MINUTE_SECOND_DIGIT -> VPointF(1, 1)
486                     R.id.HOUR_DIGIT_PAIR -> VPointF(-1, -1)
487                     R.id.MINUTE_DIGIT_PAIR -> VPointF(-1, 1)
488                     else -> VPointF(1, 1)
489                 }
490         }
491     }
492 }
493