Loading quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +7 −6 Original line number Diff line number Diff line Loading @@ -77,6 +77,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.folder.Folder; Loading Loading @@ -1004,7 +1005,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Toast.LENGTH_SHORT).show(); } else { // Else launch the selected app pair launchFromTaskbarPreservingSplitIfVisible(recents, fi.contents); launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents); mControllers.uiController.onTaskbarIconLaunched(fi); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } Loading Loading @@ -1040,7 +1041,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { .startShortcut(packageName, id, null, null, info.user); } else { launchFromTaskbarPreservingSplitIfVisible( recents, Collections.singletonList(info)); recents, view, Collections.singletonList(info)); } } catch (NullPointerException Loading Loading @@ -1078,7 +1079,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // If we are selecting a second app for split, launch the split tasks taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { launchFromTaskbarPreservingSplitIfVisible(recents, Collections.singletonList(info)); launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); } mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); Loading @@ -1100,7 +1102,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * (potentially breaking a split pair). */ private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents, List<? extends ItemInfo> itemInfos) { @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; } Loading Loading @@ -1128,8 +1130,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { if (findExactPairMatch) { // We did not find the app pair we were looking for, so launch one. recents.getSplitSelectController().getAppPairsController().launchAppPair( (WorkspaceItemInfo) itemInfos.get(0), (WorkspaceItemInfo) itemInfos.get(1)); (AppPairIcon) launchingIconView); } else { startItemInfoActivity(itemInfos.get(0)); } Loading quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +3 −3 Original line number Diff line number Diff line Loading @@ -106,6 +106,7 @@ import com.android.launcher3.Workspace; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.appprediction.PredictionRowView; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.desktop.DesktopRecentsTransitionController; Loading @@ -116,7 +117,6 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.proxy.ProxyActivityStarter; import com.android.launcher3.statehandlers.DepthController; Loading Loading @@ -1284,8 +1284,8 @@ public class QuickstepLauncher extends Launcher { /** * Launches two apps as an app pair. */ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) { mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2); public void launchAppPair(AppPairIcon appPairIcon) { mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon); } public boolean canStartHomeSafely() { Loading quickstep/src/com/android/quickstep/TaskViewUtils.java +18 −92 Original line number Diff line number Diff line Loading @@ -18,8 +18,6 @@ package com.android.quickstep; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.app.animation.Interpolators.TOUCH_RESPONSE; Loading Loading @@ -421,15 +419,11 @@ public final class TaskViewUtils { * Technically this case should be taken care of by * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether * it's a single task or multiple tasks results in different entry-points. * * If it is null, then it will simply fade in the starting apps and fade out launcher (for the * case where launcher handles animating starting split tasks from app icon) */ public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView, @NonNull StateManager stateManager, @Nullable DepthController depthController, int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { if (launchingTaskView != null) { @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override Loading @@ -447,80 +441,12 @@ public final class TaskViewUtils { RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, false /* wallpapers */, t, null /* leashMap */); final RecentsView recentsView = launchingTaskView.getRecentsView(); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, true, stateManager, recentsView, depthController); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, /* launcherClosing */ true, stateManager, recentsView, depthController); t.apply(); animatorSet.start(); return; } TransitionInfo.Change splitRoot1 = null; TransitionInfo.Change splitRoot2 = null; final ArrayList<SurfaceControl> openingTargets = new ArrayList<>(); for (int i = 0; i < transitionInfo.getChanges().size(); ++i) { final TransitionInfo.Change change = transitionInfo.getChanges().get(i); if (change.getTaskInfo() == null) { continue; } final int taskId = change.getTaskInfo().taskId; final int mode = change.getMode(); // Find the target tasks' root tasks since those are the split stages that need to // be animated (the tasks themselves are children and thus inherit animation). if (taskId == initialTaskId || taskId == secondTaskId) { if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { throw new IllegalStateException( "Expected task to be showing, but it is " + mode); } } if (taskId == initialTaskId) { splitRoot1 = change.getParent() == null ? change : transitionInfo.getChange(change.getParent()); openingTargets.add(splitRoot1.getLeash()); } if (taskId == secondTaskId) { splitRoot2 = change.getParent() == null ? change : transitionInfo.getChange(change.getParent()); openingTargets.add(splitRoot2.getLeash()); } } SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction(); ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(SPLIT_LAUNCH_DURATION); animator.addUpdateListener(valueAnimator -> { float progress = valueAnimator.getAnimatedFraction(); for (SurfaceControl leash: openingTargets) { animTransaction.setAlpha(leash, progress); } animTransaction.apply(); }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { for (SurfaceControl leash: openingTargets) { animTransaction.show(leash) .setAlpha(leash, 0.0f); } animTransaction.apply(); } @Override public void onAnimationEnd(Animator animation) { finishCallback.run(); } }); if (splitRoot1 != null && splitRoot1.getParent() != null) { // Set the highest level split root alpha; we could technically use the parent of either // splitRoot1 or splitRoot2 t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f); } t.apply(); animator.start(); } /** Loading quickstep/src/com/android/quickstep/util/AnimUtils.java +9 −0 Original line number Diff line number Diff line Loading @@ -39,4 +39,13 @@ public class AnimUtils { ? SplitAnimationTimings.TABLET_SPLIT_TO_CONFIRM : SplitAnimationTimings.PHONE_SPLIT_TO_CONFIRM; } /** * Fetches device-specific timings for the app pair launch animation. */ public static SplitAnimationTimings getDeviceAppPairLaunchTimings(boolean isTablet) { return isTablet ? SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH; } } quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt 0 → 100644 +72 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.quickstep.util import com.android.app.animation.Interpolators /** Timings for the app pair launch animation. */ abstract class AppPairLaunchTimings : SplitAnimationTimings { protected abstract val STAGED_RECT_SLIDE_DURATION: Int // Common timings that apply to app pair launches on any type of device override fun getStagedRectSlideStart() = 0 override fun getStagedRectSlideEnd() = stagedRectSlideStart + STAGED_RECT_SLIDE_DURATION override fun getPlaceholderFadeInStart() = 0 override fun getPlaceholderFadeInEnd() = 0 override fun getPlaceholderIconFadeInStart() = 0 override fun getPlaceholderIconFadeInEnd() = 0 private val iconFadeStart: Int get() = getStagedRectSlideEnd() private val iconFadeEnd: Int get() = iconFadeStart + 83 private val appRevealStart: Int get() = getStagedRectSlideEnd() + 67 private val appRevealEnd: Int get() = appRevealStart + 217 private val cellSplitStart: Int get() = (getStagedRectSlideEnd() * 0.83f).toInt() private val cellSplitEnd: Int get() = cellSplitStart + 500 override fun getStagedRectXInterpolator() = Interpolators.EMPHASIZED_COMPLEMENT override fun getStagedRectYInterpolator() = Interpolators.EMPHASIZED override fun getStagedRectScaleXInterpolator() = Interpolators.EMPHASIZED override fun getStagedRectScaleYInterpolator() = Interpolators.EMPHASIZED override fun getCellSplitInterpolator() = Interpolators.EMPHASIZED override fun getIconFadeInterpolator() = Interpolators.LINEAR override fun getCellSplitStartOffset(): Float { return cellSplitStart.toFloat() / getDuration() } override fun getCellSplitEndOffset(): Float { return cellSplitEnd.toFloat() / getDuration() } override fun getIconFadeStartOffset(): Float { return iconFadeStart.toFloat() / getDuration() } override fun getIconFadeEndOffset(): Float { return iconFadeEnd.toFloat() / getDuration() } override fun getAppRevealStartOffset(): Float { return appRevealStart.toFloat() / getDuration() } override fun getAppRevealEndOffset(): Float { return appRevealEnd.toFloat() / getDuration() } abstract override fun getDuration(): Int } Loading
quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +7 −6 Original line number Diff line number Diff line Loading @@ -77,6 +77,7 @@ import com.android.launcher3.DeviceProfile; import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.R; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dot.DotInfo; import com.android.launcher3.folder.Folder; Loading Loading @@ -1004,7 +1005,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { Toast.LENGTH_SHORT).show(); } else { // Else launch the selected app pair launchFromTaskbarPreservingSplitIfVisible(recents, fi.contents); launchFromTaskbarPreservingSplitIfVisible(recents, view, fi.contents); mControllers.uiController.onTaskbarIconLaunched(fi); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); } Loading Loading @@ -1040,7 +1041,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { .startShortcut(packageName, id, null, null, info.user); } else { launchFromTaskbarPreservingSplitIfVisible( recents, Collections.singletonList(info)); recents, view, Collections.singletonList(info)); } } catch (NullPointerException Loading Loading @@ -1078,7 +1079,8 @@ public class TaskbarActivityContext extends BaseTaskbarContext { // If we are selecting a second app for split, launch the split tasks taskbarUIController.triggerSecondAppForSplit(info, info.intent, view); } else { launchFromTaskbarPreservingSplitIfVisible(recents, Collections.singletonList(info)); launchFromTaskbarPreservingSplitIfVisible( recents, view, Collections.singletonList(info)); } mControllers.uiController.onTaskbarIconLaunched(info); mControllers.taskbarStashController.updateAndAnimateTransientTaskbar(true); Loading @@ -1100,7 +1102,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { * (potentially breaking a split pair). */ private void launchFromTaskbarPreservingSplitIfVisible(@Nullable RecentsView recents, List<? extends ItemInfo> itemInfos) { @Nullable View launchingIconView, List<? extends ItemInfo> itemInfos) { if (recents == null) { return; } Loading Loading @@ -1128,8 +1130,7 @@ public class TaskbarActivityContext extends BaseTaskbarContext { if (findExactPairMatch) { // We did not find the app pair we were looking for, so launch one. recents.getSplitSelectController().getAppPairsController().launchAppPair( (WorkspaceItemInfo) itemInfos.get(0), (WorkspaceItemInfo) itemInfos.get(1)); (AppPairIcon) launchingIconView); } else { startItemInfoActivity(itemInfos.get(0)); } Loading
quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java +3 −3 Original line number Diff line number Diff line Loading @@ -106,6 +106,7 @@ import com.android.launcher3.Workspace; import com.android.launcher3.accessibility.LauncherAccessibilityDelegate; import com.android.launcher3.anim.AnimatorPlaybackController; import com.android.launcher3.anim.PendingAnimation; import com.android.launcher3.apppairs.AppPairIcon; import com.android.launcher3.appprediction.PredictionRowView; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.desktop.DesktopRecentsTransitionController; Loading @@ -116,7 +117,6 @@ import com.android.launcher3.logging.StatsLogManager.StatsLogger; import com.android.launcher3.model.BgDataModel.FixedContainerItems; import com.android.launcher3.model.WellbeingModel; import com.android.launcher3.model.data.ItemInfo; import com.android.launcher3.model.data.WorkspaceItemInfo; import com.android.launcher3.popup.SystemShortcut; import com.android.launcher3.proxy.ProxyActivityStarter; import com.android.launcher3.statehandlers.DepthController; Loading Loading @@ -1284,8 +1284,8 @@ public class QuickstepLauncher extends Launcher { /** * Launches two apps as an app pair. */ public void launchAppPair(WorkspaceItemInfo app1, WorkspaceItemInfo app2) { mSplitSelectStateController.getAppPairsController().launchAppPair(app1, app2); public void launchAppPair(AppPairIcon appPairIcon) { mSplitSelectStateController.getAppPairsController().launchAppPair(appPairIcon); } public boolean canStartHomeSafely() { Loading
quickstep/src/com/android/quickstep/TaskViewUtils.java +18 −92 Original line number Diff line number Diff line Loading @@ -18,8 +18,6 @@ package com.android.quickstep; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.app.animation.Interpolators.LINEAR; import static com.android.app.animation.Interpolators.TOUCH_RESPONSE; Loading Loading @@ -421,15 +419,11 @@ public final class TaskViewUtils { * Technically this case should be taken care of by * {@link #composeRecentsSplitLaunchAnimatorLegacy} below, but the way we launch tasks whether * it's a single task or multiple tasks results in different entry-points. * * If it is null, then it will simply fade in the starting apps and fade out launcher (for the * case where launcher handles animating starting split tasks from app icon) */ public static void composeRecentsSplitLaunchAnimator(GroupedTaskView launchingTaskView, @NonNull StateManager stateManager, @Nullable DepthController depthController, int initialTaskId, int secondTaskId, @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { if (launchingTaskView != null) { @NonNull TransitionInfo transitionInfo, SurfaceControl.Transaction t, @NonNull Runnable finishCallback) { AnimatorSet animatorSet = new AnimatorSet(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override Loading @@ -447,80 +441,12 @@ public final class TaskViewUtils { RemoteAnimationTargetCompat.wrapNonApps( transitionInfo, false /* wallpapers */, t, null /* leashMap */); final RecentsView recentsView = launchingTaskView.getRecentsView(); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, true, stateManager, recentsView, depthController); composeRecentsLaunchAnimator(animatorSet, launchingTaskView, appTargets, wallpaperTargets, nonAppTargets, /* launcherClosing */ true, stateManager, recentsView, depthController); t.apply(); animatorSet.start(); return; } TransitionInfo.Change splitRoot1 = null; TransitionInfo.Change splitRoot2 = null; final ArrayList<SurfaceControl> openingTargets = new ArrayList<>(); for (int i = 0; i < transitionInfo.getChanges().size(); ++i) { final TransitionInfo.Change change = transitionInfo.getChanges().get(i); if (change.getTaskInfo() == null) { continue; } final int taskId = change.getTaskInfo().taskId; final int mode = change.getMode(); // Find the target tasks' root tasks since those are the split stages that need to // be animated (the tasks themselves are children and thus inherit animation). if (taskId == initialTaskId || taskId == secondTaskId) { if (!(mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) { throw new IllegalStateException( "Expected task to be showing, but it is " + mode); } } if (taskId == initialTaskId) { splitRoot1 = change.getParent() == null ? change : transitionInfo.getChange(change.getParent()); openingTargets.add(splitRoot1.getLeash()); } if (taskId == secondTaskId) { splitRoot2 = change.getParent() == null ? change : transitionInfo.getChange(change.getParent()); openingTargets.add(splitRoot2.getLeash()); } } SurfaceControl.Transaction animTransaction = new SurfaceControl.Transaction(); ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(SPLIT_LAUNCH_DURATION); animator.addUpdateListener(valueAnimator -> { float progress = valueAnimator.getAnimatedFraction(); for (SurfaceControl leash: openingTargets) { animTransaction.setAlpha(leash, progress); } animTransaction.apply(); }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { for (SurfaceControl leash: openingTargets) { animTransaction.show(leash) .setAlpha(leash, 0.0f); } animTransaction.apply(); } @Override public void onAnimationEnd(Animator animation) { finishCallback.run(); } }); if (splitRoot1 != null && splitRoot1.getParent() != null) { // Set the highest level split root alpha; we could technically use the parent of either // splitRoot1 or splitRoot2 t.setAlpha(transitionInfo.getChange(splitRoot1.getParent()).getLeash(), 1f); } t.apply(); animator.start(); } /** Loading
quickstep/src/com/android/quickstep/util/AnimUtils.java +9 −0 Original line number Diff line number Diff line Loading @@ -39,4 +39,13 @@ public class AnimUtils { ? SplitAnimationTimings.TABLET_SPLIT_TO_CONFIRM : SplitAnimationTimings.PHONE_SPLIT_TO_CONFIRM; } /** * Fetches device-specific timings for the app pair launch animation. */ public static SplitAnimationTimings getDeviceAppPairLaunchTimings(boolean isTablet) { return isTablet ? SplitAnimationTimings.TABLET_APP_PAIR_LAUNCH : SplitAnimationTimings.PHONE_APP_PAIR_LAUNCH; } }
quickstep/src/com/android/quickstep/util/AppPairLaunchTimings.kt 0 → 100644 +72 −0 Original line number Diff line number Diff line /* * Copyright (C) 2023 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.quickstep.util import com.android.app.animation.Interpolators /** Timings for the app pair launch animation. */ abstract class AppPairLaunchTimings : SplitAnimationTimings { protected abstract val STAGED_RECT_SLIDE_DURATION: Int // Common timings that apply to app pair launches on any type of device override fun getStagedRectSlideStart() = 0 override fun getStagedRectSlideEnd() = stagedRectSlideStart + STAGED_RECT_SLIDE_DURATION override fun getPlaceholderFadeInStart() = 0 override fun getPlaceholderFadeInEnd() = 0 override fun getPlaceholderIconFadeInStart() = 0 override fun getPlaceholderIconFadeInEnd() = 0 private val iconFadeStart: Int get() = getStagedRectSlideEnd() private val iconFadeEnd: Int get() = iconFadeStart + 83 private val appRevealStart: Int get() = getStagedRectSlideEnd() + 67 private val appRevealEnd: Int get() = appRevealStart + 217 private val cellSplitStart: Int get() = (getStagedRectSlideEnd() * 0.83f).toInt() private val cellSplitEnd: Int get() = cellSplitStart + 500 override fun getStagedRectXInterpolator() = Interpolators.EMPHASIZED_COMPLEMENT override fun getStagedRectYInterpolator() = Interpolators.EMPHASIZED override fun getStagedRectScaleXInterpolator() = Interpolators.EMPHASIZED override fun getStagedRectScaleYInterpolator() = Interpolators.EMPHASIZED override fun getCellSplitInterpolator() = Interpolators.EMPHASIZED override fun getIconFadeInterpolator() = Interpolators.LINEAR override fun getCellSplitStartOffset(): Float { return cellSplitStart.toFloat() / getDuration() } override fun getCellSplitEndOffset(): Float { return cellSplitEnd.toFloat() / getDuration() } override fun getIconFadeStartOffset(): Float { return iconFadeStart.toFloat() / getDuration() } override fun getIconFadeEndOffset(): Float { return iconFadeEnd.toFloat() / getDuration() } override fun getAppRevealStartOffset(): Float { return appRevealStart.toFloat() / getDuration() } override fun getAppRevealEndOffset(): Float { return appRevealEnd.toFloat() / getDuration() } abstract override fun getDuration(): Int }