mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 03:58:48 +00:00
AppScaffold Animation Performance impromements.
This commit is contained in:
committed by
jeffrey-signal
parent
443463aca8
commit
ae8b8bbe7c
@@ -25,6 +25,7 @@ import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.graphics.layer.drawLayer
|
||||
import androidx.compose.ui.graphics.rememberGraphicsLayer
|
||||
@@ -47,7 +48,6 @@ import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
|
||||
import org.thoughtcrime.securesms.serialization.JsonSerializableNavType
|
||||
import org.thoughtcrime.securesms.window.AppScaffoldAnimationDefaults
|
||||
import org.thoughtcrime.securesms.window.AppScaffoldAnimationState
|
||||
import org.thoughtcrime.securesms.window.AppScaffoldNavigator
|
||||
import org.thoughtcrime.securesms.window.WindowSizeClass
|
||||
import kotlin.reflect.typeOf
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
@@ -90,7 +90,11 @@ fun NavGraphBuilder.chatNavGraphBuilder(
|
||||
bitmap = bitmap,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.then(fakeChatListAnimationState.toModifier())
|
||||
.graphicsLayer {
|
||||
with(fakeChatListAnimationState) {
|
||||
applyChildValues()
|
||||
}
|
||||
}
|
||||
.fillMaxSize()
|
||||
)
|
||||
}
|
||||
@@ -100,7 +104,11 @@ fun NavGraphBuilder.chatNavGraphBuilder(
|
||||
fragmentState = fragmentState,
|
||||
arguments = requireNotNull(ConversationIntents.createBuilderSync(context, route.conversationArgs).build().extras) { "Handed null Conversation intent arguments." },
|
||||
modifier = Modifier
|
||||
.then(chatAnimationState.toModifier())
|
||||
.graphicsLayer {
|
||||
with(chatAnimationState) {
|
||||
applyChildValues()
|
||||
}
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.fillMaxSize()
|
||||
) { fragment ->
|
||||
@@ -129,34 +137,37 @@ fun NavGraphBuilder.chatNavGraphBuilder(
|
||||
|
||||
@Composable
|
||||
private fun Transition<Boolean>.fakeChatListAnimationState(): AppScaffoldAnimationState {
|
||||
val alpha by animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0f else 1f }
|
||||
val offset by animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) (-48).dp else 0.dp }
|
||||
val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0f else 1f }
|
||||
val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) (-48).dp else 0.dp }
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.ENTER,
|
||||
offset = offset,
|
||||
alpha = alpha
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
offset = offset,
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Transition<Boolean>.chatAnimationState(hasFake: Boolean): AppScaffoldAnimationState {
|
||||
val alpha by animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 1f else 0f }
|
||||
val alpha = animateFloat(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 1f else 0f }
|
||||
|
||||
return if (!hasFake) {
|
||||
AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.ENTER,
|
||||
offset = 0.dp,
|
||||
alpha = alpha
|
||||
)
|
||||
remember {
|
||||
AppScaffoldAnimationState(
|
||||
offset = mutableStateOf(0.dp),
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val offset by animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0.dp else 48.dp }
|
||||
val offset = animateDp(transitionSpec = { AppScaffoldAnimationDefaults.tween() }) { if (it) 0.dp else 48.dp }
|
||||
|
||||
AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.ENTER,
|
||||
offset = offset,
|
||||
alpha = alpha
|
||||
)
|
||||
remember {
|
||||
AppScaffoldAnimationState(
|
||||
offset = offset,
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalInspectionMode
|
||||
@@ -277,11 +279,19 @@ fun AppScaffold(
|
||||
exitTransition = ExitTransition.None,
|
||||
modifier = Modifier
|
||||
.zIndex(0f)
|
||||
.then(animationState.parentModifier)
|
||||
.drawWithContent {
|
||||
with(animationState) {
|
||||
applyParentValues()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(animationState.toModifier())
|
||||
.graphicsLayer {
|
||||
with(animationState) {
|
||||
applyChildValues()
|
||||
}
|
||||
}
|
||||
.clipToBounds()
|
||||
.layout { measurable, constraints ->
|
||||
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
||||
@@ -320,11 +330,19 @@ fun AppScaffold(
|
||||
exitTransition = ExitTransition.None,
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
.then(animationState.parentModifier)
|
||||
.drawWithContent {
|
||||
with(animationState) {
|
||||
applyParentValues()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(animationState.toModifier())
|
||||
.graphicsLayer {
|
||||
with(animationState) {
|
||||
applyChildValues()
|
||||
}
|
||||
}
|
||||
.clipToBounds()
|
||||
.layout { measurable, constraints ->
|
||||
val width = max(minPaneWidth.roundToPx(), constraints.maxWidth)
|
||||
|
||||
@@ -7,17 +7,19 @@ package org.thoughtcrime.securesms.window
|
||||
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.GraphicsLayerScope
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
@@ -30,28 +32,38 @@ object AppScaffoldAnimationDefaults {
|
||||
fun <T> tween() = tween<T>(durationMillis = 200, easing = TweenEasing)
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces modifier that can be composed into another modifier chain.
|
||||
* This object allows us to store "latest state" as we transition.
|
||||
*
|
||||
* @param parentModifier This modifier is applied to the [androidx.compose.material3.adaptive.layout.AnimatedPane] itself,
|
||||
* allowing additional customization like overlays without
|
||||
* the need for additional composables.
|
||||
*/
|
||||
data class AppScaffoldAnimationState(
|
||||
val navigationState: AppScaffoldNavigator.NavigationState,
|
||||
val alpha: Float = 1f,
|
||||
val scale: Float = 1f,
|
||||
val offset: Dp = 0.dp,
|
||||
val clipShape: Shape = RoundedCornerShape(0.dp),
|
||||
val parentModifier: Modifier = Modifier.Companion
|
||||
private val alpha: State<Float> = mutableStateOf(1f),
|
||||
private val scale: State<Float> = mutableStateOf(1f),
|
||||
val scaleMinimum: Float = 0f,
|
||||
private val offset: State<Dp> = mutableStateOf(0.dp),
|
||||
private val corners: State<Dp> = mutableStateOf(0.dp),
|
||||
val cornersMaximum: Dp = 1000.dp,
|
||||
private val parentOverlayAlpha: State<Float> = mutableStateOf(0f)
|
||||
) {
|
||||
fun toModifier(): Modifier {
|
||||
return Modifier
|
||||
.alpha(alpha)
|
||||
.scale(scale)
|
||||
.offset(offset)
|
||||
.clip(clipShape)
|
||||
|
||||
private val unclampedScale by scale
|
||||
private val unclampedCorners by corners
|
||||
|
||||
val contentAlpha by alpha
|
||||
val contentScale by derivedStateOf { unclampedScale.coerceAtLeast(scaleMinimum) }
|
||||
val contentOffset by offset
|
||||
val contentCorners by derivedStateOf { unclampedCorners.coerceAtMost(cornersMaximum) }
|
||||
|
||||
fun ContentDrawScope.applyParentValues() {
|
||||
drawContent()
|
||||
|
||||
drawRect(Color(0f, 0f, 0f, parentOverlayAlpha.value))
|
||||
}
|
||||
|
||||
fun GraphicsLayerScope.applyChildValues() {
|
||||
this.alpha = contentAlpha
|
||||
this.scaleX = contentScale
|
||||
this.scaleY = contentScale
|
||||
this.translationX = contentOffset.toPx()
|
||||
this.translationY = 0f
|
||||
this.clip = true
|
||||
this.shape = RoundedCornerShape(contentCorners)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,13 +79,12 @@ class AppScaffoldAnimationStateFactory(
|
||||
val Default = AppScaffoldAnimationStateFactory()
|
||||
|
||||
private val EMPTY_STATE = AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.ENTER,
|
||||
alpha = 1f
|
||||
alpha = mutableStateOf(1f)
|
||||
)
|
||||
}
|
||||
|
||||
private var latestListSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK)
|
||||
private var latestDetailSeekState: AppScaffoldAnimationState = AppScaffoldAnimationState(AppScaffoldNavigator.NavigationState.SEEK)
|
||||
private var latestListSeekState: AppScaffoldAnimationState = EMPTY_STATE
|
||||
private var latestDetailSeekState: AppScaffoldAnimationState = EMPTY_STATE
|
||||
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.getListAnimationState(navigationState: AppScaffoldNavigator.NavigationState): AppScaffoldAnimationState {
|
||||
@@ -87,6 +98,7 @@ class AppScaffoldAnimationStateFactory(
|
||||
AppScaffoldNavigator.NavigationState.SEEK -> defaultListSeekAnimationState().also {
|
||||
latestListSeekState = it
|
||||
}
|
||||
|
||||
AppScaffoldNavigator.NavigationState.RELEASE -> defaultListReleaseAnimationState(latestListSeekState)
|
||||
}
|
||||
}
|
||||
@@ -103,6 +115,7 @@ class AppScaffoldAnimationStateFactory(
|
||||
AppScaffoldNavigator.NavigationState.SEEK -> defaultDetailSeekAnimationState().also {
|
||||
latestDetailSeekState = it
|
||||
}
|
||||
|
||||
AppScaffoldNavigator.NavigationState.RELEASE -> defaultDetailReleaseAnimationState(latestDetailSeekState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,18 +11,15 @@ import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldPaneScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtMost
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
private const val SEEK_DAMPING_RATIO = Spring.DampingRatioNoBouncy
|
||||
@@ -79,7 +76,7 @@ fun ThreePaneScaffoldPaneScope.animateFloat(
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnimationState {
|
||||
val offset by animateDp(
|
||||
val offset = animateDp(
|
||||
targetWhenHiding = {
|
||||
-AppScaffoldAnimationDefaults.InitAnimationOffset
|
||||
},
|
||||
@@ -88,21 +85,22 @@ fun ThreePaneScaffoldPaneScope.defaultListInitAnimationState(): AppScaffoldAnima
|
||||
}
|
||||
)
|
||||
|
||||
val alpha by animateFloat {
|
||||
val alpha = animateFloat {
|
||||
1f
|
||||
}
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
AppScaffoldNavigator.NavigationState.ENTER,
|
||||
alpha = alpha,
|
||||
offset = offset
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
alpha = alpha,
|
||||
offset = offset
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnimationState {
|
||||
val scale by animateFloat(
|
||||
val scale = animateFloat(
|
||||
transitionSpec = {
|
||||
appScaffoldSeekSpring()
|
||||
},
|
||||
@@ -110,7 +108,7 @@ fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnima
|
||||
targetWhenHiding = { 1f }
|
||||
)
|
||||
|
||||
val offset by animateDp(
|
||||
val offset = animateDp(
|
||||
transitionSpec = {
|
||||
appScaffoldSeekSpring()
|
||||
},
|
||||
@@ -118,52 +116,51 @@ fun ThreePaneScaffoldPaneScope.defaultListSeekAnimationState(): AppScaffoldAnima
|
||||
targetWhenShowing = { 0.dp }
|
||||
)
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.SEEK,
|
||||
offset = offset,
|
||||
scale = scale.coerceAtLeast(0.9f),
|
||||
parentModifier = Modifier.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
drawRect(Color(0f, 0f, 0f, 0.2f))
|
||||
}
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
offset = offset,
|
||||
scale = scale,
|
||||
scaleMinimum = 0.9f,
|
||||
parentOverlayAlpha = mutableStateOf(0.2f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultListReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState {
|
||||
val scale by animateFloat(
|
||||
targetWhenHiding = { from.scale },
|
||||
val initialScale = remember { from.contentScale }
|
||||
val initialOffset = remember { from.contentOffset }
|
||||
|
||||
val scale = animateFloat(
|
||||
targetWhenHiding = { initialScale },
|
||||
targetWhenShowing = { 1f }
|
||||
)
|
||||
|
||||
val offset by animateDp(
|
||||
targetWhenHiding = { from.offset },
|
||||
val offset = animateDp(
|
||||
targetWhenHiding = { initialOffset },
|
||||
targetWhenShowing = { 0.dp }
|
||||
)
|
||||
|
||||
val alpha by animateFloat(
|
||||
val alpha = animateFloat(
|
||||
targetWhenHiding = { 0.2f },
|
||||
targetWhenShowing = { 0f }
|
||||
)
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.RELEASE,
|
||||
scale = scale,
|
||||
offset = offset,
|
||||
parentModifier = Modifier.drawWithContent {
|
||||
drawContent()
|
||||
|
||||
drawRect(Color(0f, 0f, 0f, alpha))
|
||||
}
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
scale = scale,
|
||||
scaleMinimum = from.scaleMinimum,
|
||||
offset = offset,
|
||||
parentOverlayAlpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAnimationState {
|
||||
val offset by animateDp(
|
||||
val offset = animateDp(
|
||||
targetWhenHiding = {
|
||||
AppScaffoldAnimationDefaults.InitAnimationOffset
|
||||
},
|
||||
@@ -172,21 +169,22 @@ fun ThreePaneScaffoldPaneScope.defaultDetailInitAnimationState(): AppScaffoldAni
|
||||
}
|
||||
)
|
||||
|
||||
val alpha by animateFloat {
|
||||
val alpha = animateFloat {
|
||||
1f
|
||||
}
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.ENTER,
|
||||
alpha = alpha,
|
||||
offset = offset
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
alpha = alpha,
|
||||
offset = offset
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAnimationState {
|
||||
val scale by animateFloat(
|
||||
val scale = animateFloat(
|
||||
transitionSpec = {
|
||||
appScaffoldSeekSpring()
|
||||
},
|
||||
@@ -194,7 +192,7 @@ fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAni
|
||||
targetWhenHiding = { 0.5f }
|
||||
)
|
||||
|
||||
val offset by animateDp(
|
||||
val offset = animateDp(
|
||||
transitionSpec = {
|
||||
appScaffoldSeekSpring()
|
||||
},
|
||||
@@ -202,30 +200,44 @@ fun ThreePaneScaffoldPaneScope.defaultDetailSeekAnimationState(): AppScaffoldAni
|
||||
targetWhenHiding = { 88.dp }
|
||||
)
|
||||
|
||||
val roundedCorners by animateDp(
|
||||
val roundedCorners = animateDp(
|
||||
transitionSpec = {
|
||||
appScaffoldSeekSpring()
|
||||
}
|
||||
) { 1000.dp }
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.SEEK,
|
||||
scale = scale.coerceAtLeast(0.9f),
|
||||
offset = offset,
|
||||
clipShape = RoundedCornerShape(roundedCorners.coerceAtMost(42.dp))
|
||||
)
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
scale = scale,
|
||||
scaleMinimum = 0.9f,
|
||||
offset = offset,
|
||||
corners = roundedCorners,
|
||||
cornersMaximum = 42.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun ThreePaneScaffoldPaneScope.defaultDetailReleaseAnimationState(from: AppScaffoldAnimationState): AppScaffoldAnimationState {
|
||||
val alpha by animateFloat { 1f }
|
||||
val scale = remember { from.contentScale }
|
||||
val offset = remember { from.contentOffset }
|
||||
val corners = remember { from.contentCorners }
|
||||
|
||||
return AppScaffoldAnimationState(
|
||||
navigationState = AppScaffoldNavigator.NavigationState.RELEASE,
|
||||
scale = from.scale,
|
||||
offset = from.offset,
|
||||
clipShape = from.clipShape,
|
||||
alpha = alpha
|
||||
)
|
||||
val scaleState = remember { mutableStateOf(scale) }
|
||||
val offsetState = remember { mutableStateOf(offset) }
|
||||
val cornersState = remember { mutableStateOf(corners) }
|
||||
|
||||
val alpha = animateFloat { 1f }
|
||||
|
||||
return remember {
|
||||
AppScaffoldAnimationState(
|
||||
scale = scaleState,
|
||||
scaleMinimum = from.scaleMinimum,
|
||||
offset = offsetState,
|
||||
corners = cornersState,
|
||||
cornersMaximum = from.cornersMaximum,
|
||||
alpha = alpha
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user