Loading res/layout/floating_sheet_app_icon.xml +78 −78 Original line number Diff line number Diff line Loading @@ -21,13 +21,12 @@ android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding" android:orientation="vertical"> <LinearLayout <FrameLayout android:id="@+id/floating_sheet_content_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="@dimen/floating_sheet_content_vertical_padding" android:background="@drawable/floating_sheet_content_background" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> Loading @@ -35,10 +34,8 @@ android:id="@+id/app_icon_style_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:clipToPadding="false" android:clipChildren="false" android:visibility="gone"> android:clipChildren="false"> <!-- This is just an invisible placeholder put in place so that the parent keeps its height Loading @@ -51,8 +48,6 @@ --> <include layout="@layout/icon_style_option2" android:layout_width="64dp" android:layout_height="64dp" android:visibility="invisible" /> <androidx.recyclerview.widget.RecyclerView Loading @@ -64,11 +59,17 @@ android:clipChildren="false" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> <FrameLayout android:id="@+id/app_shape_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:clipToPadding="false" android:clipChildren="false"> Loading @@ -83,8 +84,6 @@ --> <include layout="@layout/shape_option2" android:layout_width="64dp" android:layout_height="64dp" android:visibility="invisible" /> <androidx.recyclerview.widget.RecyclerView Loading Loading @@ -146,6 +145,7 @@ android:theme="@style/Theme.Material3.DynamicColors.DayNight" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> </FrameLayout> <com.android.wallpaper.picker.customization.ui.view.FloatingToolbar android:id="@+id/floating_toolbar" Loading src/com/android/wallpaper/customization/ui/binder/AppIconFloatingSheetBinder.kt +93 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.ColorStateList import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ImageView import android.widget.TextView import androidx.core.graphics.drawable.DrawableCompat Loading @@ -38,7 +39,7 @@ import com.android.themepicker.R import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_ICONS import com.android.wallpaper.customization.ui.view.ShapeTileDrawable import com.android.wallpaper.customization.ui.viewmodel.AppIconPickerViewModel import com.android.wallpaper.customization.ui.viewmodel.AppIconPickerViewModel.Tab import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar Loading @@ -48,6 +49,8 @@ import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2 import com.google.android.material.materialswitch.MaterialSwitch import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch object AppIconFloatingSheetBinder { Loading Loading @@ -137,6 +140,53 @@ object AppIconFloatingSheetBinder { val themedIconTitle = view.requireViewById<TextView>(R.id.themed_icon_toggle_title) val themedIconBetaLabel = view.requireViewById<TextView>(R.id.themed_icon_beta_title) data class FloatingSheetHeightsViewModel( val styleContentHeight: Int? = null, val shapeContentHeight: Int? = null, ) val floatingSheetHeights: MutableStateFlow<FloatingSheetHeightsViewModel> = MutableStateFlow(FloatingSheetHeightsViewModel()) if (isExtendibleThemeManager) { styleContent.viewTreeObserver.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { if ( styleContent.height != 0 && floatingSheetHeights.value.styleContentHeight != styleContent.height ) { floatingSheetHeights.value = floatingSheetHeights.value.copy( styleContentHeight = styleContent.height ) // Keep the height of the style floating sheet fixed so text renders // correctly after changing tabs. styleContent.layoutParams = styleContent.layoutParams.apply { height = styleContent.height } styleContent.viewTreeObserver.removeOnGlobalLayoutListener(this) } } } ) shapeContent.viewTreeObserver.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { if ( shapeContent.height != 0 && floatingSheetHeights.value.shapeContentHeight != shapeContent.height ) { floatingSheetHeights.value = floatingSheetHeights.value.copy( shapeContentHeight = shapeContent.height ) shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this) } } } ) } lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { Loading Loading @@ -176,13 +226,51 @@ object AppIconFloatingSheetBinder { } launch { viewModel.selectedTab.collect { // TODO (b/397782741): add animation when switching tabs styleContent.isVisible = (it == AppIconPickerViewModel.Tab.STYLE) shapeContent.isVisible = (it == AppIconPickerViewModel.Tab.SHAPE) val verticalPadding = view.resources.getDimensionPixelSize( R.dimen.floating_sheet_content_vertical_padding ) var currentTab: Tab? = null combine(floatingSheetHeights, viewModel.selectedTab, ::Pair).collect { (heights, selectedTab) -> val (styleContentHeight, shapeContentHeight) = heights styleContentHeight ?: return@collect shapeContentHeight ?: return@collect selectedTab ?: return@collect styleContent.isVisible = (currentTab == Tab.STYLE) shapeContent.isVisible = (currentTab == Tab.SHAPE) val fromHeight = floatingSheetContainer.height val toHeight = when (selectedTab) { Tab.STYLE -> styleContentHeight Tab.SHAPE -> shapeContentHeight } + 2 * verticalPadding val currentContent: View? = when (currentTab) { Tab.STYLE -> styleContent Tab.SHAPE -> shapeContent else -> null } val selectedContent: View = when (selectedTab) { Tab.STYLE -> styleContent Tab.SHAPE -> shapeContent } FloatingSheetHeightAnimationBinder.bind( floatingSheetContainer, fromHeight, toHeight, currentContent, selectedContent, ) currentTab = selectedTab } } } else { styleContent.isVisible = false launch { viewModel.isShapeOptionsAvailable.collect { shapeAvailable -> shapeContent.isVisible = shapeAvailable Loading src/com/android/wallpaper/customization/ui/binder/ClockFloatingSheetBinder.kt +19 −39 Original line number Diff line number Diff line Loading @@ -16,9 +16,6 @@ package com.android.wallpaper.customization.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.view.View import android.view.ViewGroup Loading Loading @@ -76,7 +73,6 @@ import kotlinx.coroutines.launch object ClockFloatingSheetBinder { private const val SLIDER_ENABLED_ALPHA = 1f private const val SLIDER_DISABLED_ALPHA = .3f private const val ANIMATION_DURATION = 200L private val _clockFloatingSheetHeights: MutableStateFlow<ClockFloatingSheetHeightsViewModel> = MutableStateFlow(ClockFloatingSheetHeightsViewModel()) Loading Loading @@ -403,6 +399,10 @@ object ClockFloatingSheetBinder { clockSizeContentHeight ?: return@collect axisPresetSliderHeight ?: return@collect clockStyleContent.isVisible = currentTab == Tab.STYLE clockColorContent.isVisible = currentTab == Tab.COLOR clockSizeContent.isVisible = currentTab == Tab.SIZE val fromHeight = floatingSheetContainer.height val toHeight = when (selectedTab) { Loading @@ -424,38 +424,22 @@ object ClockFloatingSheetBinder { Tab.COLOR -> clockColorContent Tab.SIZE -> clockSizeContent } val shouldCurrentContentFadeOut = currentTab != selectedTab // Start to animate the content height ValueAnimator.ofInt(fromHeight, toHeight) .apply { addUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Int floatingSheetContainer.layoutParams = floatingSheetContainer.layoutParams.apply { height = value } if (shouldCurrentContentFadeOut) { currentContent.alpha = getAlpha(fromHeight, toHeight, value) } } duration = ANIMATION_DURATION addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { clockStyleContent.isVisible = selectedTab == Tab.STYLE clockStyleContent.alpha = 1f clockColorContent.isVisible = selectedTab == Tab.COLOR clockColorContent.alpha = 1f clockSizeContent.isVisible = selectedTab == Tab.SIZE clockSizeContent.alpha = 1f } val selectedContent: View = when (selectedTab) { Tab.STYLE -> clockStyleContent Tab.COLOR -> clockColorContent Tab.SIZE -> clockSizeContent } val fromContent = if (currentTab != selectedTab) currentContent else null val toContent = if (currentTab != selectedTab) selectedContent else null FloatingSheetHeightAnimationBinder.bind( floatingSheetContainer, fromHeight, toHeight, fromContent, toContent, ) } .start() currentTab = selectedTab } } Loading Loading @@ -683,8 +667,4 @@ object ClockFloatingSheetBinder { colorUpdateViewModel = WeakReference(colorUpdateViewModel), shouldAnimateColor = shouldAnimateColor, ) // Alpha is 1 when current height is from height, and 0 when current height is to height. private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float = (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat()) } src/com/android/wallpaper/customization/ui/binder/FloatingSheetHeightAnimationBinder.kt 0 → 100644 +63 −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.wallpaper.customization.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.view.View import androidx.core.view.isVisible object FloatingSheetHeightAnimationBinder { private const val ANIMATION_DURATION = 200L fun bind( floatingSheetContainer: View, fromHeight: Int, toHeight: Int, fromContent: View? = null, toContent: View? = null, ) { ValueAnimator.ofInt(fromHeight, toHeight) .apply { addUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Int floatingSheetContainer.layoutParams = floatingSheetContainer.layoutParams.apply { height = value } if (fromContent != null && toContent != null) { fromContent.alpha = getAlpha(fromHeight, toHeight, value) } } duration = ANIMATION_DURATION addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { fromContent?.isVisible = false fromContent?.alpha = 1f toContent?.isVisible = true toContent?.alpha = 1f } } ) } .start() } // Alpha is 1 when current height is from height, and 0 when current height is to height. private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float = (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat()) } Loading
res/layout/floating_sheet_app_icon.xml +78 −78 Original line number Diff line number Diff line Loading @@ -21,13 +21,12 @@ android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding" android:orientation="vertical"> <LinearLayout <FrameLayout android:id="@+id/floating_sheet_content_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingVertical="@dimen/floating_sheet_content_vertical_padding" android:background="@drawable/floating_sheet_content_background" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> Loading @@ -35,10 +34,8 @@ android:id="@+id/app_icon_style_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:clipToPadding="false" android:clipChildren="false" android:visibility="gone"> android:clipChildren="false"> <!-- This is just an invisible placeholder put in place so that the parent keeps its height Loading @@ -51,8 +48,6 @@ --> <include layout="@layout/icon_style_option2" android:layout_width="64dp" android:layout_height="64dp" android:visibility="invisible" /> <androidx.recyclerview.widget.RecyclerView Loading @@ -64,11 +59,17 @@ android:clipChildren="false" /> </FrameLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> <FrameLayout android:id="@+id/app_shape_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" android:clipToPadding="false" android:clipChildren="false"> Loading @@ -83,8 +84,6 @@ --> <include layout="@layout/shape_option2" android:layout_width="64dp" android:layout_height="64dp" android:visibility="invisible" /> <androidx.recyclerview.widget.RecyclerView Loading Loading @@ -146,6 +145,7 @@ android:theme="@style/Theme.Material3.DynamicColors.DayNight" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> </FrameLayout> <com.android.wallpaper.picker.customization.ui.view.FloatingToolbar android:id="@+id/floating_toolbar" Loading
src/com/android/wallpaper/customization/ui/binder/AppIconFloatingSheetBinder.kt +93 −5 Original line number Diff line number Diff line Loading @@ -20,6 +20,7 @@ import android.content.Context import android.content.res.ColorStateList import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ImageView import android.widget.TextView import androidx.core.graphics.drawable.DrawableCompat Loading @@ -38,7 +39,7 @@ import com.android.themepicker.R import com.android.wallpaper.config.BaseFlags import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.APP_ICONS import com.android.wallpaper.customization.ui.view.ShapeTileDrawable import com.android.wallpaper.customization.ui.viewmodel.AppIconPickerViewModel import com.android.wallpaper.customization.ui.viewmodel.AppIconPickerViewModel.Tab import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel import com.android.wallpaper.picker.customization.ui.binder.ColorUpdateBinder import com.android.wallpaper.picker.customization.ui.view.FloatingToolbar Loading @@ -48,6 +49,8 @@ import com.android.wallpaper.picker.option.ui.adapter.OptionItemAdapter2 import com.google.android.material.materialswitch.MaterialSwitch import java.lang.ref.WeakReference import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch object AppIconFloatingSheetBinder { Loading Loading @@ -137,6 +140,53 @@ object AppIconFloatingSheetBinder { val themedIconTitle = view.requireViewById<TextView>(R.id.themed_icon_toggle_title) val themedIconBetaLabel = view.requireViewById<TextView>(R.id.themed_icon_beta_title) data class FloatingSheetHeightsViewModel( val styleContentHeight: Int? = null, val shapeContentHeight: Int? = null, ) val floatingSheetHeights: MutableStateFlow<FloatingSheetHeightsViewModel> = MutableStateFlow(FloatingSheetHeightsViewModel()) if (isExtendibleThemeManager) { styleContent.viewTreeObserver.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { if ( styleContent.height != 0 && floatingSheetHeights.value.styleContentHeight != styleContent.height ) { floatingSheetHeights.value = floatingSheetHeights.value.copy( styleContentHeight = styleContent.height ) // Keep the height of the style floating sheet fixed so text renders // correctly after changing tabs. styleContent.layoutParams = styleContent.layoutParams.apply { height = styleContent.height } styleContent.viewTreeObserver.removeOnGlobalLayoutListener(this) } } } ) shapeContent.viewTreeObserver.addOnGlobalLayoutListener( object : OnGlobalLayoutListener { override fun onGlobalLayout() { if ( shapeContent.height != 0 && floatingSheetHeights.value.shapeContentHeight != shapeContent.height ) { floatingSheetHeights.value = floatingSheetHeights.value.copy( shapeContentHeight = shapeContent.height ) shapeContent.viewTreeObserver.removeOnGlobalLayoutListener(this) } } } ) } lifecycleOwner.lifecycleScope.launch { lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { launch { Loading Loading @@ -176,13 +226,51 @@ object AppIconFloatingSheetBinder { } launch { viewModel.selectedTab.collect { // TODO (b/397782741): add animation when switching tabs styleContent.isVisible = (it == AppIconPickerViewModel.Tab.STYLE) shapeContent.isVisible = (it == AppIconPickerViewModel.Tab.SHAPE) val verticalPadding = view.resources.getDimensionPixelSize( R.dimen.floating_sheet_content_vertical_padding ) var currentTab: Tab? = null combine(floatingSheetHeights, viewModel.selectedTab, ::Pair).collect { (heights, selectedTab) -> val (styleContentHeight, shapeContentHeight) = heights styleContentHeight ?: return@collect shapeContentHeight ?: return@collect selectedTab ?: return@collect styleContent.isVisible = (currentTab == Tab.STYLE) shapeContent.isVisible = (currentTab == Tab.SHAPE) val fromHeight = floatingSheetContainer.height val toHeight = when (selectedTab) { Tab.STYLE -> styleContentHeight Tab.SHAPE -> shapeContentHeight } + 2 * verticalPadding val currentContent: View? = when (currentTab) { Tab.STYLE -> styleContent Tab.SHAPE -> shapeContent else -> null } val selectedContent: View = when (selectedTab) { Tab.STYLE -> styleContent Tab.SHAPE -> shapeContent } FloatingSheetHeightAnimationBinder.bind( floatingSheetContainer, fromHeight, toHeight, currentContent, selectedContent, ) currentTab = selectedTab } } } else { styleContent.isVisible = false launch { viewModel.isShapeOptionsAvailable.collect { shapeAvailable -> shapeContent.isVisible = shapeAvailable Loading
src/com/android/wallpaper/customization/ui/binder/ClockFloatingSheetBinder.kt +19 −39 Original line number Diff line number Diff line Loading @@ -16,9 +16,6 @@ package com.android.wallpaper.customization.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.content.Context import android.view.View import android.view.ViewGroup Loading Loading @@ -76,7 +73,6 @@ import kotlinx.coroutines.launch object ClockFloatingSheetBinder { private const val SLIDER_ENABLED_ALPHA = 1f private const val SLIDER_DISABLED_ALPHA = .3f private const val ANIMATION_DURATION = 200L private val _clockFloatingSheetHeights: MutableStateFlow<ClockFloatingSheetHeightsViewModel> = MutableStateFlow(ClockFloatingSheetHeightsViewModel()) Loading Loading @@ -403,6 +399,10 @@ object ClockFloatingSheetBinder { clockSizeContentHeight ?: return@collect axisPresetSliderHeight ?: return@collect clockStyleContent.isVisible = currentTab == Tab.STYLE clockColorContent.isVisible = currentTab == Tab.COLOR clockSizeContent.isVisible = currentTab == Tab.SIZE val fromHeight = floatingSheetContainer.height val toHeight = when (selectedTab) { Loading @@ -424,38 +424,22 @@ object ClockFloatingSheetBinder { Tab.COLOR -> clockColorContent Tab.SIZE -> clockSizeContent } val shouldCurrentContentFadeOut = currentTab != selectedTab // Start to animate the content height ValueAnimator.ofInt(fromHeight, toHeight) .apply { addUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Int floatingSheetContainer.layoutParams = floatingSheetContainer.layoutParams.apply { height = value } if (shouldCurrentContentFadeOut) { currentContent.alpha = getAlpha(fromHeight, toHeight, value) } } duration = ANIMATION_DURATION addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { clockStyleContent.isVisible = selectedTab == Tab.STYLE clockStyleContent.alpha = 1f clockColorContent.isVisible = selectedTab == Tab.COLOR clockColorContent.alpha = 1f clockSizeContent.isVisible = selectedTab == Tab.SIZE clockSizeContent.alpha = 1f } val selectedContent: View = when (selectedTab) { Tab.STYLE -> clockStyleContent Tab.COLOR -> clockColorContent Tab.SIZE -> clockSizeContent } val fromContent = if (currentTab != selectedTab) currentContent else null val toContent = if (currentTab != selectedTab) selectedContent else null FloatingSheetHeightAnimationBinder.bind( floatingSheetContainer, fromHeight, toHeight, fromContent, toContent, ) } .start() currentTab = selectedTab } } Loading Loading @@ -683,8 +667,4 @@ object ClockFloatingSheetBinder { colorUpdateViewModel = WeakReference(colorUpdateViewModel), shouldAnimateColor = shouldAnimateColor, ) // Alpha is 1 when current height is from height, and 0 when current height is to height. private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float = (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat()) }
src/com/android/wallpaper/customization/ui/binder/FloatingSheetHeightAnimationBinder.kt 0 → 100644 +63 −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.wallpaper.customization.ui.binder import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.ValueAnimator import android.view.View import androidx.core.view.isVisible object FloatingSheetHeightAnimationBinder { private const val ANIMATION_DURATION = 200L fun bind( floatingSheetContainer: View, fromHeight: Int, toHeight: Int, fromContent: View? = null, toContent: View? = null, ) { ValueAnimator.ofInt(fromHeight, toHeight) .apply { addUpdateListener { valueAnimator -> val value = valueAnimator.animatedValue as Int floatingSheetContainer.layoutParams = floatingSheetContainer.layoutParams.apply { height = value } if (fromContent != null && toContent != null) { fromContent.alpha = getAlpha(fromHeight, toHeight, value) } } duration = ANIMATION_DURATION addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { fromContent?.isVisible = false fromContent?.alpha = 1f toContent?.isVisible = true toContent?.alpha = 1f } } ) } .start() } // Alpha is 1 when current height is from height, and 0 when current height is to height. private fun getAlpha(fromHeight: Int, toHeight: Int, currentHeight: Int): Float = (1 - (currentHeight - fromHeight).toFloat() / (toHeight - fromHeight).toFloat()) }