pref: Dialog 弹出动画

This commit is contained in:
HuanCheng65 2023-09-30 15:47:01 +08:00
parent 7a3c2c354d
commit 9356f043ed
No known key found for this signature in database
GPG Key ID: 5EC9DD60A32C7360
5 changed files with 341 additions and 87 deletions

View File

@ -35,30 +35,12 @@ import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle import com.ramcosta.composedestinations.spec.DestinationStyle
@Destination
@Composable
fun CopyTextPage(
text: String,
navigator: DestinationsNavigator,
) {
val context = LocalContext.current
CopyTextPageContent(
text = text,
onCopy = {
TiebaUtil.copyText(context, it)
},
onCancel = {
navigator.navigateUp()
}
)
}
object CopyTextDialogStyle : DestinationStyle.Dialog { object CopyTextDialogStyle : DestinationStyle.Dialog {
override val properties: DialogProperties override val properties: DialogProperties
get() = DialogProperties( get() = DialogProperties(
usePlatformDefaultWidth = false, usePlatformDefaultWidth = false,
decorFitsSystemWindows = false
) )
} }
@Destination( @Destination(
@ -70,6 +52,7 @@ fun CopyTextDialogPage(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier modifier = Modifier

View File

@ -122,7 +122,7 @@ import com.huanchengfly.tieba.post.ui.common.theme.compose.invertChipContent
import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator import com.huanchengfly.tieba.post.ui.common.theme.compose.pullRefreshIndicator
import com.huanchengfly.tieba.post.ui.common.theme.compose.threadBottomBar import com.huanchengfly.tieba.post.ui.common.theme.compose.threadBottomBar
import com.huanchengfly.tieba.post.ui.page.ProvideNavigator import com.huanchengfly.tieba.post.ui.page.ProvideNavigator
import com.huanchengfly.tieba.post.ui.page.destinations.CopyTextPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.CopyTextDialogPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ForumPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.ReplyPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.ReplyPageDestination
import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsSheetPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsSheetPageDestination
@ -875,7 +875,7 @@ fun ThreadPage(
}, },
onMenuCopyClick = { onMenuCopyClick = {
navigator.navigate( navigator.navigate(
CopyTextPageDestination(it) CopyTextDialogPageDestination(it)
) )
}, },
onMenuFavoriteClick = { onMenuFavoriteClick = {
@ -1216,7 +1216,7 @@ fun ThreadPage(
}, },
onMenuCopyClick = { onMenuCopyClick = {
navigator.navigate( navigator.navigate(
CopyTextPageDestination(it) CopyTextDialogPageDestination(it)
) )
}, },
onMenuFavoriteClick = { onMenuFavoriteClick = {

View File

@ -14,7 +14,6 @@ import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonDefaults
import androidx.compose.material.ButtonElevation import androidx.compose.material.ButtonElevation
import androidx.compose.material.ContentAlpha import androidx.compose.material.ContentAlpha
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalContentAlpha import androidx.compose.material.LocalContentAlpha
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProvideTextStyle import androidx.compose.material.ProvideTextStyle
@ -25,12 +24,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
@OptIn(ExperimentalMaterialApi::class)
@Composable @Composable
fun Button( fun Button(
onClick: () -> Unit, onClick: () -> Unit,
@ -50,6 +49,7 @@ fun Button(
val contentColor by colors.contentColor(enabled) val contentColor by colors.contentColor(enabled)
Surface( Surface(
modifier = Modifier modifier = Modifier
.clip(shape)
.clickable( .clickable(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,

View File

@ -1,18 +1,33 @@
package com.huanchengfly.tieba.post.ui.widgets.compose package com.huanchengfly.tieba.post.ui.widgets.compose
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.* import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.ProvideTextStyle
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -24,9 +39,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.DialogProperties
import com.huanchengfly.tieba.post.R import com.huanchengfly.tieba.post.R
import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme
import com.huanchengfly.tieba.post.ui.widgets.compose.dialogs.AnyPopDialog
import com.huanchengfly.tieba.post.ui.widgets.compose.dialogs.AnyPopDialogProperties
import com.huanchengfly.tieba.post.ui.widgets.compose.picker.TimePicker import com.huanchengfly.tieba.post.ui.widgets.compose.picker.TimePicker
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -305,71 +321,60 @@ fun Dialog(
content: @Composable (DialogScope.() -> Unit), content: @Composable (DialogScope.() -> Unit),
) { ) {
if (dialogState.show) { if (dialogState.show) {
var isActiveClose by remember {
mutableStateOf(false)
}
val dialogScope = DialogScope( val dialogScope = DialogScope(
onDismiss = onDismiss, onDismiss = {
dialogState = dialogState, isActiveClose = true
},
) )
androidx.compose.ui.window.Dialog( AnyPopDialog(
onDismissRequest = { dialogScope.dismiss() }, isActiveClose = isActiveClose,
properties = DialogProperties( onDismiss = {
usePlatformDefaultWidth = false, onDismiss?.invoke()
dialogState.show = false
},
properties = AnyPopDialogProperties(
dismissOnBackPress = cancelable, dismissOnBackPress = cancelable,
dismissOnClickOutside = cancelableOnTouchOutside dismissOnClickOutside = cancelableOnTouchOutside
) )
) { ) {
Box(modifier = Modifier Column(
.fillMaxSize() modifier = modifier
.clickable( .fillMaxWidth()
interactionSource = remember { MutableInteractionSource() }, .padding(16.dp)
indication = null, .background(
enabled = cancelableOnTouchOutside color = ExtendedTheme.colors.windowBackground,
) { shape = RoundedCornerShape(24.dp)
dialogScope.dismiss() )
} .padding(vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) { ) {
Column( ProvideContentColor(color = ExtendedTheme.colors.text) {
modifier = modifier if (title != null) {
.align(Alignment.BottomCenter) Box(
.fillMaxWidth() modifier = Modifier
.padding(16.dp) .padding(horizontal = 24.dp)
.background( .align(Alignment.CenterHorizontally)
color = ExtendedTheme.colors.windowBackground, ) {
shape = RoundedCornerShape(24.dp) ProvideTextStyle(value = MaterialTheme.typography.h6.copy(fontWeight = FontWeight.Bold)) {
) dialogScope.title()
.padding(vertical = 24.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = {}
),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ProvideContentColor(color = ExtendedTheme.colors.text) {
if (title != null) {
Box(
modifier = Modifier
.padding(horizontal = 24.dp)
.align(Alignment.CenterHorizontally)
) {
ProvideTextStyle(value = MaterialTheme.typography.h6.copy(fontWeight = FontWeight.Bold)) {
dialogScope.title()
}
} }
} }
Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { }
dialogScope.content() Box(modifier = Modifier.align(Alignment.CenterHorizontally)) {
} dialogScope.content()
Column( }
modifier = Modifier Column(
.padding(horizontal = 24.dp), modifier = Modifier
verticalArrangement = Arrangement.spacedBy(8.dp) .padding(horizontal = 24.dp),
) { verticalArrangement = Arrangement.spacedBy(8.dp)
dialogScope.buttons() ) {
} dialogScope.buttons()
} }
} }
} }
} }
} }
} }
@ -418,11 +423,9 @@ class DialogState private constructor(
} }
class DialogScope( class DialogScope(
private val dialogState: DialogState, private val onDismiss: () -> Unit,
private val onDismiss: (() -> Unit)? = null,
) { ) {
fun dismiss() { fun dismiss() {
onDismiss?.invoke() onDismiss()
dialogState.show = false
} }
} }

View File

@ -0,0 +1,268 @@
package com.huanchengfly.tieba.post.ui.widgets.compose.dialogs
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.view.View
import android.view.Window
import android.view.WindowManager
import androidx.activity.compose.BackHandler
import androidx.compose.animation.Animatable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider
import androidx.compose.ui.window.SecureFlagPolicy
import androidx.core.view.WindowCompat
import kotlinx.coroutines.delay
@Composable
fun getDialogWindow(): Window? = (LocalView.current.parent as? DialogWindowProvider)?.window
@Composable
fun getActivityWindow(): Window? = LocalView.current.context.getActivityWindow()
tailrec fun Context.getActivityWindow(): Window? = when (this) {
is Activity -> window
is ContextWrapper -> baseContext.getActivityWindow()
else -> null
}
private const val DefaultDurationMillis: Int = 250
@Composable
private fun DialogFullScreen(
isActiveClose: Boolean,
onDismissRequest: () -> Unit,
properties: AnyPopDialogProperties,
content: @Composable () -> Unit,
) {
var isAnimateLayout by remember {
mutableStateOf(false)
}
var isBackPress by remember {
mutableStateOf(false)
}
LaunchedEffect(isActiveClose) {
if (isActiveClose) {
isBackPress = true
isAnimateLayout = false
}
}
val handleBackPress = {
if (!isBackPress) {
isBackPress = true
isAnimateLayout = false
}
}
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
// 这里不使用新的测量规范不能设置为false
usePlatformDefaultWidth = true,
decorFitsSystemWindows = false,
dismissOnBackPress = false,
dismissOnClickOutside = false,
securePolicy = properties.securePolicy
),
content = {
val animColor = remember { Animatable(Color.Transparent) }
LaunchedEffect(isAnimateLayout) {
if (properties.backgroundDimEnabled) {
animColor.animateTo(
if (isAnimateLayout) Color.Black.copy(alpha = 0.45F) else Color.Transparent,
animationSpec = tween(properties.durationMillis)
)
} else {
delay(properties.durationMillis.toLong())
}
if (!isAnimateLayout) {
onDismissRequest.invoke()
}
}
val activityWindow = getActivityWindow()
val dialogWindow = getDialogWindow()
val parentView = LocalView.current.parent as View
SideEffect {
if (activityWindow != null && dialogWindow != null && !isBackPress && !isAnimateLayout) {
val attributes = WindowManager.LayoutParams()
attributes.copyFrom(activityWindow.attributes)
attributes.type = dialogWindow.attributes.type
dialogWindow.attributes = attributes
dialogWindow.setLayout(
activityWindow.decorView.width,
activityWindow.decorView.height
)
WindowCompat.getInsetsController(dialogWindow, parentView)
.isAppearanceLightNavigationBars =
properties.isAppearanceLightNavigationBars
isAnimateLayout = true
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = when (properties.direction) {
DirectionState.TOP -> Alignment.TopCenter
DirectionState.LEFT -> Alignment.CenterStart
DirectionState.RIGHT -> Alignment.CenterEnd
else -> Alignment.BottomCenter
}
) {
Spacer(
modifier = Modifier
.fillMaxSize()
.background(animColor.value)
.clickOutSideModifier(
dismissOnClickOutside = properties.dismissOnClickOutside,
onTap = handleBackPress
)
)
AnimatedVisibility(
modifier = Modifier.pointerInput(Unit) {},
visible = isAnimateLayout,
enter = when (properties.direction) {
DirectionState.TOP -> slideInVertically(initialOffsetY = { -it })
DirectionState.LEFT -> slideInHorizontally(initialOffsetX = { -it })
DirectionState.RIGHT -> slideInHorizontally(initialOffsetX = { it })
else -> slideInVertically(initialOffsetY = { it })
},
exit = when (properties.direction) {
DirectionState.TOP -> fadeOut() + slideOutVertically(targetOffsetY = { -it })
DirectionState.LEFT -> fadeOut() + slideOutHorizontally(targetOffsetX = { -it })
DirectionState.RIGHT -> fadeOut() + slideOutHorizontally(targetOffsetX = { it })
else -> fadeOut() + slideOutVertically(targetOffsetY = { it })
}
) {
content()
}
}
BackHandler(enabled = properties.dismissOnBackPress, onBack = handleBackPress)
}
)
}
/**
* @author 被风吹过的夏天
* @see <a href="https://github.com/TheMelody/AnyPopDialog-Compose">https://github.com/TheMelody/AnyPopDialog-Compose</a>
* @param isActiveClose 设置为true可触发动画关闭Dialog动画完自动触发[onDismiss]
* @param properties Dialog相关配置
* @param onDismiss Dialog关闭的回调
* @param content 可组合项视图
*/
@Composable
fun AnyPopDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
isActiveClose: Boolean = false,
properties: AnyPopDialogProperties = AnyPopDialogProperties(direction = DirectionState.BOTTOM),
content: @Composable () -> Unit,
) {
DialogFullScreen(
isActiveClose = isActiveClose,
onDismissRequest = onDismiss,
properties = properties
) {
Column(
modifier = modifier
.systemBarsPadding()
.imePadding()
) {
content()
}
}
}
/**
* @param dismissOnBackPress 是否支持返回关闭Dialog
* @param dismissOnClickOutside 是否支持空白区域点击关闭Dialog
* @param isAppearanceLightNavigationBars 导航栏前景色是不是亮色
* @param direction 当前对话框弹出的方向
* @param backgroundDimEnabled 背景渐入检出开关
* @param durationMillis 弹框消失和进入的时长
* @param securePolicy 屏幕安全策略
*/
@Immutable
class AnyPopDialogProperties(
val direction: DirectionState = DirectionState.BOTTOM,
val dismissOnBackPress: Boolean = true,
val dismissOnClickOutside: Boolean = true,
val isAppearanceLightNavigationBars: Boolean = true,
val backgroundDimEnabled: Boolean = true,
val durationMillis: Int = DefaultDurationMillis,
val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is AnyPopDialogProperties) return false
if (dismissOnBackPress != other.dismissOnBackPress) return false
if (isAppearanceLightNavigationBars != other.isAppearanceLightNavigationBars) return false
if (dismissOnClickOutside != other.dismissOnClickOutside) return false
if (direction != other.direction) return false
if (backgroundDimEnabled != other.backgroundDimEnabled) return false
if (durationMillis != other.durationMillis) return false
return securePolicy == other.securePolicy
}
override fun hashCode(): Int {
var result = dismissOnBackPress.hashCode()
result = 31 * result + dismissOnClickOutside.hashCode()
result = 31 * result + isAppearanceLightNavigationBars.hashCode()
result = 31 * result + direction.hashCode()
result = 31 * result + backgroundDimEnabled.hashCode()
result = 31 * result + durationMillis.hashCode()
result = 31 * result + securePolicy.hashCode()
return result
}
}
enum class DirectionState {
TOP,
LEFT,
RIGHT,
BOTTOM
}
private fun Modifier.clickOutSideModifier(
dismissOnClickOutside: Boolean,
onTap: () -> Unit,
) = this.then(
if (dismissOnClickOutside) {
Modifier.pointerInput(Unit) {
detectTapGestures(onTap = {
onTap()
})
}
} else Modifier
)