Loading mechanics/src/com/android/mechanics/spec/builder/MotionBuilderContext.kt +4 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,10 @@ import com.android.mechanics.spring.SpringParameters * Device / scheme specific context for building motion specs. * * See go/motion-system. * * @see rememberMotionBuilderContext for Compose * @see standardViewMotionBuilderContext for Views * @see expressiveViewMotionBuilderContext for Views */ interface MotionBuilderContext : Density { /** Loading mechanics/src/com/android/mechanics/view/ViewMotionBuilderContext.kt 0 → 100644 +127 −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.mechanics.view import android.content.Context import androidx.compose.ui.unit.Density import com.android.mechanics.spec.builder.MaterialSprings import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spring.SpringParameters import com.android.mechanics.view.ViewMaterialSprings.Default /** * Creates a [MotionBuilderContext] using the **standard** motion spec. * * See go/motion-system. * * @param context The context to derive the density from. */ fun standardViewMotionBuilderContext(context: Context): MotionBuilderContext { return standardViewMotionBuilderContext(context.resources.displayMetrics.density) } /** * Creates a [MotionBuilderContext] using the **standard** motion spec. * * See go/motion-system. * * @param density The density of the display, as a scaling factor for the dp to px conversion. */ fun standardViewMotionBuilderContext(density: Float): MotionBuilderContext { return with(ViewMaterialSprings.Default) { ViewMotionBuilderContext(Spatial, Effects, Density(density)) } } /** * Creates a [MotionBuilderContext] using the **expressive** motion spec. * * See go/motion-system. * * @param context The context to derive the density from. */ fun expressiveViewMotionBuilderContext(context: Context): MotionBuilderContext { return expressiveViewMotionBuilderContext(context.resources.displayMetrics.density) } /** * Creates a [MotionBuilderContext] using the **expressive** motion spec. * * See go/motion-system. * * @param density The density of the display, as a scaling factor for the dp to px conversion. */ fun expressiveViewMotionBuilderContext(density: Float): MotionBuilderContext { return with(ViewMaterialSprings.Expressive) { ViewMotionBuilderContext(Spatial, Effects, Density(density)) } } /** * Material motion system spring definitions. * * See go/motion-system. * * NOTE: These are only defined here since material spring parameters are not available for View * based APIs. There might be a delay in updating these values, should the material tokens be * updated in the future. * * @see rememberMotionBuilderContext for Compose */ object ViewMaterialSprings { object Default { val Spatial = MaterialSprings( SpringParameters(700.0f, 0.9f), SpringParameters(1400.0f, 0.9f), SpringParameters(300.0f, 0.9f), MotionBuilderContext.StableThresholdSpatial, ) val Effects = MaterialSprings( SpringParameters(1600.0f, 1.0f), SpringParameters(3800.0f, 1.0f), SpringParameters(800.0f, 1.0f), MotionBuilderContext.StableThresholdEffects, ) } object Expressive { val Spatial = MaterialSprings( SpringParameters(380.0f, 0.8f), SpringParameters(800.0f, 0.6f), SpringParameters(200.0f, 0.8f), MotionBuilderContext.StableThresholdSpatial, ) val Effects = MaterialSprings( SpringParameters(1600.0f, 1.0f), SpringParameters(3800.0f, 1.0f), SpringParameters(800.0f, 1.0f), MotionBuilderContext.StableThresholdEffects, ) } } internal class ViewMotionBuilderContext( override val spatial: MaterialSprings, override val effects: MaterialSprings, density: Density, ) : MotionBuilderContext, Density by density mechanics/tests/src/com/android/mechanics/view/ViewMotionBuilderContextTest.kt 0 → 100644 +77 −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. */ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) package com.android.mechanics.view import android.content.Context import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.builder.rememberMotionBuilderContext import com.google.common.truth.Truth import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ViewMotionBuilderContextTest { @get:Rule(order = 0) val rule = createComposeRule() @Test fun materialSprings_standardScheme_matchesComposeDefinition() { lateinit var viewContext: Context lateinit var composeReference: MotionBuilderContext rule.setContent { viewContext = LocalContext.current MaterialTheme(motionScheme = MotionScheme.standard()) { composeReference = rememberMotionBuilderContext() } } val underTest = standardViewMotionBuilderContext(viewContext) Truth.assertThat(underTest.density).isEqualTo(composeReference.density) Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial) Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects) } @Test fun materialSprings_expressiveScheme_matchesComposeDefinition() { lateinit var viewContext: Context lateinit var composeReference: MotionBuilderContext rule.setContent { viewContext = LocalContext.current MaterialTheme(motionScheme = MotionScheme.expressive()) { composeReference = rememberMotionBuilderContext() } } val underTest = expressiveViewMotionBuilderContext(viewContext) Truth.assertThat(underTest.density).isEqualTo(composeReference.density) Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial) Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects) } } toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt +19 −25 Original line number Diff line number Diff line Loading @@ -22,41 +22,38 @@ package com.google.android.torus.core.power */ class FpsThrottler { companion object { private const val NANO_TO_MILLIS = 1 / 1E6 const val NANO_TO_MILLIS = 1 / 1E6 const val FPS_120 = 120f const val FPS_60 = 60f const val FPS_30 = 30f const val FPS_18 = 18f @Deprecated(message = "Use FPS_60 instead.") const val HIGH_FPS = 60f @Deprecated(message = "Use FPS_30 instead.") const val MED_FPS = 30f @Deprecated(message = "Use FPS_18 instead.") const val LOW_FPS = 18f @Deprecated(message = "Use FPS_60 instead.") const val HIGH_FPS = 60f @Deprecated(message = "Use FPS_30 instead.") const val MED_FPS = 30f @Deprecated(message = "Use FPS_18 instead.") const val LOW_FPS = 18f /** Small tolerance (ms) for float precision in frame timing. */ const val TOLERANCE_MILLIS = 1L } private var fps: Float = FPS_60 @Volatile private var frameTimeMillis: Double = 1000.0 / fps.toDouble() @Volatile private var frameTimeMillis: Double = 1000.0 / fps.toDouble() private var lastFrameTimeNanos: Long = -1 @Volatile private var continuousRenderingMode: Boolean = true @Volatile private var continuousRenderingMode: Boolean = true @Volatile private var requestRendering: Boolean = false @Volatile private var requestRendering: Boolean = false private fun updateFrameTime() { frameTimeMillis = 1000.0 / fps.toDouble() } /** * If [fps] is non-zero, update the requested FPS and calculate the frame time * for the requested FPS. Otherwise disable continuous rendering (on demand rendering) * without changing the frame rate. * If [fps] is non-zero, update the requested FPS and calculate the frame time for the requested * FPS. Otherwise disable continuous rendering (on demand rendering) without changing the frame * rate. * * @param fps The requested FPS value. */ Loading Loading @@ -86,13 +83,11 @@ class FpsThrottler { } /** * Calculates whether we can render the next frame. In continuous mode return true only * if enough time has passed since the last render to maintain requested FPS. * In on demand mode, return true only if [requestRendering] was called to render * the next frame. * Calculates whether we can render the next frame. In continuous mode return true only if * enough time has passed since the last render to maintain requested FPS. In on demand mode, * return true only if [requestRendering] was called to render the next frame. * * @param frameTimeNanos The time in nanoseconds when the current frame started. * * @return true if we can render the next frame. */ fun canRender(frameTimeNanos: Long): Boolean { Loading @@ -102,7 +97,7 @@ class FpsThrottler { true } else { val deltaMillis = (frameTimeNanos - lastFrameTimeNanos) * NANO_TO_MILLIS return (deltaMillis >= frameTimeMillis) && (fps > 0f) return (deltaMillis >= frameTimeMillis - TOLERANCE_MILLIS) && (fps > 0f) } } else { // on demand rendering Loading @@ -120,7 +115,6 @@ class FpsThrottler { * @param frameTimeNanos The time in nanoseconds when the current frame started. * @param onRenderPermitted The client delegate to dispatch if rendering is permitted at this * time. * * @return true if a frame is permitted and then actually rendered. */ fun tryRender(frameTimeNanos: Long, onRenderPermitted: () -> Boolean): Boolean { Loading toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt +44 −48 Original line number Diff line number Diff line Loading @@ -31,9 +31,9 @@ import com.google.android.torus.core.wallpaper.LiveWallpaper import java.io.PrintWriter /** * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This * class also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls * related to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This class * also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls related * to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. * * By default it won't start [startUpdateLoop]. To run animations and update logic per frame, call * [startUpdateLoop] and [stopUpdateLoop] when it's no longer needed. Loading @@ -46,22 +46,22 @@ abstract class CanvasWallpaperEngine( /** * Defines if the surface should be hardware accelerated or not. If you are using * [RuntimeShader], this value should be set to true. When setting it to true, some * functions might not be supported. Please refer to the documentation: * [RuntimeShader], this value should be set to true. When setting it to true, some functions * might not be supported. Please refer to the documentation: * https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported */ private val hardwareAccelerated: Boolean = false, ) : LiveWallpaper.LiveWallpaperConnector(), TorusEngine { private val choreographer = Choreographer.getInstance() private val timeController = TimeController().also { it.resetDeltaTime(SystemClock.uptimeMillis()) } private val timeController = TimeController().also { it.resetDeltaTime(SystemClock.uptimeMillis()) } private val frameScheduler = FrameCallback() private val fpsThrottler = FpsThrottler() protected var screenSize = Size(0, 0) private set private var resizeCalled: Boolean = false private var isWallpaperEngineVisible = false Loading Loading @@ -110,8 +110,8 @@ abstract class CanvasWallpaperEngine( * update logic and render in this loop. * * @param deltaMillis The time in millis since the last time [onUpdate] was called. * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, * in the [System.nanoTime] timebase. * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the * [System.nanoTime] timebase. */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) open fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) { Loading @@ -130,9 +130,10 @@ abstract class CanvasWallpaperEngine( } final override fun create(isFirstActiveInstance: Boolean) { screenSize = Size( screenSize = Size( getCurrentSurfaceHolder().surfaceFrame.width(), getCurrentSurfaceHolder().surfaceFrame.height() getCurrentSurfaceHolder().surfaceFrame.height(), ) onCreate(isFirstActiveInstance) Loading @@ -141,8 +142,10 @@ abstract class CanvasWallpaperEngine( if (shouldInvokeResume) { Log.e( TAG, "Force invoke resume. onVisibilityChanged must have been called" + "before onCreate.") TAG, "Force invoke resume. onVisibilityChanged must have been called" + "before onCreate.", ) resume() shouldInvokeResume = false } Loading @@ -151,8 +154,10 @@ abstract class CanvasWallpaperEngine( final override fun pause() { if (!isCreated) { Log.e( TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + " resume on next create.") TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + " resume on next create.", ) shouldInvokeResume = true return } Loading @@ -166,8 +171,10 @@ abstract class CanvasWallpaperEngine( final override fun resume() { if (!isCreated) { Log.e( TAG, "Engine is not yet created but resume is called. Set a flag to " + "invoke resume on next create.") TAG, "Engine is not yet created but resume is called. Set a flag to " + "invoke resume on next create.", ) shouldInvokeResume = true return } Loading Loading @@ -200,7 +207,6 @@ abstract class CanvasWallpaperEngine( * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the * [System.nanoTime] timebase. * @param onRender The callback triggered when the canvas is ready for render. * * @return Whether it is rendered. */ fun renderWithFpsLimit(frameTimeNanos: Long, onRender: (canvas: Canvas) -> Unit): Boolean { Loading @@ -215,16 +221,13 @@ abstract class CanvasWallpaperEngine( return renderWithFpsLimit(frameTimeNanos, onRender) } return fpsThrottler.tryRender(frameTimeNanos) { renderToCanvas(onRender) } return fpsThrottler.tryRender(frameTimeNanos) { renderToCanvas(onRender) } } /** * Renders to canvas. * * @param onRender The callback triggered when the canvas is ready for render. * * @return Whether it is rendered. */ fun render(onRender: (canvas: Canvas) -> Unit): Boolean { Loading @@ -250,9 +253,7 @@ abstract class CanvasWallpaperEngine( fpsThrottler.updateFps(fps) } /** * Starts the update loop. */ /** Starts the update loop. */ protected fun startUpdateLoop() { if (!frameScheduler.running) { frameScheduler.running = true Loading @@ -260,9 +261,7 @@ abstract class CanvasWallpaperEngine( } } /** * Stops the update loop. */ /** Stops the update loop. */ protected fun stopUpdateLoop() { if (frameScheduler.running) { frameScheduler.running = false Loading @@ -276,14 +275,14 @@ abstract class CanvasWallpaperEngine( var canvas: Canvas? = null try { canvas = if (hardwareAccelerated) { canvas = if (hardwareAccelerated) { surfaceHolder.lockHardwareCanvas() } else { surfaceHolder.lockCanvas() } ?: return false onRender(canvas) } catch (e: java.lang.Exception) { Log.e("canvas_exception", "canvas exception", e) } finally { Loading @@ -294,12 +293,9 @@ abstract class CanvasWallpaperEngine( return true } private fun getCurrentSurfaceHolder(): SurfaceHolder = getEngineSurfaceHolder() ?: defaultHolder private fun getCurrentSurfaceHolder(): SurfaceHolder = getEngineSurfaceHolder() ?: defaultHolder /** * Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. */ /** Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. */ inner class FrameCallback : Choreographer.FrameCallback { internal var running: Boolean = false Loading Loading
mechanics/src/com/android/mechanics/spec/builder/MotionBuilderContext.kt +4 −0 Original line number Diff line number Diff line Loading @@ -31,6 +31,10 @@ import com.android.mechanics.spring.SpringParameters * Device / scheme specific context for building motion specs. * * See go/motion-system. * * @see rememberMotionBuilderContext for Compose * @see standardViewMotionBuilderContext for Views * @see expressiveViewMotionBuilderContext for Views */ interface MotionBuilderContext : Density { /** Loading
mechanics/src/com/android/mechanics/view/ViewMotionBuilderContext.kt 0 → 100644 +127 −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.mechanics.view import android.content.Context import androidx.compose.ui.unit.Density import com.android.mechanics.spec.builder.MaterialSprings import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spring.SpringParameters import com.android.mechanics.view.ViewMaterialSprings.Default /** * Creates a [MotionBuilderContext] using the **standard** motion spec. * * See go/motion-system. * * @param context The context to derive the density from. */ fun standardViewMotionBuilderContext(context: Context): MotionBuilderContext { return standardViewMotionBuilderContext(context.resources.displayMetrics.density) } /** * Creates a [MotionBuilderContext] using the **standard** motion spec. * * See go/motion-system. * * @param density The density of the display, as a scaling factor for the dp to px conversion. */ fun standardViewMotionBuilderContext(density: Float): MotionBuilderContext { return with(ViewMaterialSprings.Default) { ViewMotionBuilderContext(Spatial, Effects, Density(density)) } } /** * Creates a [MotionBuilderContext] using the **expressive** motion spec. * * See go/motion-system. * * @param context The context to derive the density from. */ fun expressiveViewMotionBuilderContext(context: Context): MotionBuilderContext { return expressiveViewMotionBuilderContext(context.resources.displayMetrics.density) } /** * Creates a [MotionBuilderContext] using the **expressive** motion spec. * * See go/motion-system. * * @param density The density of the display, as a scaling factor for the dp to px conversion. */ fun expressiveViewMotionBuilderContext(density: Float): MotionBuilderContext { return with(ViewMaterialSprings.Expressive) { ViewMotionBuilderContext(Spatial, Effects, Density(density)) } } /** * Material motion system spring definitions. * * See go/motion-system. * * NOTE: These are only defined here since material spring parameters are not available for View * based APIs. There might be a delay in updating these values, should the material tokens be * updated in the future. * * @see rememberMotionBuilderContext for Compose */ object ViewMaterialSprings { object Default { val Spatial = MaterialSprings( SpringParameters(700.0f, 0.9f), SpringParameters(1400.0f, 0.9f), SpringParameters(300.0f, 0.9f), MotionBuilderContext.StableThresholdSpatial, ) val Effects = MaterialSprings( SpringParameters(1600.0f, 1.0f), SpringParameters(3800.0f, 1.0f), SpringParameters(800.0f, 1.0f), MotionBuilderContext.StableThresholdEffects, ) } object Expressive { val Spatial = MaterialSprings( SpringParameters(380.0f, 0.8f), SpringParameters(800.0f, 0.6f), SpringParameters(200.0f, 0.8f), MotionBuilderContext.StableThresholdSpatial, ) val Effects = MaterialSprings( SpringParameters(1600.0f, 1.0f), SpringParameters(3800.0f, 1.0f), SpringParameters(800.0f, 1.0f), MotionBuilderContext.StableThresholdEffects, ) } } internal class ViewMotionBuilderContext( override val spatial: MaterialSprings, override val effects: MaterialSprings, density: Density, ) : MotionBuilderContext, Density by density
mechanics/tests/src/com/android/mechanics/view/ViewMotionBuilderContextTest.kt 0 → 100644 +77 −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. */ @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) package com.android.mechanics.view import android.content.Context import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.spec.builder.MotionBuilderContext import com.android.mechanics.spec.builder.rememberMotionBuilderContext import com.google.common.truth.Truth import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ViewMotionBuilderContextTest { @get:Rule(order = 0) val rule = createComposeRule() @Test fun materialSprings_standardScheme_matchesComposeDefinition() { lateinit var viewContext: Context lateinit var composeReference: MotionBuilderContext rule.setContent { viewContext = LocalContext.current MaterialTheme(motionScheme = MotionScheme.standard()) { composeReference = rememberMotionBuilderContext() } } val underTest = standardViewMotionBuilderContext(viewContext) Truth.assertThat(underTest.density).isEqualTo(composeReference.density) Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial) Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects) } @Test fun materialSprings_expressiveScheme_matchesComposeDefinition() { lateinit var viewContext: Context lateinit var composeReference: MotionBuilderContext rule.setContent { viewContext = LocalContext.current MaterialTheme(motionScheme = MotionScheme.expressive()) { composeReference = rememberMotionBuilderContext() } } val underTest = expressiveViewMotionBuilderContext(viewContext) Truth.assertThat(underTest.density).isEqualTo(composeReference.density) Truth.assertThat(underTest.spatial).isEqualTo(composeReference.spatial) Truth.assertThat(underTest.effects).isEqualTo(composeReference.effects) } }
toruslib/torus-core/src/main/java/com/google/android/torus/core/power/FpsThrottler.kt +19 −25 Original line number Diff line number Diff line Loading @@ -22,41 +22,38 @@ package com.google.android.torus.core.power */ class FpsThrottler { companion object { private const val NANO_TO_MILLIS = 1 / 1E6 const val NANO_TO_MILLIS = 1 / 1E6 const val FPS_120 = 120f const val FPS_60 = 60f const val FPS_30 = 30f const val FPS_18 = 18f @Deprecated(message = "Use FPS_60 instead.") const val HIGH_FPS = 60f @Deprecated(message = "Use FPS_30 instead.") const val MED_FPS = 30f @Deprecated(message = "Use FPS_18 instead.") const val LOW_FPS = 18f @Deprecated(message = "Use FPS_60 instead.") const val HIGH_FPS = 60f @Deprecated(message = "Use FPS_30 instead.") const val MED_FPS = 30f @Deprecated(message = "Use FPS_18 instead.") const val LOW_FPS = 18f /** Small tolerance (ms) for float precision in frame timing. */ const val TOLERANCE_MILLIS = 1L } private var fps: Float = FPS_60 @Volatile private var frameTimeMillis: Double = 1000.0 / fps.toDouble() @Volatile private var frameTimeMillis: Double = 1000.0 / fps.toDouble() private var lastFrameTimeNanos: Long = -1 @Volatile private var continuousRenderingMode: Boolean = true @Volatile private var continuousRenderingMode: Boolean = true @Volatile private var requestRendering: Boolean = false @Volatile private var requestRendering: Boolean = false private fun updateFrameTime() { frameTimeMillis = 1000.0 / fps.toDouble() } /** * If [fps] is non-zero, update the requested FPS and calculate the frame time * for the requested FPS. Otherwise disable continuous rendering (on demand rendering) * without changing the frame rate. * If [fps] is non-zero, update the requested FPS and calculate the frame time for the requested * FPS. Otherwise disable continuous rendering (on demand rendering) without changing the frame * rate. * * @param fps The requested FPS value. */ Loading Loading @@ -86,13 +83,11 @@ class FpsThrottler { } /** * Calculates whether we can render the next frame. In continuous mode return true only * if enough time has passed since the last render to maintain requested FPS. * In on demand mode, return true only if [requestRendering] was called to render * the next frame. * Calculates whether we can render the next frame. In continuous mode return true only if * enough time has passed since the last render to maintain requested FPS. In on demand mode, * return true only if [requestRendering] was called to render the next frame. * * @param frameTimeNanos The time in nanoseconds when the current frame started. * * @return true if we can render the next frame. */ fun canRender(frameTimeNanos: Long): Boolean { Loading @@ -102,7 +97,7 @@ class FpsThrottler { true } else { val deltaMillis = (frameTimeNanos - lastFrameTimeNanos) * NANO_TO_MILLIS return (deltaMillis >= frameTimeMillis) && (fps > 0f) return (deltaMillis >= frameTimeMillis - TOLERANCE_MILLIS) && (fps > 0f) } } else { // on demand rendering Loading @@ -120,7 +115,6 @@ class FpsThrottler { * @param frameTimeNanos The time in nanoseconds when the current frame started. * @param onRenderPermitted The client delegate to dispatch if rendering is permitted at this * time. * * @return true if a frame is permitted and then actually rendered. */ fun tryRender(frameTimeNanos: Long, onRenderPermitted: () -> Boolean): Boolean { Loading
toruslib/torus-framework-canvas/src/main/java/com/google/android/torus/canvas/engine/CanvasWallpaperEngine.kt +44 −48 Original line number Diff line number Diff line Loading @@ -31,9 +31,9 @@ import com.google.android.torus.core.wallpaper.LiveWallpaper import java.io.PrintWriter /** * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This * class also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls * related to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. * Class that implements [TorusEngine] using Canvas and can be used in a [LiveWallpaper]. This class * also inherits from [LiveWallpaper.LiveWallpaperConnector] which allows to do some calls related * to Live Wallpapers, like the method [isPreview] or [notifyWallpaperColorsChanged]. * * By default it won't start [startUpdateLoop]. To run animations and update logic per frame, call * [startUpdateLoop] and [stopUpdateLoop] when it's no longer needed. Loading @@ -46,22 +46,22 @@ abstract class CanvasWallpaperEngine( /** * Defines if the surface should be hardware accelerated or not. If you are using * [RuntimeShader], this value should be set to true. When setting it to true, some * functions might not be supported. Please refer to the documentation: * [RuntimeShader], this value should be set to true. When setting it to true, some functions * might not be supported. Please refer to the documentation: * https://developer.android.com/guide/topics/graphics/hardware-accel#unsupported */ private val hardwareAccelerated: Boolean = false, ) : LiveWallpaper.LiveWallpaperConnector(), TorusEngine { private val choreographer = Choreographer.getInstance() private val timeController = TimeController().also { it.resetDeltaTime(SystemClock.uptimeMillis()) } private val timeController = TimeController().also { it.resetDeltaTime(SystemClock.uptimeMillis()) } private val frameScheduler = FrameCallback() private val fpsThrottler = FpsThrottler() protected var screenSize = Size(0, 0) private set private var resizeCalled: Boolean = false private var isWallpaperEngineVisible = false Loading Loading @@ -110,8 +110,8 @@ abstract class CanvasWallpaperEngine( * update logic and render in this loop. * * @param deltaMillis The time in millis since the last time [onUpdate] was called. * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, * in the [System.nanoTime] timebase. * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the * [System.nanoTime] timebase. */ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) open fun onUpdate(deltaMillis: Long, frameTimeNanos: Long) { Loading @@ -130,9 +130,10 @@ abstract class CanvasWallpaperEngine( } final override fun create(isFirstActiveInstance: Boolean) { screenSize = Size( screenSize = Size( getCurrentSurfaceHolder().surfaceFrame.width(), getCurrentSurfaceHolder().surfaceFrame.height() getCurrentSurfaceHolder().surfaceFrame.height(), ) onCreate(isFirstActiveInstance) Loading @@ -141,8 +142,10 @@ abstract class CanvasWallpaperEngine( if (shouldInvokeResume) { Log.e( TAG, "Force invoke resume. onVisibilityChanged must have been called" + "before onCreate.") TAG, "Force invoke resume. onVisibilityChanged must have been called" + "before onCreate.", ) resume() shouldInvokeResume = false } Loading @@ -151,8 +154,10 @@ abstract class CanvasWallpaperEngine( final override fun pause() { if (!isCreated) { Log.e( TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + " resume on next create.") TAG, "Engine is not yet created but pause is called. Set a flag to invoke" + " resume on next create.", ) shouldInvokeResume = true return } Loading @@ -166,8 +171,10 @@ abstract class CanvasWallpaperEngine( final override fun resume() { if (!isCreated) { Log.e( TAG, "Engine is not yet created but resume is called. Set a flag to " + "invoke resume on next create.") TAG, "Engine is not yet created but resume is called. Set a flag to " + "invoke resume on next create.", ) shouldInvokeResume = true return } Loading Loading @@ -200,7 +207,6 @@ abstract class CanvasWallpaperEngine( * @param frameTimeNanos The time in nanoseconds when the frame started being rendered, in the * [System.nanoTime] timebase. * @param onRender The callback triggered when the canvas is ready for render. * * @return Whether it is rendered. */ fun renderWithFpsLimit(frameTimeNanos: Long, onRender: (canvas: Canvas) -> Unit): Boolean { Loading @@ -215,16 +221,13 @@ abstract class CanvasWallpaperEngine( return renderWithFpsLimit(frameTimeNanos, onRender) } return fpsThrottler.tryRender(frameTimeNanos) { renderToCanvas(onRender) } return fpsThrottler.tryRender(frameTimeNanos) { renderToCanvas(onRender) } } /** * Renders to canvas. * * @param onRender The callback triggered when the canvas is ready for render. * * @return Whether it is rendered. */ fun render(onRender: (canvas: Canvas) -> Unit): Boolean { Loading @@ -250,9 +253,7 @@ abstract class CanvasWallpaperEngine( fpsThrottler.updateFps(fps) } /** * Starts the update loop. */ /** Starts the update loop. */ protected fun startUpdateLoop() { if (!frameScheduler.running) { frameScheduler.running = true Loading @@ -260,9 +261,7 @@ abstract class CanvasWallpaperEngine( } } /** * Stops the update loop. */ /** Stops the update loop. */ protected fun stopUpdateLoop() { if (frameScheduler.running) { frameScheduler.running = false Loading @@ -276,14 +275,14 @@ abstract class CanvasWallpaperEngine( var canvas: Canvas? = null try { canvas = if (hardwareAccelerated) { canvas = if (hardwareAccelerated) { surfaceHolder.lockHardwareCanvas() } else { surfaceHolder.lockCanvas() } ?: return false onRender(canvas) } catch (e: java.lang.Exception) { Log.e("canvas_exception", "canvas exception", e) } finally { Loading @@ -294,12 +293,9 @@ abstract class CanvasWallpaperEngine( return true } private fun getCurrentSurfaceHolder(): SurfaceHolder = getEngineSurfaceHolder() ?: defaultHolder private fun getCurrentSurfaceHolder(): SurfaceHolder = getEngineSurfaceHolder() ?: defaultHolder /** * Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. */ /** Implementation of [Choreographer.FrameCallback] which triggers [onUpdate]. */ inner class FrameCallback : Choreographer.FrameCallback { internal var running: Boolean = false Loading