Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Prezel/core/designsystem/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ dependencies {
implementation(libs.coil.kt.compose)
implementation(libs.kotlinx.datetime)
implementation(libs.timber)

implementation(libs.balloon.compose)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package com.team.prezel.core.designsystem.component.feedback.tooltip

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.geometry.Rect
import androidx.compose.ui.graphics.toComposeRect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.skydoves.balloon.ArrowPositionRules
import com.skydoves.balloon.Balloon
import com.skydoves.balloon.BalloonAnimation
import com.skydoves.balloon.compose.balloon
import com.skydoves.balloon.compose.rememberBalloonBuilder
import com.skydoves.balloon.compose.rememberBalloonState
import com.skydoves.balloon.compose.setBackgroundColor
import com.team.prezel.core.designsystem.R
import com.team.prezel.core.designsystem.icon.PrezelIcons
import com.team.prezel.core.designsystem.preview.BasicPreview
import com.team.prezel.core.designsystem.preview.PreviewScaffold
import com.team.prezel.core.designsystem.theme.PrezelColorScheme
import com.team.prezel.core.designsystem.theme.PrezelTheme

@Composable
fun PrezelTooltipBox(
text: String,
modifier: Modifier = Modifier,
showDismissIcon: Boolean = true,
showArrow: Boolean = true,
content: @Composable BoxScope.() -> Unit,
) {
val builder = rememberBalloonBuilder(showArrow)
val state = rememberBalloonState(builder)
val view = LocalView.current
val visibleFrame = remember { android.graphics.Rect() }
var shouldRestoreTooltip by remember { mutableStateOf(false) }
var isAnchorVisibleInWindow by remember { mutableStateOf(true) }

LaunchedEffect(shouldRestoreTooltip, isAnchorVisibleInWindow) {
if (shouldRestoreTooltip && isAnchorVisibleInWindow) state.showAlignTop() else state.dismiss()
}

Box(
content = content,
modifier = modifier
.noRippleClick { shouldRestoreTooltip = true }
.onGloballyPositioned { coordinates ->
view.getWindowVisibleDisplayFrame(visibleFrame)
isAnchorVisibleInWindow = coordinates.boundsInWindow().intersects(visibleFrame.toComposeRect())
}.balloon(state) {
TooltipContent(
text = text,
showDismissIcon = showDismissIcon,
modifier = Modifier.noRippleClick {
shouldRestoreTooltip = false
state.dismiss()
},
)
},
)
}

@Composable
private fun rememberBalloonBuilder(showArrow: Boolean): Balloon.Builder =
rememberBalloonBuilder {
setIsVisibleArrow(showArrow)
setArrowWidth(12)
setArrowHeight(6)
setArrowPosition(0.5f)
setArrowPositionRules(ArrowPositionRules.ALIGN_ANCHOR)
setCornerRadius(6f)
setPaddingVertical(4)
setPaddingLeft(8)
setPaddingRight(6)
setDismissWhenTouchOutside(false)
setBalloonAnimation(BalloonAnimation.NONE)
setBackgroundColor(PrezelColorScheme.Dark.bgMedium)
}

private fun Rect.intersects(other: Rect): Boolean = left < other.right && right > other.left && top < other.bottom && bottom > other.top

@Composable
private fun Modifier.noRippleClick(onClick: () -> Unit): Modifier =
this.clickable(
interactionSource = null,
indication = null,
onClick = onClick,
)

@Composable
private fun TooltipContent(
text: String,
modifier: Modifier = Modifier,
showDismissIcon: Boolean = false,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Text(
text = text,
style = PrezelTheme.typography.caption1Regular,
color = PrezelColorScheme.Dark.textLarge,
)

if (showDismissIcon) {
Spacer(modifier = Modifier.width(PrezelTheme.spacing.V2))
Icon(
painter = painterResource(PrezelIcons.Cancel),
contentDescription = stringResource(R.string.core_designsystem_tooltip_cancel_btn_content_desc),
tint = PrezelColorScheme.Dark.iconLarge,
modifier = Modifier
.size(14.dp)
.offset(x = 2.dp),
)
}
}
}

@BasicPreview
@Composable
private fun PrezelTooltipBoxPreview() {
PrezelTheme {
PreviewScaffold(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Text(text = "정확도", style = PrezelTheme.typography.title2Bold)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
PrezelTooltipBox(text = "SPM은 1분당 말하는 음절의 수에요.") {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Label", style = PrezelTheme.typography.body3Regular)
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painterResource(PrezelIcons.Blank),
contentDescription = "",
)
}
}

PrezelTooltipBox(text = "SPM은 1분당 말하는 음절의 수에요.") {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Label", style = PrezelTheme.typography.body3Regular)
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painterResource(PrezelIcons.Blank),
contentDescription = "",
)
}
}

PrezelTooltipBox(text = "SPM은 1분당 말하는 음절의 수에요.") {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = "Label", style = PrezelTheme.typography.body3Regular)
Spacer(modifier = Modifier.width(8.dp))
Icon(
painter = painterResource(PrezelIcons.Blank),
contentDescription = "",
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1000.dp),
contentAlignment = Alignment.Center,
) {
Text(text = "This is Blank for Scroll")
}
}
}
}
}
1 change: 1 addition & 0 deletions Prezel/core/designsystem/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
<string name="core_designsystem_checkbox_desc">체크박스</string>
<string name="core_designsystem_date_picker_confirm_btn">선택하기</string>
<string name="core_designsystem_date_picker_month_title">%1$d년 %2$d월</string>
<string name="core_designsystem_tooltip_cancel_btn_content_desc">툴팁 닫기</string>

<string-array name="core_designsystem_weekday_labels">
<item>일</item>
Expand Down
3 changes: 3 additions & 0 deletions Prezel/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ kotlinxCollectionsImmutable = "0.4.0"
kotlinxSerialization = "1.9.0"
kakao = "2.23.2"
lottie = "6.6.7"
balloonCompose = "1.7.6"

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand Down Expand Up @@ -77,7 +78,9 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

kakao-user = { module = "com.kakao.sdk:v2-user", version.ref = "kakao" }

lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" }
balloon-compose = { module = "com.github.skydoves:balloon-compose", version.ref = "balloonCompose" }

# Dependencies of the included build-logic
android-gradleApiPlugin = { group = "com.android.tools.build", name = "gradle-api", version.ref = "agp" }
Expand Down