From 613186107247cc7eeb38d26c34f14b0d6a149420 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:44:54 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[Feat]=20BottomSheet=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/dialog/BottomSheetDialog.kt | 46 ++ .../dialog/BottomSheetDialogProperties.kt | 570 ++++++++++++++++++ 2 files changed, 616 insertions(+) create mode 100644 core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialog.kt create mode 100644 core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialogProperties.kt diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialog.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialog.kt new file mode 100644 index 00000000..201e2527 --- /dev/null +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialog.kt @@ -0,0 +1,46 @@ +package team.ppac.designsystem.component.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import team.ppac.designsystem.FarmemeTheme +import team.ppac.designsystem.foundation.FarmemeRadius + +@Composable +fun FarmemeBottomSheetDialog( + modifier: Modifier = Modifier, + canceledOnTouchOutside: Boolean = true, + isDraggable: Boolean = false, + bottomSheetProperties: BottomSheetDialogProperties = BottomSheetDialogProperties( + dismissOnClickOutside = canceledOnTouchOutside, + dismissOnBackPress = canceledOnTouchOutside, + navigationBarProperties = NavigationBarProperties( + color = FarmemeTheme.backgroundColor.white + ), + behaviorProperties = BottomSheetBehaviorProperties( + isDraggable = isDraggable + ) + ), + onBottomSheetDismiss: () -> Unit, + content: @Composable () -> Unit, +) { + BottomSheetDialog( + onDismissRequest = onBottomSheetDismiss, + properties = bottomSheetProperties, + ) { + Column( + modifier = modifier + .clip(FarmemeRadius.RadiusTop20.shape) + .fillMaxWidth() + .background(FarmemeTheme.backgroundColor.white) + .padding(top = 16.dp, bottom = 10.dp) + ) { + content() + } + } +} \ No newline at end of file diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialogProperties.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialogProperties.kt new file mode 100644 index 00000000..60c6f461 --- /dev/null +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/dialog/BottomSheetDialogProperties.kt @@ -0,0 +1,570 @@ +package team.ppac.designsystem.component.dialog + +import android.content.Context +import android.graphics.Outline +import android.os.Build +import android.view.* +import androidx.activity.addCallback +import androidx.activity.setViewTreeOnBackPressedDispatcherOwner +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import androidx.annotation.Px +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.* +import androidx.compose.ui.semantics.dialog +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogWindowProvider +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.core.view.WindowCompat +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.findViewTreeViewModelStoreOwner +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.savedstate.findViewTreeSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.PEEK_HEIGHT_AUTO +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HALF_EXPANDED +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN +import com.google.android.material.bottomsheet.BottomSheetDialog +import team.ppac.designsystem.R +import java.util.* + +@Immutable +class BottomSheetDialogProperties( + val dismissOnBackPress: Boolean = true, + val dismissOnClickOutside: Boolean = true, + val dismissWithAnimation: Boolean = false, + val enableEdgeToEdge: Boolean = false, + val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, + val navigationBarProperties: NavigationBarProperties = NavigationBarProperties(), + val behaviorProperties: BottomSheetBehaviorProperties = BottomSheetBehaviorProperties() +) { + + @Deprecated("Use NavigationBarProperties(color = navigationBarColor) instead") + constructor( + dismissOnBackPress: Boolean = true, + dismissOnClickOutside: Boolean = true, + dismissWithAnimation: Boolean = false, + securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, + navigationBarColor: Color + ) : this( + dismissOnBackPress, + dismissOnClickOutside, + dismissWithAnimation, + false, + securePolicy, + NavigationBarProperties(color = navigationBarColor) + ) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BottomSheetDialogProperties) return false + + if (dismissOnBackPress != other.dismissOnBackPress) return false + if (dismissOnClickOutside != other.dismissOnClickOutside) return false + if (dismissWithAnimation != other.dismissWithAnimation) return false + if (enableEdgeToEdge != other.enableEdgeToEdge) return false + if (securePolicy != other.securePolicy) return false + if (navigationBarProperties != other.navigationBarProperties) return false + return behaviorProperties == other.behaviorProperties + } + + override fun hashCode(): Int { + var result = dismissOnBackPress.hashCode() + result = 31 * result + dismissOnClickOutside.hashCode() + result = 31 * result + dismissWithAnimation.hashCode() + result = 31 * result + enableEdgeToEdge.hashCode() + result = 31 * result + securePolicy.hashCode() + result = 31 * result + navigationBarProperties.hashCode() + result = 31 * result + behaviorProperties.hashCode() + return result + } +} + +/** + * Properties used to set [com.google.android.material.bottomsheet.BottomSheetBehavior]. + * + * @see [com.google.android.material.bottomsheet.BottomSheetBehavior] + */ +@Immutable +class BottomSheetBehaviorProperties( + val state: State = State.Collapsed, + val maxWidth: Size = Size.NotSet, + val maxHeight: Size = Size.NotSet, + val isDraggable: Boolean = true, + @IntRange(from = 0) + val expandedOffset: Int = 0, + @FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false) + val halfExpandedRatio: Float = 0.5F, + val isHideable: Boolean = true, + val peekHeight: PeekHeight = PeekHeight.Auto, + val isFitToContents: Boolean = true, + val skipCollapsed: Boolean = false, + val isGestureInsetBottomIgnored: Boolean = false +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is BottomSheetBehaviorProperties) return false + + if (state != other.state) return false + if (maxWidth != other.maxWidth) return false + if (maxHeight != other.maxHeight) return false + if (isDraggable != other.isDraggable) return false + if (expandedOffset != other.expandedOffset) return false + if (halfExpandedRatio != other.halfExpandedRatio) return false + if (isHideable != other.isHideable) return false + if (peekHeight != other.peekHeight) return false + if (isFitToContents != other.isFitToContents) return false + if (skipCollapsed != other.skipCollapsed) return false + return isGestureInsetBottomIgnored == other.isGestureInsetBottomIgnored + } + + override fun hashCode(): Int { + var result = state.hashCode() + result = 31 * result + maxWidth.hashCode() + result = 31 * result + maxHeight.hashCode() + result = 31 * result + isDraggable.hashCode() + result = 31 * result + expandedOffset.hashCode() + result = 31 * result + halfExpandedRatio.hashCode() + result = 31 * result + isHideable.hashCode() + result = 31 * result + peekHeight.hashCode() + result = 31 * result + isFitToContents.hashCode() + result = 31 * result + skipCollapsed.hashCode() + result = 31 * result + isGestureInsetBottomIgnored.hashCode() + return result + } + + @Immutable + enum class State { + @Stable + Expanded, + + @Stable + HalfExpanded, + + @Stable + Collapsed + } + + @JvmInline + @Immutable + value class Size(@Px val value: Int) { + + companion object { + @Stable + val NotSet = Size(-1) + } + } + + @JvmInline + @Stable + value class PeekHeight(val value: Int) { + + companion object { + @Stable + val Auto = PeekHeight(PEEK_HEIGHT_AUTO) + } + } +} + +/** + * Properties used to customize navigationBar. + + * @param color The **desired** [Color] to set. This may require modification if running on an + * API level that only supports white navigation bar icons. Additionally this will be ignored + * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the + * system UI automatically applies background protection in other navigation modes. + * @param darkIcons Whether dark navigation bar icons would be preferable. + * @param navigationBarContrastEnforced Whether the system should ensure that the navigation + * bar has enough contrast when a fully transparent background is requested. Only supported on + * API 29+. + * @param transformColorForLightContent A lambda which will be invoked to transform [color] if + * dark icons were requested but are not available. Defaults to applying a black scrim. + * + * Inspired by [Accompanist SystemUiController](https://github.com/google/accompanist/blob/main/systemuicontroller/src/main/java/com/google/accompanist/systemuicontroller/SystemUiController.kt) + */ + +@Immutable +class NavigationBarProperties( + val color: Color = Color.Unspecified, + val darkIcons: Boolean = color.luminance() > 0.5f, + val navigationBarContrastEnforced: Boolean = true, + val transformColorForLightContent: (Color) -> Color = BlackScrimmed +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NavigationBarProperties) return false + + if (color != other.color) return false + if (darkIcons != other.darkIcons) return false + if (navigationBarContrastEnforced != other.navigationBarContrastEnforced) return false + return transformColorForLightContent == other.transformColorForLightContent + } + + override fun hashCode(): Int { + var result = color.hashCode() + result = 31 * result + darkIcons.hashCode() + result = 31 * result + navigationBarContrastEnforced.hashCode() + return result + } +} + +private val BlackScrim = Color(0f, 0f, 0f, 0.3f) // 30% opaque black +private val BlackScrimmed: (Color) -> Color = { original -> + BlackScrim.compositeOver(original) +} + +/** + * Opens a bottomsheet dialog with the given content. + * + * The dialog is visible as long as it is part of the composition hierarchy. + * In order to let the user dismiss the BottomSheetDialog, the implementation of [onDismissRequest] should + * contain a way to remove to remove the dialog from the composition hierarchy. + * + * Example usage: + * + * @sample com.holix.android.bottomsheetdialogcomposedemo.MainActivity + * + * @param onDismissRequest Executes when the user tries to dismiss the dialog. + * @param properties [BottomSheetDialogProperties] for further customization of this dialog's behavior. + * @param content The content to be displayed inside the dialog. + */ +@Composable +fun BottomSheetDialog( + onDismissRequest: () -> Unit, + properties: BottomSheetDialogProperties = BottomSheetDialogProperties(), + content: @Composable () -> Unit +) { + val view = LocalView.current + val density = LocalDensity.current + val layoutDirection = LocalLayoutDirection.current + val composition = rememberCompositionContext() + val currentContent by rememberUpdatedState(content) + val dialogId = rememberSaveable { UUID.randomUUID() } + val dialog = remember(view, density) { + BottomSheetDialogWrapper( + onDismissRequest, + properties, + view, + layoutDirection, + density, + dialogId + ).apply { + setContent(composition) { + BottomSheetDialogLayout( + Modifier + .nestedScroll(rememberNestedScrollInteropConnection()) + .scrollable( + state = rememberScrollableState { 0f }, + orientation = Orientation.Vertical + ) + .semantics { dialog() }, + ) { + currentContent() + } + } + } + } + + DisposableEffect(dialog) { + dialog.show() + + onDispose { + dialog.dismiss() + dialog.disposeComposition() + } + } + + SideEffect { + dialog.updateParameters( + onDismissRequest = onDismissRequest, + properties = properties, + layoutDirection = layoutDirection + ) + } +} + +@Suppress("ViewConstructor") +private class BottomSheetDialogLayout( + context: Context, + override val window: Window +) : AbstractComposeView(context), DialogWindowProvider { + private var content: @Composable () -> Unit by mutableStateOf({}) + + override var shouldCreateCompositionOnAttachedToWindow: Boolean = false + private set + + fun setContent(parent: CompositionContext, content: @Composable () -> Unit) { + setParentCompositionContext(parent) + this.content = content + shouldCreateCompositionOnAttachedToWindow = true + createComposition() + } + + @Composable + override fun Content() { + content() + } +} + +private class BottomSheetDialogWrapper( + private var onDismissRequest: () -> Unit, + private var properties: BottomSheetDialogProperties, + private val composeView: View, + layoutDirection: LayoutDirection, + density: Density, + dialogId: UUID +) : BottomSheetDialog( + ContextThemeWrapper( + composeView.context, + if (properties.enableEdgeToEdge) { + R.style.TransparentEdgeToEdgeEnabledBottomSheetTheme + } else { + R.style.TransparentEdgeToEdgeDisabledBottomSheetTheme + } + ) +), + ViewRootForInspector { + private val bottomSheetDialogLayout: BottomSheetDialogLayout + + private val bottomSheetCallbackForAnimation: BottomSheetBehavior.BottomSheetCallback = + object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, slideOffset: Float) { + } + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == STATE_HIDDEN) { + onDismissRequest() + } + } + } + + private val maxSupportedElevation = 30.dp + + override val subCompositionView: AbstractComposeView get() = bottomSheetDialogLayout + + // to control insets + private val windowInsetsController = window?.let { + WindowCompat.getInsetsController(it, it.decorView) + } + private var navigationBarDarkContentEnabled: Boolean + get() = windowInsetsController?.isAppearanceLightNavigationBars == true + set(value) { + windowInsetsController?.isAppearanceLightNavigationBars = value + } + + private var isNavigationBarContrastEnforced: Boolean + get() = Build.VERSION.SDK_INT >= 29 && window?.isNavigationBarContrastEnforced == true + set(value) { + if (Build.VERSION.SDK_INT >= 29) { + window?.isNavigationBarContrastEnforced = value + } + } + + init { + val window = window ?: error("Dialog has no window") + window.setBackgroundDrawableResource(android.R.color.transparent) + bottomSheetDialogLayout = BottomSheetDialogLayout(context, window).apply { + // Set unique id for AbstractComposeView. This allows state restoration for the state + // defined inside the Dialog via rememberSaveable() + setTag(androidx.compose.ui.R.id.compose_view_saveable_id_tag, "Dialog:$dialogId") + // Enable children to draw their shadow by not clipping them + clipChildren = false + // Allocate space for elevation + with(density) { elevation = maxSupportedElevation.toPx() } + // Simple outline to force window manager to allocate space for shadow. + // Note that the outline affects clickable area for the dismiss listener. In case of + // shapes like circle the area for dismiss might be to small (rectangular outline + // consuming clicks outside of the circle). + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, result: Outline) { + result.setRect(0, 0, view.width, view.height) + // We set alpha to 0 to hide the view's shadow and let the composable to draw + // its own shadow. This still enables us to get the extra space needed in the + // surface. + result.alpha = 0f + } + } + } + + /** + * Disables clipping for [this] and all its descendant [ViewGroup]s until we reach a + * [BottomSheetDialogLayout] (the [ViewGroup] containing the Compose hierarchy). + */ + fun ViewGroup.disableClipping() { + clipChildren = false + if (this is BottomSheetDialogLayout) return + for (i in 0 until childCount) { + (getChildAt(i) as? ViewGroup)?.disableClipping() + } + } + + // Turn of all clipping so shadows can be drawn outside the window + (window.decorView as? ViewGroup)?.disableClipping() + setContentView(bottomSheetDialogLayout) + bottomSheetDialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner()) + bottomSheetDialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner()) + bottomSheetDialogLayout.setViewTreeOnBackPressedDispatcherOwner(this) + bottomSheetDialogLayout.setViewTreeSavedStateRegistryOwner( + composeView.findViewTreeSavedStateRegistryOwner() + ) + + // Initial setup + updateParameters(onDismissRequest, properties, layoutDirection) + + onBackPressedDispatcher.addCallback(this) { + if (properties.dismissOnBackPress) { + cancel() + } + } + } + + private fun setLayoutDirection(layoutDirection: LayoutDirection) { + bottomSheetDialogLayout.layoutDirection = when (layoutDirection) { + LayoutDirection.Ltr -> android.util.LayoutDirection.LTR + LayoutDirection.Rtl -> android.util.LayoutDirection.RTL + } + } + + fun setContent(parentComposition: CompositionContext, children: @Composable () -> Unit) { + bottomSheetDialogLayout.setContent(parentComposition, children) + } + + private fun setSecurePolicy(securePolicy: SecureFlagPolicy) { + val secureFlagEnabled = + securePolicy.shouldApplySecureFlag(composeView.isFlagSecureEnabled()) + window!!.setFlags( + if (secureFlagEnabled) { + WindowManager.LayoutParams.FLAG_SECURE + } else { + WindowManager.LayoutParams.FLAG_SECURE.inv() + }, + WindowManager.LayoutParams.FLAG_SECURE + ) + } + + private fun setNavigationBarProperties(properties: NavigationBarProperties) { + with(properties) { + navigationBarDarkContentEnabled = darkIcons + isNavigationBarContrastEnforced = navigationBarContrastEnforced + + window?.navigationBarColor = when { + darkIcons && windowInsetsController?.isAppearanceLightNavigationBars != true -> { + // If we're set to use dark icons, but our windowInsetsController call didn't + // succeed (usually due to API level), we instead transform the color to maintain + // contrast + transformColorForLightContent(color) + } + + else -> color + }.toArgb() + } + } + + override fun setDismissWithAnimation(dismissWithAnimation: Boolean) { + super.setDismissWithAnimation(dismissWithAnimation) + if (dismissWithAnimation) { + behavior.addBottomSheetCallback(bottomSheetCallbackForAnimation) + } else { + behavior.removeBottomSheetCallback(bottomSheetCallbackForAnimation) + } + } + + private fun setBehaviorProperties(behaviorProperties: BottomSheetBehaviorProperties) { + this.behavior.state = when (behaviorProperties.state) { + BottomSheetBehaviorProperties.State.Expanded -> STATE_EXPANDED + BottomSheetBehaviorProperties.State.Collapsed -> STATE_COLLAPSED + BottomSheetBehaviorProperties.State.HalfExpanded -> STATE_HALF_EXPANDED + } + this.behavior.maxWidth = behaviorProperties.maxWidth.value + this.behavior.maxHeight = behaviorProperties.maxHeight.value + this.behavior.isDraggable = behaviorProperties.isDraggable + this.behavior.expandedOffset = behaviorProperties.expandedOffset + this.behavior.halfExpandedRatio = behaviorProperties.halfExpandedRatio + this.behavior.isHideable = behaviorProperties.isHideable + this.behavior.peekHeight = behaviorProperties.peekHeight.value + this.behavior.isFitToContents = behaviorProperties.isFitToContents + this.behavior.skipCollapsed = behaviorProperties.skipCollapsed + this.behavior.isGestureInsetBottomIgnored = behaviorProperties.isGestureInsetBottomIgnored + } + + fun updateParameters( + onDismissRequest: () -> Unit, + properties: BottomSheetDialogProperties, + layoutDirection: LayoutDirection + ) { + this.onDismissRequest = onDismissRequest + this.properties = properties + setSecurePolicy(properties.securePolicy) + setLayoutDirection(layoutDirection) + setCanceledOnTouchOutside(properties.dismissOnClickOutside) + setNavigationBarProperties(properties.navigationBarProperties) + setBehaviorProperties(properties.behaviorProperties) + dismissWithAnimation = properties.dismissWithAnimation + } + + fun disposeComposition() { + bottomSheetDialogLayout.disposeComposition() + } + + override fun cancel() { + if (properties.dismissWithAnimation) { + // call setState(STATE_HIDDEN) -> onDismissRequest will be called in BottomSheetCallback + super.cancel() + } else { + // dismiss with window animation + onDismissRequest() + } + } +} + +@Composable +private fun BottomSheetDialogLayout( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Layout( + content = content, + modifier = modifier + ) { measurables, constraints -> + val placeables = measurables.map { it.measure(constraints) } + val width = placeables.maxByOrNull { it.width }?.width ?: constraints.minWidth + val height = placeables.maxByOrNull { it.height }?.height ?: constraints.minHeight + layout(width, height) { + placeables.forEach { it.placeRelative(0, 0) } + } + } +} + +fun View.isFlagSecureEnabled(): Boolean { + val windowParams = rootView.layoutParams as? WindowManager.LayoutParams + if (windowParams != null) { + return (windowParams.flags and WindowManager.LayoutParams.FLAG_SECURE) != 0 + } + return false +} + +fun SecureFlagPolicy.shouldApplySecureFlag(isSecureFlagSetOnParent: Boolean): Boolean { + return when (this) { + SecureFlagPolicy.SecureOff -> false + SecureFlagPolicy.SecureOn -> true + SecureFlagPolicy.Inherit -> isSecureFlagSetOnParent + } +} \ No newline at end of file From 75b7382b797ae6607be3d3ad7232095a825ef8c8 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:45:06 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[Feat]=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../team/ppac/designsystem/foundation/Icon.kt | 10 ++++++++++ .../src/main/res/drawable/ic_stroke.xml | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 core/designsystem/src/main/res/drawable/ic_stroke.xml diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Icon.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Icon.kt index 7e443f47..99183d5e 100644 --- a/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Icon.kt +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Icon.kt @@ -430,4 +430,14 @@ object FarmemeIcon { contentDescription = null, tint = Color.Unspecified ) + + @Composable + fun StrokeDot( + modifier: Modifier = Modifier, + ) = Icon( + modifier = modifier, + painter = painterResource(id = R.drawable.ic_stroke), + contentDescription = null, + tint = Color.Unspecified + ) } \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/ic_stroke.xml b/core/designsystem/src/main/res/drawable/ic_stroke.xml new file mode 100644 index 00000000..158acf41 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_stroke.xml @@ -0,0 +1,18 @@ + + + + + From 1573d1cdb2749e66279caca17bd6c7acb7e311a6 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:45:16 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[Feat]=20Top=20Radius=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/kotlin/team/ppac/designsystem/foundation/Radius.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Radius.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Radius.kt index 9bf8025e..8d8f51a1 100644 --- a/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Radius.kt +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/foundation/Radius.kt @@ -13,5 +13,6 @@ enum class FarmemeRadius(val shape: RoundedCornerShape) { Radius35(RoundedCornerShape(size = 35.dp)), Radius40(RoundedCornerShape(size = 40.dp)), RadiusTop30(RoundedCornerShape(topStart = 30.dp, topEnd = 30.dp)), + RadiusTop20(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)), RadiusBottom30(RoundedCornerShape(bottomStart = 30.dp, bottomEnd = 30.dp)), } \ No newline at end of file From dffb2a6ed6ee9a9569714fba8573300ad513bd39 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:46:22 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[Feat]=20google=20material=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/designsystem/build.gradle.kts | 1 + gradle/libs.versions.toml | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index 59955d7c..58793951 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -28,4 +28,5 @@ dependencies { implementation(platform(libs.compose.bom)) implementation(libs.bundles.compose) implementation(libs.timber) + implementation(libs.google.material) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a86646e8..adf2177a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -63,6 +63,7 @@ activity-ktx = "1.9.0" google-services = "4.4.2" firebase-bom = "33.1.1" firebase-crashlytics = "2.9.9" +materialVersion = "1.12.0" [libraries] # java @@ -138,6 +139,9 @@ firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.r firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } +# google +google-material = { group = "com.google.android.material", name = "material", version.ref = "materialVersion" } + # appsflyer af-android-sdk = { group = "com.appsflyer", name = "af-android-sdk", version.ref = "appsflyer" } From 36788bcdbf6c5cf795f0623c2d90aa03d638e100 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:47:07 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[Feat]=20BottomSheet=20Style=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/values/styles.xml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 core/designsystem/src/main/res/values/styles.xml diff --git a/core/designsystem/src/main/res/values/styles.xml b/core/designsystem/src/main/res/values/styles.xml new file mode 100644 index 00000000..a8e53441 --- /dev/null +++ b/core/designsystem/src/main/res/values/styles.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + \ No newline at end of file From 47d480bbc1f454e99c3adb27d1347c2e7c1e9024 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:50:40 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[Feat]=20=EC=83=81=EC=84=B8=20=ED=83=91?= =?UTF-8?q?=EB=B0=94=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../designsystem/component/toolbar/Toolbar.kt | 2 +- .../java/team/ppac/detail/DetailScreen.kt | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt index e9582a28..a099b347 100644 --- a/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt +++ b/core/designsystem/src/main/kotlin/team/ppac/designsystem/component/toolbar/Toolbar.kt @@ -92,7 +92,7 @@ fun FarmemeBackToolBar(title: String, onBackIconClick: () -> Unit) { } @Composable -internal fun FarmemeToolbar( +fun FarmemeToolbar( modifier: Modifier = Modifier, title: String = "", navigationIcon: (@Composable () -> Unit)? = null, diff --git a/feature/detail/src/main/java/team/ppac/detail/DetailScreen.kt b/feature/detail/src/main/java/team/ppac/detail/DetailScreen.kt index 788bac5d..684f16ad 100644 --- a/feature/detail/src/main/java/team/ppac/detail/DetailScreen.kt +++ b/feature/detail/src/main/java/team/ppac/detail/DetailScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -24,8 +25,10 @@ import coil.request.CachePolicy import coil.request.ImageRequest import team.ppac.designsystem.FarmemeTheme import team.ppac.designsystem.component.scaffold.FarmemeScaffold -import team.ppac.designsystem.component.toolbar.FarmemeBackToolBar +import team.ppac.designsystem.component.toolbar.FarmemeToolbar +import team.ppac.designsystem.foundation.FarmemeIcon import team.ppac.designsystem.foundation.FarmemeRadius +import team.ppac.designsystem.util.extension.noRippleClickable import team.ppac.detail.component.DetailBottomBar import team.ppac.detail.component.DetailContent import team.ppac.detail.mvi.DetailIntent @@ -44,13 +47,27 @@ internal fun DetailScreen( saveBitmap: (bitmap: Bitmap) -> Unit, onHashTagsClick: () -> Unit, currentDetailScreenSize: DetailScreenSize, + onOptionClick: () -> Unit, ) { FarmemeScaffold( modifier = modifier, topBar = { - FarmemeBackToolBar( + FarmemeToolbar( title = "밈 자세히 보기", - onBackIconClick = onClickBackButton, + navigationIcon = { + FarmemeIcon.Back( + modifier = Modifier + .size(20.dp) + .noRippleClickable(onClick = onClickBackButton) + ) + }, + actionIcon = { + FarmemeIcon.StrokeDot( + modifier = Modifier + .size(20.dp) + .noRippleClickable(onClick = onOptionClick) + ) + } ) }, backgroundImage = { @@ -156,6 +173,7 @@ private fun PreviewDetailScreen() { onClickButtonButtons = {}, saveBitmap = {}, onHashTagsClick = {}, + onOptionClick = {}, currentDetailScreenSize = DetailScreenSize.MEDIUM ) } \ No newline at end of file From a966c9842cd4c82a380c171b12bd53c9a865f0c6 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:51:00 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[Feat]=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=ED=91=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/team/ppac/detail/DetailRoute.kt | 21 +++++++++++++++++++ .../java/team/ppac/detail/DetailViewModel.kt | 12 +++++++++++ .../java/team/ppac/detail/mvi/DetailIntent.kt | 3 +++ .../team/ppac/detail/mvi/DetailUiState.kt | 3 +++ 4 files changed, 39 insertions(+) diff --git a/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt b/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt index 12d00b9c..ee605c1b 100644 --- a/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt +++ b/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt @@ -2,9 +2,12 @@ package team.ppac.detail import android.graphics.Bitmap import androidx.compose.animation.Crossfade +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -15,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -34,7 +38,9 @@ import team.ppac.common.android.component.error.FarmemeErrorScreen import team.ppac.common.android.util.ComposableLifecycle import team.ppac.common.android.util.copyImageToClipBoard import team.ppac.common.android.util.shareOneLink +import team.ppac.designsystem.FarmemeTheme import team.ppac.designsystem.R +import team.ppac.designsystem.component.dialog.FarmemeBottomSheetDialog import team.ppac.detail.mvi.DetailIntent import team.ppac.detail.mvi.DetailSideEffect import team.ppac.detail.util.DetailScreenSize @@ -165,6 +171,20 @@ internal fun DetailRoute( } } + if (uiState.showOptionBottomSheet) { + FarmemeBottomSheetDialog( + onBottomSheetDismiss = { viewModel.intent(DetailIntent.ClickBottomSheetDismiss) } + ) { + Text( + modifier = Modifier + .padding(vertical = 15.dp) + .background(FarmemeTheme.backgroundColor.white), + text = "신고하기", + textAlign = TextAlign.Center + ) + } + } + Crossfade(targetState = uiState.isError) { isError -> if (isError) { FarmemeErrorScreen( @@ -188,6 +208,7 @@ internal fun DetailRoute( onClickButtonButtons = viewModel::intent, saveBitmap = saveBitmap, onHashTagsClick = { viewModel.intent(DetailIntent.ClickHashtags) }, + onOptionClick = { viewModel.intent(DetailIntent.ClickOption) }, currentDetailScreenSize = currentDetailScreenSize, ) LottieAnimation( diff --git a/feature/detail/src/main/java/team/ppac/detail/DetailViewModel.kt b/feature/detail/src/main/java/team/ppac/detail/DetailViewModel.kt index 45585159..d2a84631 100644 --- a/feature/detail/src/main/java/team/ppac/detail/DetailViewModel.kt +++ b/feature/detail/src/main/java/team/ppac/detail/DetailViewModel.kt @@ -114,9 +114,21 @@ class DetailViewModel @Inject constructor( is DetailIntent.ClickHashtags -> { postSideEffect(DetailSideEffect.LogHashTagsClicked) } + + is DetailIntent.ClickOption -> { + showOptionBottomSheet(true) + } + + is DetailIntent.ClickBottomSheetDismiss -> { + showOptionBottomSheet(false) + } } } + private fun showOptionBottomSheet(showOptionBottomSheet: Boolean) { + reduce { copy(showOptionBottomSheet = showOptionBottomSheet) } + } + private suspend fun getMeme(memeId: String) { reduce { copy(isLoading = true) } val meme = getMemeUseCase(memeId) diff --git a/feature/detail/src/main/java/team/ppac/detail/mvi/DetailIntent.kt b/feature/detail/src/main/java/team/ppac/detail/mvi/DetailIntent.kt index 421db0c9..995f4682 100644 --- a/feature/detail/src/main/java/team/ppac/detail/mvi/DetailIntent.kt +++ b/feature/detail/src/main/java/team/ppac/detail/mvi/DetailIntent.kt @@ -11,6 +11,9 @@ sealed interface DetailIntent : UiIntent { data object ClickHashtags : DetailIntent + data object ClickOption : DetailIntent + data object ClickBottomSheetDismiss : DetailIntent + sealed interface ClickBottomButton : DetailIntent { data object Copy : ClickBottomButton data class Farmeme(val isSavedMeme: Boolean) : ClickBottomButton diff --git a/feature/detail/src/main/java/team/ppac/detail/mvi/DetailUiState.kt b/feature/detail/src/main/java/team/ppac/detail/mvi/DetailUiState.kt index e5373a0c..b62eef6f 100644 --- a/feature/detail/src/main/java/team/ppac/detail/mvi/DetailUiState.kt +++ b/feature/detail/src/main/java/team/ppac/detail/mvi/DetailUiState.kt @@ -9,6 +9,7 @@ data class DetailUiState( val detailMemeUiModel: DetailMemeUiModel, val isError: Boolean, val isLoading: Boolean, + val showOptionBottomSheet: Boolean, ) : UiState { companion object { @@ -25,6 +26,7 @@ data class DetailUiState( ), isError = false, isLoading = false, + showOptionBottomSheet = false, ) val PREVIEW_STATE = DetailUiState( @@ -40,6 +42,7 @@ data class DetailUiState( ), isError = false, isLoading = false, + showOptionBottomSheet = false, ) } } From 2f89c8e1a23abad669d1d36908418e95d4fcdb30 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:59:08 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[Feat]=20=EC=99=B8=EB=B6=80=20=EC=9B=B9?= =?UTF-8?q?=EB=B7=B0=20=EB=A1=9C=EB=93=9C=20extension=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ppac/common/android/extension/ActivityExtension.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/common/android/src/main/kotlin/team/ppac/common/android/extension/ActivityExtension.kt b/core/common/android/src/main/kotlin/team/ppac/common/android/extension/ActivityExtension.kt index 2684abc1..2a730740 100644 --- a/core/common/android/src/main/kotlin/team/ppac/common/android/extension/ActivityExtension.kt +++ b/core/common/android/src/main/kotlin/team/ppac/common/android/extension/ActivityExtension.kt @@ -2,6 +2,7 @@ package team.ppac.common.android.extension import android.app.Activity import android.content.Intent +import android.net.Uri import androidx.core.app.ActivityCompat import kotlin.system.exitProcess @@ -16,4 +17,13 @@ inline fun Activity.getIntent( fun Activity.forceKillApplication() { ActivityCompat.finishAffinity(this) exitProcess(0) +} + +fun Activity.openExternalWebView(url: String) { + val webUrl = if (url.startsWith("http")) url else "https://$url" + + val intent = Intent() + intent.action = Intent.ACTION_VIEW + intent.data = Uri.parse(webUrl) + startActivity(intent) } \ No newline at end of file From 16f56c516b9ac0ddf03106c652373dac83e03312 Mon Sep 17 00:00:00 2001 From: JaesungLeee Date: Thu, 3 Oct 2024 23:59:17 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[Feat]=20=EC=8B=A0=EA=B3=A0=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20=ED=8F=BC=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/team/ppac/detail/DetailActivity.kt | 8 ++++++-- .../src/main/java/team/ppac/detail/DetailRoute.kt | 12 ++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/feature/detail/src/main/java/team/ppac/detail/DetailActivity.kt b/feature/detail/src/main/java/team/ppac/detail/DetailActivity.kt index 116409d4..1f58a388 100644 --- a/feature/detail/src/main/java/team/ppac/detail/DetailActivity.kt +++ b/feature/detail/src/main/java/team/ppac/detail/DetailActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent import androidx.core.view.WindowCompat import dagger.hilt.android.AndroidEntryPoint import team.ppac.analytics.AnalyticsHelper +import team.ppac.common.android.extension.openExternalWebView import team.ppac.common.android.util.noTransitionAnimation import team.ppac.designsystem.FarmemeTheme import javax.inject.Inject @@ -18,15 +19,18 @@ class DetailActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + noTransitionAnimation() WindowCompat.setDecorFitsSystemWindows(window, false) setContent { FarmemeTheme { DetailRoute( analyticsHelper = analyticsHelper, - navigateToBack = { finish() } + navigateToBack = { finish() }, + navigateToReport = { + openExternalWebView("https://forms.gle/a5QkMnLD8AANtYCo7") + } ) } } - noTransitionAnimation() } } \ No newline at end of file diff --git a/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt b/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt index ee605c1b..28a3d82c 100644 --- a/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt +++ b/feature/detail/src/main/java/team/ppac/detail/DetailRoute.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -41,6 +42,7 @@ import team.ppac.common.android.util.shareOneLink import team.ppac.designsystem.FarmemeTheme import team.ppac.designsystem.R import team.ppac.designsystem.component.dialog.FarmemeBottomSheetDialog +import team.ppac.designsystem.util.extension.noRippleClickable import team.ppac.detail.mvi.DetailIntent import team.ppac.detail.mvi.DetailSideEffect import team.ppac.detail.util.DetailScreenSize @@ -52,6 +54,7 @@ internal fun DetailRoute( analyticsHelper: AnalyticsHelper, viewModel: DetailViewModel = hiltViewModel(), navigateToBack: () -> Unit, + navigateToReport: () -> Unit, ) { val context = LocalContext.current val configuration = LocalConfiguration.current @@ -177,10 +180,15 @@ internal fun DetailRoute( ) { Text( modifier = Modifier + .fillMaxWidth() .padding(vertical = 15.dp) - .background(FarmemeTheme.backgroundColor.white), + .background(FarmemeTheme.backgroundColor.white) + .noRippleClickable(onClick = navigateToReport), text = "신고하기", - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + style = FarmemeTheme.typography.body.xLarge.medium.copy( + color = FarmemeTheme.textColor.primary + ) ) } }