diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/dialogs/CopyDialogPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/dialogs/CopyDialogPage.kt index 5aedc17d..2ca6de21 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/dialogs/CopyDialogPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/dialogs/CopyDialogPage.kt @@ -35,30 +35,12 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator 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 { override val properties: DialogProperties get() = DialogProperties( usePlatformDefaultWidth = false, + decorFitsSystemWindows = false ) - } @Destination( @@ -70,6 +52,7 @@ fun CopyTextDialogPage( navigator: DestinationsNavigator, ) { val context = LocalContext.current + Box( contentAlignment = Alignment.Center, modifier = Modifier diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt index c50b6e31..a14a158a 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/page/thread/ThreadPage.kt @@ -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.threadBottomBar 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.ReplyPageDestination import com.huanchengfly.tieba.post.ui.page.destinations.SubPostsSheetPageDestination @@ -875,7 +875,7 @@ fun ThreadPage( }, onMenuCopyClick = { navigator.navigate( - CopyTextPageDestination(it) + CopyTextDialogPageDestination(it) ) }, onMenuFavoriteClick = { @@ -1216,7 +1216,7 @@ fun ThreadPage( }, onMenuCopyClick = { navigator.navigate( - CopyTextPageDestination(it) + CopyTextDialogPageDestination(it) ) }, onMenuFavoriteClick = { diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Buttons.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Buttons.kt index 5a0e53c2..99cdbcf6 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Buttons.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Buttons.kt @@ -14,7 +14,6 @@ import androidx.compose.material.ButtonColors import androidx.compose.material.ButtonDefaults import androidx.compose.material.ButtonElevation import androidx.compose.material.ContentAlpha -import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle @@ -25,12 +24,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.dp import com.huanchengfly.tieba.post.ui.common.theme.compose.ExtendedTheme -@OptIn(ExperimentalMaterialApi::class) @Composable fun Button( onClick: () -> Unit, @@ -50,6 +49,7 @@ fun Button( val contentColor by colors.contentColor(enabled) Surface( modifier = Modifier + .clip(shape) .clickable( onClick = onClick, enabled = enabled, diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Dialogs.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Dialogs.kt index cfce3055..fc82a73f 100644 --- a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Dialogs.kt +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/Dialogs.kt @@ -1,18 +1,33 @@ package com.huanchengfly.tieba.post.ui.widgets.compose import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.text.KeyboardActions 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.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.listSaver import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi 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.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.DialogProperties import com.huanchengfly.tieba.post.R 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 kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -305,71 +321,60 @@ fun Dialog( content: @Composable (DialogScope.() -> Unit), ) { if (dialogState.show) { + var isActiveClose by remember { + mutableStateOf(false) + } val dialogScope = DialogScope( - onDismiss = onDismiss, - dialogState = dialogState, + onDismiss = { + isActiveClose = true + }, ) - androidx.compose.ui.window.Dialog( - onDismissRequest = { dialogScope.dismiss() }, - properties = DialogProperties( - usePlatformDefaultWidth = false, + AnyPopDialog( + isActiveClose = isActiveClose, + onDismiss = { + onDismiss?.invoke() + dialogState.show = false + }, + properties = AnyPopDialogProperties( dismissOnBackPress = cancelable, dismissOnClickOutside = cancelableOnTouchOutside ) ) { - Box(modifier = Modifier - .fillMaxSize() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - enabled = cancelableOnTouchOutside - ) { - dialogScope.dismiss() - } + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + .background( + color = ExtendedTheme.colors.windowBackground, + shape = RoundedCornerShape(24.dp) + ) + .padding(vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Column( - modifier = modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .padding(16.dp) - .background( - color = ExtendedTheme.colors.windowBackground, - shape = RoundedCornerShape(24.dp) - ) - .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() - } + 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() - } - Column( - modifier = Modifier - .padding(horizontal = 24.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - dialogScope.buttons() - } + } + Box(modifier = Modifier.align(Alignment.CenterHorizontally)) { + dialogScope.content() + } + Column( + modifier = Modifier + .padding(horizontal = 24.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + dialogScope.buttons() } } } - } } } @@ -418,11 +423,9 @@ class DialogState private constructor( } class DialogScope( - private val dialogState: DialogState, - private val onDismiss: (() -> Unit)? = null, + private val onDismiss: () -> Unit, ) { fun dismiss() { - onDismiss?.invoke() - dialogState.show = false + onDismiss() } } \ No newline at end of file diff --git a/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/dialogs/AnyPopDialog.kt b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/dialogs/AnyPopDialog.kt new file mode 100644 index 00000000..73fad80c --- /dev/null +++ b/app/src/main/java/com/huanchengfly/tieba/post/ui/widgets/compose/dialogs/AnyPopDialog.kt @@ -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 https://github.com/TheMelody/AnyPopDialog-Compose + * @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 +) \ No newline at end of file