diff --git a/src/components/views/elements/ProgressBar.tsx b/src/components/views/elements/ProgressBar.tsx index c4ff06e04e4..af06f579ead 100644 --- a/src/components/views/elements/ProgressBar.tsx +++ b/src/components/views/elements/ProgressBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020,2022 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,13 +16,20 @@ limitations under the License. import React from "react"; +import { useSmoothAnimation } from "../../../hooks/useSmoothAnimation"; + interface IProps { value: number; max: number; + animated?: boolean; } -const ProgressBar: React.FC = ({ value, max }) => { - return ; +const PROGRESS_BAR_ANIMATION_DURATION = 300; +const ProgressBar: React.FC = ({ value, max, animated }) => { + // Animating progress bars via CSS transition isn’t possible in all of our supported browsers yet. + // As workaround, we’re using animations through JS requestAnimationFrame + const currentValue = useSmoothAnimation(0, value, PROGRESS_BAR_ANIMATION_DURATION, animated); + return ; }; export default ProgressBar; diff --git a/src/hooks/useAnimation.ts b/src/hooks/useAnimation.ts new file mode 100644 index 00000000000..c728d577049 --- /dev/null +++ b/src/hooks/useAnimation.ts @@ -0,0 +1,55 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import { useCallback, useEffect, useRef } from "react"; + +import SettingsStore from "../settings/SettingsStore"; + +const debuglog = (...args: any[]) => { + if (SettingsStore.getValue("debug_animation")) { + logger.log.call(console, "Animation debuglog:", ...args); + } +}; + +export function useAnimation(enabled: boolean, callback: (timestamp: DOMHighResTimeStamp) => boolean) { + const handle = useRef(null); + + const handler = useCallback( + (timestamp: DOMHighResTimeStamp) => { + if (callback(timestamp)) { + handle.current = requestAnimationFrame(handler); + } else { + debuglog("Finished animation!"); + } + }, + [callback], + ); + + useEffect(() => { + debuglog("Started animation!"); + if (enabled) { + handle.current = requestAnimationFrame(handler); + } + return () => { + if (handle.current) { + debuglog("Aborted animation!"); + cancelAnimationFrame(handle.current); + handle.current = null; + } + }; + }, [enabled, handler]); +} diff --git a/src/hooks/useSmoothAnimation.ts b/src/hooks/useSmoothAnimation.ts new file mode 100644 index 00000000000..8d652f32579 --- /dev/null +++ b/src/hooks/useSmoothAnimation.ts @@ -0,0 +1,85 @@ +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +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. +*/ + +import { logger } from "matrix-js-sdk/src/logger"; +import { useCallback, useEffect, useRef, useState } from "react"; + +import SettingsStore from "../settings/SettingsStore"; +import { useAnimation } from "./useAnimation"; + +const debuglog = (...args: any[]) => { + if (SettingsStore.getValue("debug_animation")) { + logger.log.call(console, "Animation debuglog:", ...args); + } +}; + +/** + * Utility function to smoothly animate to a certain target value + * @param initialValue Initial value to be used as initial starting point + * @param targetValue Desired value to animate to (can be changed repeatedly to whatever is current at that time) + * @param duration Duration that each animation should take + * @param enabled Whether the animation should run or not + */ +export function useSmoothAnimation( + initialValue: number, + targetValue: number, + duration: number, + enabled: boolean, +): number { + const state = useRef<{ timestamp: DOMHighResTimeStamp | null, value: number }>({ + timestamp: null, + value: initialValue, + }); + const [currentValue, setCurrentValue] = useState(initialValue); + const [currentStepSize, setCurrentStepSize] = useState(0); + + useEffect(() => { + const totalDelta = targetValue - state.current.value; + setCurrentStepSize(totalDelta / duration); + state.current = { ...state.current, timestamp: null }; + }, [duration, targetValue]); + + const update = useCallback( + (timestamp: DOMHighResTimeStamp): boolean => { + if (!state.current.timestamp) { + state.current = { ...state.current, timestamp }; + return true; + } + + if (Math.abs(currentStepSize) < Number.EPSILON) { + return false; + } + + const timeDelta = timestamp - state.current.timestamp; + const valueDelta = currentStepSize * timeDelta; + const maxValueDelta = targetValue - state.current.value; + const clampedValueDelta = Math.sign(valueDelta) * Math.min(Math.abs(maxValueDelta), Math.abs(valueDelta)); + const value = state.current.value + clampedValueDelta; + + debuglog(`Animating to ${targetValue} at ${value} timeDelta=${timeDelta}, valueDelta=${valueDelta}`); + + setCurrentValue(value); + state.current = { value, timestamp }; + + return Math.abs(maxValueDelta) > Number.EPSILON; + }, + [currentStepSize, targetValue], + ); + + useAnimation(enabled, update); + + return currentValue; +} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index b9871b3bbdb..44d82da5cd4 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -952,6 +952,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: false, }, + "debug_animation": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, + default: false, + }, "audioInputMuted": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: false,