Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f85044e5 authored by Hawkwood Glazier's avatar Hawkwood Glazier
Browse files

Modify layout & rendering translation for ClockView

This has the views in question better report their bounds, which allows
other elements to animate around them correctly as the text is squashed
or expanded. It also corrects a few issues with rendering of the text
during an animation. Previously all of this was handled via canvas
translations. I've also added some utility value types that should help
with clarity of the math, but also avoid making too many allocations
during render/layout.

Bug: 394676792
Bug: 398004673
Test: Manually checked layout & rendering
Flag: com.android.systemui.shared.clock_reactive_variants
Change-Id: Ib46597d76a3cdd5bb08ce208cec9f1a2fe56a729
parent 93cc9d2f
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@
    <dimen name="small_clock_padding_top">28dp</dimen>
    <dimen name="clock_padding_start">28dp</dimen>
    <dimen name="weather_date_icon_padding">28dp</dimen>
    <dimen name="clock_vertical_digit_buffer">8dp</dimen>

    <!-- When large clock is showing, offset the smartspace by this amount -->
    <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
+12 −15
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
@@ -16,20 +16,17 @@

package com.android.systemui.shared.clocks

import android.graphics.Rect
import android.view.View
import android.graphics.Canvas

fun computeLayoutDiff(
    view: View,
    targetRegion: Rect,
    isLargeClock: Boolean,
): Pair<Float, Float> {
    val parent = view.parent
    if (parent is View && parent.isLaidOut() && isLargeClock) {
        return Pair(
            targetRegion.centerX() - parent.width / 2f,
            targetRegion.centerY() - parent.height / 2f
        )
object CanvasUtil {
    fun Canvas.translate(pt: VPointF) = this.translate(pt.x, pt.y)

    fun Canvas.translate(pt: VPoint) = this.translate(pt.x.toFloat(), pt.y.toFloat())

    fun <T> Canvas.use(func: (Canvas) -> T): T {
        val saveNum = save()
        val result = func(this)
        restoreToCount(saveNum)
        return result
    }
    return Pair(0f, 0f)
}
+18 −36
Original line number Diff line number Diff line
@@ -20,57 +20,42 @@ import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
import android.graphics.Point
import com.android.systemui.shared.clocks.VPointF.Companion.times

class DigitTranslateAnimator(val updateCallback: () -> Unit) {
    val DEFAULT_ANIMATION_DURATION = 500L
    val updatedTranslate = Point(0, 0)
class DigitTranslateAnimator(private val updateCallback: (VPointF) -> Unit) {
    var currentTranslation = VPointF.ZERO
    var baseTranslation = VPointF.ZERO
    var targetTranslation = VPointF.ZERO

    val baseTranslation = Point(0, 0)
    var targetTranslation: Point? = null
    val bounceAnimator: ValueAnimator =
    private val bounceAnimator: ValueAnimator =
        ValueAnimator.ofFloat(1f).apply {
            duration = DEFAULT_ANIMATION_DURATION
            addUpdateListener {
                updateTranslation(it.animatedFraction, updatedTranslate)
                updateCallback()
            }
            addUpdateListener { updateCallback(getInterpolatedTranslation(it.animatedFraction)) }
            addListener(
                object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) {
                        rebase()
                        baseTranslation = currentTranslation
                    }

                    override fun onAnimationCancel(animation: Animator) {
                        rebase()
                        baseTranslation = currentTranslation
                    }
                }
            )
        }

    fun rebase() {
        baseTranslation.x = updatedTranslate.x
        baseTranslation.y = updatedTranslate.y
    }

    fun animatePosition(
        animate: Boolean = true,
        delay: Long = 0,
        duration: Long = -1L,
        duration: Long,
        interpolator: TimeInterpolator? = null,
        targetTranslation: Point? = null,
        targetTranslation: VPointF,
        onAnimationEnd: Runnable? = null,
    ) {
        this.targetTranslation = targetTranslation ?: Point(0, 0)
        this.targetTranslation = targetTranslation
        if (animate) {
            bounceAnimator.cancel()
            bounceAnimator.startDelay = delay
            bounceAnimator.duration =
                if (duration == -1L) {
                    DEFAULT_ANIMATION_DURATION
                } else {
                    duration
                }
            bounceAnimator.duration = duration
            interpolator?.let { bounceAnimator.interpolator = it }
            if (onAnimationEnd != null) {
                val listener =
@@ -89,16 +74,13 @@ class DigitTranslateAnimator(val updateCallback: () -> Unit) {
            bounceAnimator.start()
        } else {
            // No animation is requested, thus set base and target state to the same state.
            updateTranslation(1F, updatedTranslate)
            rebase()
            updateCallback()
            currentTranslation = targetTranslation
            baseTranslation = targetTranslation
            updateCallback(targetTranslation)
        }
    }

    fun updateTranslation(progress: Float, outPoint: Point) {
        outPoint.x =
            (baseTranslation.x + progress * (targetTranslation!!.x - baseTranslation.x)).toInt()
        outPoint.y =
            (baseTranslation.y + progress * (targetTranslation!!.y - baseTranslation.y)).toInt()
    fun getInterpolatedTranslation(progress: Float): VPointF {
        return baseTranslation + progress * (targetTranslation - baseTranslation)
    }
}
+7 −5
Original line number Diff line number Diff line
@@ -35,12 +35,14 @@ import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
import com.android.systemui.plugins.clocks.ThemeConfig
import com.android.systemui.plugins.clocks.WeatherData
import com.android.systemui.plugins.clocks.ZenData
import com.android.systemui.shared.clocks.ViewUtils.computeLayoutDiff
import com.android.systemui.shared.clocks.view.FlexClockView
import com.android.systemui.shared.clocks.view.HorizontalAlignment
import com.android.systemui.shared.clocks.view.VerticalAlignment
import java.util.Locale
import java.util.TimeZone
import kotlin.math.max
import kotlin.math.roundToInt

// TODO(b/364680879): Merge w/ ComposedDigitalLayerController
class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock: Boolean) :
@@ -168,17 +170,17 @@ class FlexClockFaceController(clockCtx: ClockContext, private val isLargeClock:
                        else targetRegion.height() / maxHeight

                    FrameLayout.LayoutParams(
                        (maxWidth * scale).toInt(),
                        (maxHeight * scale).toInt(),
                        (maxWidth * scale).roundToInt(),
                        (maxHeight * scale).roundToInt(),
                    )
                }

            lp.gravity = Gravity.CENTER
            view.layoutParams = lp
            targetRegion?.let {
                val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock)
                view.translationX = xDiff
                view.translationY = yDiff
                val diff = view.computeLayoutDiff(it, isLargeClock)
                view.translationX = diff.x
                view.translationY = diff.y
            }
        }

+224 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.shared.clocks

import android.graphics.Point
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt

private val X_MASK: ULong = 0xFFFFFFFF00000000U
private val Y_MASK: ULong = 0x00000000FFFFFFFFU

private fun unpackX(data: ULong): Int = ((data and X_MASK) shr 32).toInt()

private fun unpackY(data: ULong): Int = (data and Y_MASK).toInt()

private fun pack(x: Int, y: Int): ULong {
    return ((x.toULong() shl 32) and X_MASK) or (y.toULong() and Y_MASK)
}

@JvmInline
value class VPointF(private val data: ULong) {
    val x: Float
        get() = Float.fromBits(unpackX(data))

    val y: Float
        get() = Float.fromBits(unpackY(data))

    constructor() : this(0f, 0f)

    constructor(pt: PointF) : this(pt.x, pt.y)

    constructor(x: Int, y: Int) : this(x.toFloat(), y.toFloat())

    constructor(x: Int, y: Float) : this(x.toFloat(), y)

    constructor(x: Float, y: Int) : this(x, y.toFloat())

    constructor(x: Float, y: Float) : this(pack(x.toBits(), y.toBits()))

    fun toPointF() = PointF(x, y)

    fun lengthSq(): Float = x * x + y * y

    fun length(): Float = sqrt(lengthSq())

    fun abs() = VPointF(abs(x), abs(y))

    fun dot(pt: VPointF): Float = x * pt.x + y * pt.y

    fun normalize(): VPointF {
        val length = this.length()
        return VPointF(x / length, y / length)
    }

    operator fun component1(): Float = x

    operator fun component2(): Float = y

    override fun toString() = "($x, $y)"

    operator fun plus(pt: VPoint) = VPointF(x + pt.x, y + pt.y)

    operator fun plus(pt: VPointF) = VPointF(x + pt.x, y + pt.y)

    operator fun plus(value: Int) = VPointF(x + value, y + value)

    operator fun plus(value: Float) = VPointF(x + value, y + value)

    operator fun minus(pt: VPoint) = VPointF(x - pt.x, y - pt.y)

    operator fun minus(pt: VPointF) = VPointF(x - pt.x, y - pt.y)

    operator fun minus(value: Int) = VPointF(x - value, y - value)

    operator fun minus(value: Float) = VPointF(x - value, y - value)

    operator fun times(pt: VPoint) = VPointF(x * pt.x, y * pt.y)

    operator fun times(pt: VPointF) = VPointF(x * pt.x, y * pt.y)

    operator fun times(value: Int) = VPointF(x * value, y * value)

    operator fun times(value: Float) = VPointF(x * value, y * value)

    operator fun div(pt: VPoint) = VPointF(x / pt.x, y / pt.y)

    operator fun div(pt: VPointF) = VPointF(x / pt.x, y / pt.y)

    operator fun div(value: Int) = VPointF(x / value, y / value)

    operator fun div(value: Float) = VPointF(x / value, y / value)

    companion object {
        val ZERO = VPointF(0, 0)

        fun max(lhs: VPointF, rhs: VPointF) = VPointF(max(lhs.x, rhs.x), max(lhs.y, rhs.y))

        fun min(lhs: VPointF, rhs: VPointF) = VPointF(min(lhs.x, rhs.x), min(lhs.y, rhs.y))

        operator fun Float.plus(value: VPointF) = VPointF(this + value.x, this + value.y)

        operator fun Int.minus(value: VPointF) = VPointF(this - value.x, this - value.y)

        operator fun Float.minus(value: VPointF) = VPointF(this - value.x, this - value.y)

        operator fun Int.times(value: VPointF) = VPointF(this * value.x, this * value.y)

        operator fun Float.times(value: VPointF) = VPointF(this * value.x, this * value.y)

        operator fun Int.div(value: VPointF) = VPointF(this / value.x, this / value.y)

        operator fun Float.div(value: VPointF) = VPointF(this / value.x, this / value.y)

        val RectF.center: VPointF
            get() = VPointF(centerX(), centerY())

        val RectF.size: VPointF
            get() = VPointF(width(), height())
    }
}

@JvmInline
value class VPoint(private val data: ULong) {
    val x: Int
        get() = unpackX(data)

    val y: Int
        get() = unpackY(data)

    constructor() : this(0, 0)

    constructor(x: Int, y: Int) : this(pack(x, y))

    fun toPoint() = Point(x, y)

    fun abs() = VPoint(abs(x), abs(y))

    operator fun component1(): Int = x

    operator fun component2(): Int = y

    override fun toString() = "($x, $y)"

    operator fun plus(pt: VPoint) = VPoint(x + pt.x, y + pt.y)

    operator fun plus(pt: VPointF) = VPointF(x + pt.x, y + pt.y)

    operator fun plus(value: Int) = VPoint(x + value, y + value)

    operator fun plus(value: Float) = VPointF(x + value, y + value)

    operator fun minus(pt: VPoint) = VPoint(x - pt.x, y - pt.y)

    operator fun minus(pt: VPointF) = VPointF(x - pt.x, y - pt.y)

    operator fun minus(value: Int) = VPoint(x - value, y - value)

    operator fun minus(value: Float) = VPointF(x - value, y - value)

    operator fun times(pt: VPoint) = VPoint(x * pt.x, y * pt.y)

    operator fun times(pt: VPointF) = VPointF(x * pt.x, y * pt.y)

    operator fun times(value: Int) = VPoint(x * value, y * value)

    operator fun times(value: Float) = VPointF(x * value, y * value)

    operator fun div(pt: VPoint) = VPoint(x / pt.x, y / pt.y)

    operator fun div(pt: VPointF) = VPointF(x / pt.x, y / pt.y)

    operator fun div(value: Int) = VPoint(x / value, y / value)

    operator fun div(value: Float) = VPointF(x / value, y / value)

    companion object {
        val ZERO = VPoint(0, 0)

        fun max(lhs: VPoint, rhs: VPoint) = VPoint(max(lhs.x, rhs.x), max(lhs.y, rhs.y))

        fun min(lhs: VPoint, rhs: VPoint) = VPoint(min(lhs.x, rhs.x), min(lhs.y, rhs.y))

        operator fun Int.plus(value: VPoint) = VPoint(this + value.x, this + value.y)

        operator fun Float.plus(value: VPoint) = VPointF(this + value.x, this + value.y)

        operator fun Int.minus(value: VPoint) = VPoint(this - value.x, this - value.y)

        operator fun Float.minus(value: VPoint) = VPointF(this - value.x, this - value.y)

        operator fun Int.times(value: VPoint) = VPoint(this * value.x, this * value.y)

        operator fun Float.times(value: VPoint) = VPointF(this * value.x, this * value.y)

        operator fun Int.div(value: VPoint) = VPoint(this / value.x, this / value.y)

        operator fun Float.div(value: VPoint) = VPointF(this / value.x, this / value.y)

        val Rect.center: VPoint
            get() = VPoint(centerX(), centerY())

        val Rect.size: VPoint
            get() = VPoint(width(), height())
    }
}
Loading